Del 8 av 10 Moderat

Komplett Discord Bot Guide

Del 8: Avanserte Features

Fremgang: 8/10 (80%)

Del 8: Avanserte Features

Nå skal vi lage de virkelig kule funksjonene! Dette er hvor boten din går fra "grunnleggende" til "imponerende" med avanserte systemer som XP belønninger, auto-roles og mye mer.

Hva lærer vi i denne delen?

  1. Avansert XP system - Belønninger, multipliers, leaderboard improvements
  2. Auto-role system - Gi roller basert på aktivitet og levels
  3. Custom embed builder - La brukere lage fancy meldinger
  4. Reaction role system - Få roller ved å reagere på meldinger
  5. Voice activity tracking - XP for tid i voice kanaler
  6. Economy system - Virtuell økonomi med coins
  7. Server settings - Konfigurerbare bot innstillinger per server

Steg 1: Avansert XP System

1.1: Oppdater database for XP features

Legg til i database/database.js - i init():

javascript
// XP multipliers og belønninger
this.db.exec(`
    CREATE TABLE IF NOT EXISTS xp_rewards (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        guild_id TEXT NOT NULL,
        level INTEGER NOT NULL,
        reward_type TEXT NOT NULL, -- 'role', 'coins', 'title'
        reward_value TEXT NOT NULL, -- role_id, coin_amount, eller title_name
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE(guild_id, level)
    )
`);

// Economy system  
this.db.exec(`
    CREATE TABLE IF NOT EXISTS user_economy (
        guild_id TEXT NOT NULL,
        user_id TEXT NOT NULL,
        coins INTEGER DEFAULT 0,
        daily_streak INTEGER DEFAULT 0,
        last_daily DATETIME,
        total_earned INTEGER DEFAULT 0,
        total_spent INTEGER DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (guild_id, user_id)
    )
`);

// Voice activity tracking
this.db.exec(`
    CREATE TABLE IF NOT EXISTS voice_sessions (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        guild_id TEXT NOT NULL,
        user_id TEXT NOT NULL,
        channel_id TEXT NOT NULL,
        joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        left_at DATETIME,
        duration_minutes INTEGER
    )
`);

Legg til nye database funksjoner:

javascript
// XP Reward functions
addXPReward(guildId, level, rewardType, rewardValue) {
    const stmt = this.db.prepare(`
        INSERT OR REPLACE INTO xp_rewards 
        (guild_id, level, reward_type, reward_value)
        VALUES (?, ?, ?, ?)
    `);
    return stmt.run(guildId, level, rewardType, rewardValue);
}

getXPReward(guildId, level) {
    const stmt = this.db.prepare(`
        SELECT * FROM xp_rewards 
        WHERE guild_id = ? AND level = ?
    `);
    return stmt.get(guildId, level);
}

getAllXPRewards(guildId) {
    const stmt = this.db.prepare(`
        SELECT * FROM xp_rewards 
        WHERE guild_id = ? 
        ORDER BY level ASC
    `);
    return stmt.all(guildId);
}

// Economy functions  
getUserEconomy(guildId, userId) {
    const stmt = this.db.prepare(`
        SELECT * FROM user_economy 
        WHERE guild_id = ? AND user_id = ?
    `);
    return stmt.get(guildId, userId);
}

createUserEconomy(guildId, userId) {
    const stmt = this.db.prepare(`
        INSERT OR IGNORE INTO user_economy (guild_id, user_id)
        VALUES (?, ?)
    `);
    return stmt.run(guildId, userId);
}

addCoins(guildId, userId, amount) {
    this.createUserEconomy(guildId, userId);
    const stmt = this.db.prepare(`
        UPDATE user_economy 
        SET coins = coins + ?, 
            total_earned = total_earned + ?
        WHERE guild_id = ? AND user_id = ?
    `);
    return stmt.run(amount, amount, guildId, userId);
}

