All files / src/processors ActionExecutor.js

100% Statements 42/42
100% Branches 24/24
100% Functions 6/6
100% Lines 41/41

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127                                    106x                             4x       4x 4x   4x 4x 2x 2x 1x   1x     1x             7x   7x 1x 1x         6x 5x 4x 1x 1x         4x 3x 3x   1x         6x   6x 1x 1x       5x 4x 4x 1x 1x       4x 3x 3x   1x                 17x 4x       16x                 9x
/**
 * ActionExecutor - Safe execution of Discord role-based actions
 * 
 * Handles the secure execution of role assignments and removals triggered by reactions.
 * Provides validation, parsing, and safe execution of configured actions.
 * 
 * Security design rationale:
 * - Action string parsing prevents code injection while allowing flexibility
 * - User eligibility validation ensures only authorized members get roles
 * - Role validation prevents assignment of non-existent or restricted roles
 * - Comprehensive error handling prevents partial state or security bypasses
 * 
 * Supported action formats:
 * - AddRole(user_id,'role_name') - Assigns a role to the reacting user
 * - RemoveRole(user_id,'role_name') - Removes a role from the reacting user
 */
class ActionExecutor {
    constructor(bot) {
        this.bot = bot;
    }
 
    /**
     * Execute a configured action safely with validation
     * 
     * Parses action strings and delegates to appropriate handlers while ensuring
     * security and validity of all operations.
     * 
     * @param {string} action - Action string to parse and execute
     * @param {GuildMember} member - Discord member to apply action to
     * @param {Guild} guild - Discord guild context
     * @returns {boolean} - True if action succeeded, false otherwise
     */
    async executeAction(action, member, guild) {
        console.log(`Executing action: ${action} for user: ${member.user.tag}`);
 
        // Parse action string using regex to extract role names
        // This allows flexible configuration while maintaining security
        const addRoleMatch = action.match(/AddRole\(user_id,'(.+?)'\)/);
        const removeRoleMatch = action.match(/RemoveRole\(user_id,'(.+?)'\)/);
 
        try {
            if (addRoleMatch) {
                await this.executeAddRole(addRoleMatch[1], member, guild);
            } else if (removeRoleMatch) {
                await this.executeRemoveRole(removeRoleMatch[1], member, guild);
            } else {
                console.log(`Unknown action: ${action}`);
            }
        } catch (error) {
            console.error('Error executing action:', error);
        }
    }
 
    // Add a role to a user with eligibility validation
    // Prevents unauthorized role assignments and duplicate role additions
    async executeAddRole(roleName, member, guild) {
        const role = this.findRole(roleName, guild);
        
        if (!role) {
            console.log(`Role not found: ${roleName}`);
            return;
        }
 
        // Validate user eligibility for non-member roles
        // Member role can be assigned to anyone, but other roles require validation
        if (roleName !== 'member') {
            const validation = this.bot.getUserValidator().canAct(member, this.bot.getMemberRoleId());
            if (!validation.canAct) {
                console.log(`User ${member.user.tag} cannot act: ${validation.reason}`);
                return;
            }
        }
 
        // Check if user already has the role to avoid unnecessary Discord API calls
        if (!member.roles.cache.has(role.id)) {
            await member.roles.add(role);
            console.log(`✅ Added role ${role.name} to ${member.user.tag}`);
        } else {
            console.log(`User ${member.user.tag} already has role ${role.name}`);
        }
    }
 
    async executeRemoveRole(roleName, member, guild) {
        const role = this.findRole(roleName, guild);
        
        if (!role) {
            console.log(`Role not found: ${roleName}`);
            return;
        }
 
        // For non-member role actions, check if user can act
        if (roleName !== 'member') {
            const validation = this.bot.getUserValidator().canAct(member, this.bot.getMemberRoleId());
            if (!validation.canAct) {
                console.log(`User ${member.user.tag} cannot act: ${validation.reason}`);
                return;
            }
        }
 
        if (member.roles.cache.has(role.id)) {
            await member.roles.remove(role);
            console.log(`❌ Removed role ${role.name} from ${member.user.tag}`);
        } else {
            console.log(`User ${member.user.tag} doesn't have role ${role.name}`);
        }
    }
 
    // Find Discord role by name with special handling for configured roles
    // Prioritizes role IDs from configuration over name-based lookup for reliability
    findRole(roleName, guild) {
        // Use configured role ID for special roles to avoid name conflicts
        // Role names can change, but IDs remain constant
        if (roleName === 'member' && this.bot.getMemberRoleId()) {
            return guild.roles.cache.get(this.bot.getMemberRoleId());
        }
        
        // Fall back to name-based search for custom roles
        return guild.roles.cache.find(r => r.name === roleName);
    }
 
    // Future: Add more action types here
    // executeTimeout(duration, member, guild) { ... }
    // executeKick(member, guild) { ... }
    // executeCustomAction(actionData, member, guild) { ... }
}
 
module.exports = ActionExecutor;