removeCoins(guildId, userId, amount) {
    this.createUserEconomy(guildId, userId);
    const stmt = this.db.prepare(`
        UPDATE user_economy 
        SET coins = coins - ?, 
            total_spent = total_spent + ?
        WHERE guild_id = ? AND user_id = ? AND coins >= ?
    `);
    return stmt.run(amount, amount, guildId, userId, amount);
}

// Voice tracking functions
startVoiceSession(guildId, userId, channelId) {
    const stmt = this.db.prepare(`
        INSERT INTO voice_sessions (guild_id, user_id, channel_id)
        VALUES (?, ?, ?)
    `);
    return stmt.run(guildId, userId, channelId);
}

endVoiceSession(guildId, userId) {
    const stmt = this.db.prepare(`
        UPDATE voice_sessions 
        SET left_at = CURRENT_TIMESTAMP,
            duration_minutes = (
                strftime('%s', CURRENT_TIMESTAMP) - strftime('%s', joined_at)
            ) / 60
        WHERE guild_id = ? AND user_id = ? AND left_at IS NULL
    `);
    return stmt.run(guildId, userId);
}

getVoiceTime(guildId, userId, days = 30) {
    const stmt = this.db.prepare(`
        SELECT COALESCE(SUM(duration_minutes), 0) as total_minutes
        FROM voice_sessions 
        WHERE guild_id = ? AND user_id = ? 
        AND datetime(joined_at) >= datetime('now', '-${days} days')
    `);
    return stmt.get(guildId, userId).total_minutes;
}

1.2: Oppdater XP system med belønninger

Rediger events/message/messageCreate.js - erstatt XP delen:

javascript
// XP System med belønninger
const userData = database.getUser(message.author.id);
const oldLevel = userData ? userData.level : 0;

// XP multiplier basert på tid på dagen og kanal type
let xpMultiplier = 1;

// Bonus XP på kvelden (18-23)
const hour = new Date().getHours();
if (hour >= 18 && hour <= 23) xpMultiplier += 0.5;

// Bonus XP i visse kanaler
const bonusChannels = ['general', 'chat', 'discussion'];
if (bonusChannels.some(name => message.channel.name.includes(name))) {
    xpMultiplier += 0.3;
}

// Random XP med multiplier
const baseXP = Math.floor(Math.random() * 10) + 5; // 5-14 base XP  
const finalXP = Math.floor(baseXP * xpMultiplier);

database.addXP(message.author.id, finalXP);

// Gi coins for aktivitet
database.addCoins(message.guild.id, message.author.id, Math.floor(finalXP / 2));

// Sjekk for level up og belønninger
const newUserData = database.getUser(message.author.id);
const newLevel = newUserData.level;

if (newLevel > oldLevel) {
    message.react('🎉');
    
    // Sjekk for belønninger
    const reward = database.getXPReward(message.guild.id, newLevel);
    let rewardText = '';
    
    if (reward) {
        if (reward.reward_type === 'role') {
            const role = message.guild.roles.cache.get(reward.reward_value);
            if (role) {
                try {
                    const member = await message.guild.members.fetch(message.author.id);
                    await member.roles.add(role);
                    rewardText = `\n🎁 **Belønning:** Du fikk rollen ${role.name}!`;
                } catch (roleError) {
                    console.error('❌ Kunne ikke gi rolle belønning:', roleError);
                }
            }
        } else if (reward.reward_type === 'coins') {
            const coinAmount = parseInt(reward.reward_value);
            database.addCoins(message.guild.id, message.author.id, coinAmount);
            rewardText = `\n🪙 **Belønning:** ${coinAmount} coins!`;
        }
    }
    
    await message.reply(`🎉 Gratulerer ${message.author}! Du nådde level ${newLevel}! ${rewardText}`);
    console.log(`🎉 ${message.author.tag} nådde level ${newLevel}`);
}

🗣️ Steg 2: Voice Activity Tracking

2.1: Voice state events

Lag events/voice/voiceStateUpdate.js:

javascript
module.exports = {
    name: 'voiceStateUpdate',
    async execute(oldState, newState) {
        const database = newState.client.database;
        const guildId = newState.guild.id;
        const userId = newState.member.user.id;
        
        // Bruker jointet voice kanal
        if (!oldState.channelId && newState.channelId) {
            console.log(`🎤 ${newState.member.user.tag} jointet voice kanal: ${newState.channel.name}`);
            
            database.startVoiceSession(guildId, userId, newState.channelId);
            
            // Opprett bruker hvis ikke finnes
            database.createOrUpdateUser(
                userId, 
                newState.member.user.username, 
                newState.member.user.discriminator
            );
        }
        
        // Bruker forlot voice kanal
        if (oldState.channelId && !newState.channelId) {
            console.log(`🎤 ${newState.member.user.tag} forlot voice kanal`);
            
            database.endVoiceSession(guildId, userId);
            
            // Gi XP for voice aktivitet (1 XP per minutt, minimum 1 minutt)
            const voiceTime = database.getVoiceTime(guildId, userId, 1); // Siste dag
            if (voiceTime >= 1) {
                const voiceXP = Math.floor(voiceTime); // 1 XP per minutt i voice
                database.addXP(userId, voiceXP);
                
                // Gi coins også
                database.addCoins(guildId, userId, Math.floor(voiceXP / 2));
                
                console.log(`🎤 Ga ${newState.member.user.tag} ${voiceXP} XP for ${Math.floor(voiceTime)} minutter i voice`);
            }
        }
        
        // Bruker byttet voice kanal
        if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId) {
            console.log(`🎤 ${newState.member.user.tag} byttet fra ${oldState.channel.name} til ${newState.channel.name}`);
            
            // End current session og start ny
            database.endVoiceSession(guildId, userId);
            database.startVoiceSession(guildId, userId, newState.channelId);
        }
    },
};

🎭 Steg 3: Reaction Role System

3.1: Reaction role database

Legg til i database/database.js:

javascript
// Reaction roles
this.db.exec(`
    CREATE TABLE IF NOT EXISTS reaction_roles (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        guild_id TEXT NOT NULL,
        message_id TEXT NOT NULL,
        emoji TEXT NOT NULL,
        role_id TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        UNIQUE(message_id, emoji)
    )
`);

// Reaction role functions
addReactionRole(guildId, messageId, emoji, roleId) {
    const stmt = this.db.prepare(`
        INSERT OR REPLACE INTO reaction_roles 
        (guild_id, message_id, emoji, role_id)
        VALUES (?, ?, ?, ?)
    `);
    return stmt.run(guildId, messageId, emoji, roleId);
}

getReactionRole(messageId, emoji) {
    const stmt = this.db.prepare(`
        SELECT * FROM reaction_roles 
        WHERE message_id = ? AND emoji = ?
    `);
    return stmt.get(messageId, emoji);
}

getReactionRoles(messageId) {
    const stmt = this.db.prepare(`
        SELECT * FROM reaction_roles 
        WHERE message_id = ?
    `);
    return stmt.all(messageId);
}

removeReactionRole(messageId, emoji) {
    const stmt = this.db.prepare(`
        DELETE FROM reaction_roles 
        WHERE message_id = ? AND emoji = ?
    `);
    return stmt.run(messageId, emoji);
}

3.2: Reaction role commands

Lag commands/admin/reactionrole.js:

javascript
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const permissions = require('../../utils/permissions.js');

module.exports = {
    data: new SlashCommandBuilder()
        .setName('reactionrole')
        .setDescription('Sett opp reaction roles')
        .addSubcommand(subcommand =>
            subcommand.setName('add')
                .setDescription('Legg til reaction role')
                .addStringOption(option =>
                    option.setName('message_id')
                        .setDescription('ID på meldingen som skal ha reaction role')
                        .setRequired(true)
                )
                .addStringOption(option =>
                    option.setName('emoji')
                        .setDescription('Emoji som gir rolle (f.eks. 🎮 eller :custom_emoji:)')
                        .setRequired(true)
                )
                .addRoleOption(option =>
                    option.setName('rolle')
                        .setDescription('Rollen som skal gis')
                        .setRequired(true)
                )
        )
        .addSubcommand(subcommand =>
            subcommand.setName('remove')
                .setDescription('Fjern reaction role')
                .addStringOption(option =>
                    option.setName('message_id')
                        .setDescription('ID på meldingen')
                        .setRequired(true)
                )
                .addStringOption(option =>
                    option.setName('emoji')
                        .setDescription('Emoji som skal fjernes')
                        .setRequired(true)
                )
        )
        .addSubcommand(subcommand =>
            subcommand.setName('list')
                .setDescription('Vis alle reaction roles i denne serveren')
        ),
    
    async execute(interaction) {
        if (!permissions.hasStaffRole(interaction.member, 'ADMIN')) {
            return interaction.reply({
                content: '❌ Du trenger administrator tillatelser for å bruke denne kommandoen.',
                ephemeral: true
            });
        }
        
        const database = interaction.client.database;
        const subcommand = interaction.options.getSubcommand();
        
        if (subcommand === 'add') {
            const messageId = interaction.options.getString('message_id');
            const emoji = interaction.options.getString('emoji');
            const role = interaction.options.getRole('rolle');
            
            // Prøv å finne meldingen
            let message;
            try {
                message = await interaction.channel.messages.fetch(messageId);
            } catch (error) {
                return interaction.reply({
                    content: '❌ Kunne ikke finne melding med den ID-en i denne kanalen.',
                    ephemeral: true
                });
            }
            
            // Legg til reaction role i database
            database.addReactionRole(interaction.guild.id, messageId, emoji, role.id);
            
            // Legg til reaction på meldingen
            try {
                await message.react(emoji);
            } catch (error) {
                return interaction.reply({
                    content: '❌ Kunne ikke reagere med den emoji. Sjekk at emoji er gyldig.',
                    ephemeral: true
                });
            }
            
            const embed = new EmbedBuilder()
                .setColor(0x2ecc71)
                .setTitle('✅ Reaction Role Lagt Til')
                .addFields(
                    { name: '📝 Melding ID', value: messageId, inline: true },
                    { name: '😀 Emoji', value: emoji, inline: true },
                    { name: '🎭 Rolle', value: role.toString(), inline: true }
                )
                .setTimestamp();
            
            await interaction.reply({ embeds: [embed], ephemeral: true });
            
        } else if (subcommand === 'remove') {
            const messageId = interaction.options.getString('message_id');
            const emoji = interaction.options.getString('emoji');
            
            const removed = database.removeReactionRole(messageId, emoji);
            
            if (removed.changes > 0) {
                await interaction.reply({
                    content: `✅ Reaction role for ${emoji} på melding ${messageId} er fjernet.`,
                    ephemeral: true
                });
            } else {
                await interaction.reply({
                    content: '❌ Fant ikke den reaction role kombinasjonen.',
                    ephemeral: true
                });
            }
            
        } else if (subcommand === 'list') {
            // Implementer liste funksjonalitet
            await interaction.reply({
                content: '📋 Reaction roles liste funksjon kommer snart!',
                ephemeral: true
            });
        }
    },
};

3.3: Reaction role event handler

Oppdater events/message/messageReactionAdd.js:

javascript
module.exports = {
    name: 'messageReactionAdd',
    async execute(reaction, user) {
        if (user.bot) return;
        
        // Handle partial reactions
        if (reaction.partial) {
            try {
                await reaction.fetch();
            } catch (error) {
                console.error('Could not fetch reaction:', error);
                return;
            }
        }
        
        const database = reaction.client.database;
        const emojiName = reaction.emoji.name;
        
        // Sjekk om dette er en reaction role
        const reactionRole = database.getReactionRole(reaction.message.id, emojiName);
        
        if (reactionRole) {
            const guild = reaction.message.guild;
            const member = guild.members.cache.get(user.id);
            const role = guild.roles.cache.get(reactionRole.role_id);
            
            if (role && member && !member.roles.cache.has(role.id)) {
                try {
                    await member.roles.add(role);
                    console.log(`✅ Ga ${user.tag} rollen ${role.name} via reaction role`);
                    
                    // Send DM bekrefelse
                    try {
                        const dmEmbed = new EmbedBuilder()
                            .setColor(0x2ecc71)
                            .setTitle('🎭 Rolle Gitt!')
                            .setDescription(`Du har fått rollen **${role.name}** i ${guild.name}!`)
                            .setThumbnail(guild.iconURL())
                            .setTimestamp();
                        
                        await user.send({ embeds: [dmEmbed] });
                    } catch (dmError) {
                        // Ignore DM errors
                    }
                    
                } catch (error) {
                    console.error('❌ Kunne ikke gi rolle via reaction:', error);
                }
            }
        }
    },
};

// Lag også messageReactionRemove.js

Lag events/message/messageReactionRemove.js:

javascript
module.exports = {
    name: 'messageReactionRemove',
    async execute(reaction, user) {
        if (user.bot) return;
        
        if (reaction.partial) {
            try {
                await reaction.fetch();
            } catch (error) {
                console.error('Could not fetch reaction:', error);
                return;
            }
        }
        
        const database = reaction.client.database;
        const emojiName = reaction.emoji.name;
        
        // Sjekk om dette er en reaction role
        const reactionRole = database.getReactionRole(reaction.message.id, emojiName);
        
        if (reactionRole) {
            const guild = reaction.message.guild;
            const member = guild.members.cache.get(user.id);
            const role = guild.roles.cache.get(reactionRole.role_id);
            
            if (role && member && member.roles.cache.has(role.id)) {
                try {
                    await member.roles.remove(role);
                    console.log(`➖ Fjernet rollen ${role.name} fra ${user.tag} via reaction role`);
                } catch (error) {
                    console.error('❌ Kunne ikke fjerne rolle via reaction:', error);
                }
            }
        }
    },
};

💰 Steg 4: Economy System

4.1: Balance command

Lag commands/economy/balance.js:

javascript
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');

module.exports = {
    data: new SlashCommandBuilder()
        .setName('balance')
        .setDescription('Sjekk din eller andres økonomi')
        .addUserOption(option =>
            option.setName('bruker')
                .setDescription('Brukeren å sjekke (valgfritt)')
                .setRequired(false)
        ),
    
    async execute(interaction) {
        const target = interaction.options.getUser('bruker') || interaction.user;
        const database = interaction.client.database;
        
        // Opprett økonomi hvis ikke finnes
        database.createUserEconomy(interaction.guild.id, target.id);
        
        // Hent økonomi data
        const economy = database.getUserEconomy(interaction.guild.id, target.id);
        const userData = database.getUser(target.id);
        
        const embed = new EmbedBuilder()
            .setColor(0xf1c40f)
            .setTitle(`💰 ${target.username} sin økonomi`)
            .setThumbnail(target.displayAvatarURL())
            .addFields(
                { 
                    name: '🪙 Nåværende balanse', 
                    value: `${economy.coins.toLocaleString()} coins`, 
                    inline: true 
                },
                { 
                    name: '📈 Totalt tjent', 
                    value: `${economy.total_earned.toLocaleString()} coins`, 
                    inline: true 
                },
                { 
                    name: '💸 Totalt brukt', 
                    value: `${economy.total_spent.toLocaleString()} coins`, 
                    inline: true 
                },
                { 
                    name: '🔥 Daily streak', 
                    value: `${economy.daily_streak} dager`, 
                    inline: true 
                },
                { 
                    name: '🏆 Level', 
                    value: userData ? userData.level.toString() : '0', 
                    inline: true 
                },
                { 
                    name: '⭐ Total XP', 
                    value: userData ? userData.xp.toLocaleString() : '0', 
                    inline: true 
                }
            )
            .setFooter({
                text: 'Bruk /daily for å få daglige coins!',
                iconURL: interaction.guild.iconURL()
            })
            .setTimestamp();
        
        await interaction.reply({ embeds: [embed] });
    },
};

4.2: Daily rewards

Lag commands/economy/daily.js:

javascript
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');

module.exports = {
    data: new SlashCommandBuilder()
        .setName('daily')
        .setDescription('Få dine daglige coins!'),
    
    async execute(interaction) {
        const database = interaction.client.database;
        const guildId = interaction.guild.id;
        const userId = interaction.user.id;
        
        // Opprett økonomi hvis ikke finnes
        database.createUserEconomy(guildId, userId);
        
        const economy = database.getUserEconomy(guildId, userId);
        const lastDaily = economy.last_daily ? new Date(economy.last_daily) : null;
        const now = new Date();
        
        // Sjekk om bruker kan få daily
        if (lastDaily) {
            const timeSinceDaily = now - lastDaily;
            const hoursLeft = 24 - (timeSinceDaily / (1000 * 60 * 60));
            
            if (hoursLeft > 0) {
                const embed = new EmbedBuilder()
                    .setColor(0xe74c3c)
                    .setTitle('⏰ Daily allerede hentet!')
                    .setDescription(`Du kan få din neste daily om **${Math.ceil(hoursLeft)} timer**.`)
                    .setFooter({ text: 'Kom tilbake senere!' })
                    .setTimestamp();
                
                return interaction.reply({ embeds: [embed], ephemeral: true });
            }
        }
        
        // Kalkuler streak bonus
        let streak = economy.daily_streak;
        const yesterday = new Date(now);
        yesterday.setDate(yesterday.getDate() - 1);
        
        if (lastDaily && lastDaily.toDateString() === yesterday.toDateString()) {
            streak += 1; // Fortsett streak
        } else if (!lastDaily || lastDaily.toDateString() !== yesterday.toDateString()) {
            streak = 1; // Start ny streak
        }
        
        // Kalkuler coins (base 100, +10 per streak dag, max 500)
        const baseCoins = 100;
        const streakBonus = Math.min(streak * 10, 400);
        const totalCoins = baseCoins + streakBonus;
        
        // Oppdater database
        const stmt = database.db.prepare(`
            UPDATE user_economy 
            SET coins = coins + ?,
                daily_streak = ?,
                last_daily = CURRENT_TIMESTAMP,
                total_earned = total_earned + ?
            WHERE guild_id = ? AND user_id = ?
        `);
        stmt.run(totalCoins, streak, totalCoins, guildId, userId);
        
        const embed = new EmbedBuilder()
            .setColor(0x2ecc71)
            .setTitle('🎁 Daily Coins Hentet!')
            .addFields(
                { 
                    name: '🪙 Coins mottatt', 
                    value: `${totalCoins.toLocaleString()}`, 
                    inline: true 
                },
                { 
                    name: '🔥 Streak', 
                    value: `${streak} dager`, 
                    inline: true 
                },
                { 
                    name: '💰 Ny balanse', 
                    value: `${(economy.coins + totalCoins).toLocaleString()}`, 
                    inline: true 
                }
            )
            .setDescription(`${baseCoins} base coins + ${streakBonus} streak bonus!`)
            .setThumbnail(interaction.user.displayAvatarURL())
            .setFooter({ 
                text: 'Kom tilbake i morgen for mer coins!',
                iconURL: interaction.guild.iconURL()
            })
            .setTimestamp();
        
        await interaction.reply({ embeds: [embed] });
        
        console.log(`💰 ${interaction.user.tag} hentet daily: ${totalCoins} coins (streak: ${streak})`);
    },
};

🎨 Steg 5: Custom Embed Builder

5.1: Embed builder command

Lag commands/utility/embed.js:

javascript
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const permissions = require('../../utils/permissions.js');

module.exports = {
    data: new SlashCommandBuilder()
        .setName('embed')
        .setDescription('Lag custom embed meldinger')
        .addStringOption(option =>
            option.setName('title')
                .setDescription('Embed tittel')
                .setRequired(true)
                .setMaxLength(256)
        )
        .addStringOption(option =>
            option.setName('description')
                .setDescription('Embed beskrivelse')
                .setRequired(false)
                .setMaxLength(2048)
        )
        .addStringOption(option =>
            option.setName('color')
                .setDescription('Embed farge (hex kode, f.eks. #ff0000)')
                .setRequired(false)
        )
        .addStringOption(option =>
            option.setName('image')
                .setDescription('Bilde URL')
                .setRequired(false)
        )
        .addStringOption(option =>
            option.setName('thumbnail')
                .setDescription('Thumbnail URL')
                .setRequired(false)
        )
        .addBooleanOption(option =>
            option.setName('timestamp')
                .setDescription('Vis timestamp?')
                .setRequired(false)
        ),
    
    async execute(interaction) {
        if (!permissions.hasStaffRole(interaction.member, 'HELPER')) {
            return interaction.reply({
                content: '❌ Du trenger helper tillatelser for å bruke denne kommandoen.',
                ephemeral: true
            });
        }
        
        const title = interaction.options.getString('title');
        const description = interaction.options.getString('description') || '';
        const colorHex = interaction.options.getString('color') || '#3498db';
        const image = interaction.options.getString('image');
        const thumbnail = interaction.options.getString('thumbnail');
        const showTimestamp = interaction.options.getBoolean('timestamp') || false;
        
        // Parse color
        let color = 0x3498db;
        try {
            color = parseInt(colorHex.replace('#', ''), 16);
        } catch (error) {
            color = 0x3498db; // Default blue
        }
        
        const embed = new EmbedBuilder()
            .setColor(color)
            .setTitle(title)
            .setDescription(description)
            .setFooter({
                text: `Laget av ${interaction.user.tag}`,
                iconURL: interaction.user.displayAvatarURL()
            });
        
        if (image) {
            try {
                embed.setImage(image);
            } catch (error) {
                // Invalid image URL - ignore
            }
        }
        
        if (thumbnail) {
            try {
                embed.setThumbnail(thumbnail);
            } catch (error) {
                // Invalid thumbnail URL - ignore  
            }
        }
        
        if (showTimestamp) {
            embed.setTimestamp();
        }
        
        // Buttons for additional options
        const row = new ActionRowBuilder()
            .addComponents(
                new ButtonBuilder()
                    .setCustomId('embed_add_field')
                    .setLabel('Legg til felt')
                    .setStyle(ButtonStyle.Primary)
                    .setEmoji('📝'),
                new ButtonBuilder()
                    .setCustomId('embed_send')
                    .setLabel('Send embed')
                    .setStyle(ButtonStyle.Success)
                    .setEmoji('✅'),
                new ButtonBuilder()
                    .setCustomId('embed_cancel')
                    .setLabel('Avbryt')
                    .setStyle(ButtonStyle.Danger)
                    .setEmoji('❌')
            );
        
        await interaction.reply({
            content: '📋 **Embed Preview:**',
            embeds: [embed],
            components: [row],
            ephemeral: true
        });
        
        // Store embed data for buttons (you'd want to use a better storage method in production)
        if (!interaction.client.embedBuilder) {
            interaction.client.embedBuilder = new Map();
        }
        
        interaction.client.embedBuilder.set(interaction.user.id, {
            embed: embed.toJSON(),
            originalInteraction: interaction
        });
    },
};

5.2: Embed button interactions

Lag events/interaction/buttonInteraction.js:

javascript
const { EmbedBuilder, ModalBuilder, TextInputBuilder, ActionRowBuilder, TextInputStyle } = require('discord.js');

module.exports = {
    name: 'interactionCreate',
    async execute(interaction) {
        if (!interaction.isButton()) return;
        
        const { customId, user } = interaction;
        
        if (customId.startsWith('embed_')) {
            const embedData = interaction.client.embedBuilder?.get(user.id);
            if (!embedData) {
                return interaction.reply({
                    content: '❌ Embed data ikke funnet. Prøv å bruke `/embed` kommandoen igjen.',
                    ephemeral: true
                });
            }
            
            if (customId === 'embed_add_field') {
                // Show modal for adding field
                const modal = new ModalBuilder()
                    .setCustomId('embed_field_modal')
                    .setTitle('Legg til felt i embed');
                
                const nameInput = new TextInputBuilder()
                    .setCustomId('field_name')
                    .setLabel('Felt navn')
                    .setStyle(TextInputStyle.Short)
                    .setMaxLength(256)
                    .setRequired(true);
                
                const valueInput = new TextInputBuilder()
                    .setCustomId('field_value')
                    .setLabel('Felt verdi')
                    .setStyle(TextInputStyle.Paragraph)
                    .setMaxLength(1024)
                    .setRequired(true);
                
                const inlineInput = new TextInputBuilder()
                    .setCustomId('field_inline')
                    .setLabel('Inline? (true/false)')
                    .setStyle(TextInputStyle.Short)
                    .setValue('false')
                    .setRequired(false);
                
                modal.addComponents(
                    new ActionRowBuilder().addComponents(nameInput),
                    new ActionRowBuilder().addComponents(valueInput),
                    new ActionRowBuilder().addComponents(inlineInput)
                );
                
                await interaction.showModal(modal);
                
            } else if (customId === 'embed_send') {
                // Send the embed to the channel
                const embed = new EmbedBuilder(embedData.embed);
                
                await interaction.channel.send({ embeds: [embed] });
                await interaction.reply({
                    content: '✅ Embed sendt!',
                    ephemeral: true
                });
                
                // Clean up
                interaction.client.embedBuilder.delete(user.id);
                
            } else if (customId === 'embed_cancel') {
                // Cancel embed creation
                await interaction.reply({
                    content: '❌ Embed opprettelse avbrutt.',
                    ephemeral: true
                });
                
                // Clean up
                interaction.client.embedBuilder.delete(user.id);
            }
        }
    }
};

✅ Testing Avanserte Features

6.1: Registrer nye commands

bash
node deploy-commands.js

6.2: Start boten og test

bash
node index.js

6.3: Test alle nye features

XP System:

  • Send meldinger og se forbedret XP system
  • Join voice kanaler og få voice XP

Economy System:

  • /balance - Sjekk din økonomi
  • /daily - Få daglige coins
  • Test streak system over flere dager

Reaction Roles:

  • /reactionrole add - Sett opp reaction role
  • React på meldinger og få roller automatisk

Custom Embeds:

  • /embed - Lag fancy embeds med buttons og interaktivitet

📊 Oppsummering

Avanserte features vi har lagt til:

  • 💎 Forbedret XP system - Multipliers, belønninger, voice tracking
  • 💰 Economy system - Coins, daily rewards, streak system
  • 🎭 Reaction roles - Automatisk rolle tildeling
  • 🎨 Custom embed builder - Lag profesjonelle meldinger
  • 🗣️ Voice activity - XP for tid i voice kanaler
  • 🏆 Level rewards - Automatiske belønninger ved level ups

Database utvidelser:

  • xp_rewards - Level belønninger
  • user_economy - Virtual økonomi
  • voice_sessions - Voice aktivitet tracking
  • reaction_roles - Reaction rolle system

🚀 Neste Steg

I del 9 skal vi fokusere på Testing og Debugging! Vi lærer:

  • Comprehensive testing av alle bot features
  • Error handling og logging improvements
  • Performance optimization
  • User testing og feedback implementation
  • Bug fixing og stability improvements

Klar for å gjøre boten bulletproof?