All files / src/processors ProposalParser.js

100% Statements 24/24
93.33% Branches 14/15
100% Functions 5/5
100% Lines 24/24

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                                        24x                             13x 18x     10x 10x   7x 7x 7x       6x 6x       3x 3x 3x 3x           5x 5x 5x 5x       5x                                       5x 5x 4x       1x       11x
/**
 * ProposalParser - Proposal format validation and message generation
 * 
 * Handles the parsing and validation of proposal messages to ensure they follow
 * required formats for proper democratic processing.
 * 
 * Format enforcement rationale:
 * - Structured formats ensure proposals are clear and actionable
 * - Type-specific validation prevents proposals in wrong channels
 * - Consistent formatting enables automated processing and reduces ambiguity
 * - Withdrawal detection allows for removal of previously passed proposals
 * 
 * Supported formats:
 * - **Policy**: content (for policy proposals)
 * - **Moderator**: content (for staff changes)
 * - **Governance**: content (for system changes)
 * - **Withdraw**: reference (for proposal removals)
 */
class ProposalParser {
    constructor(proposalConfig) {
        this.proposalConfig = proposalConfig;
    }
 
    /**
     * Determine proposal type from channel context and content format
     * 
     * Validates that proposals are submitted in the correct channel with proper formatting.
     * Different proposal types have different channels and format requirements.
     * 
     * @param {string} channelId - Discord channel where proposal was submitted
     * @param {string} content - Raw proposal content to validate
     * @returns {Object|null} - {type, config, isWithdrawal} or null if invalid
     */
    getProposalType(channelId, content) {
        // Match channel to proposal type configuration
        for (const [type, config] of Object.entries(this.proposalConfig)) {
            if (config.debateChannelId === channelId) {
                // Validate that content follows required format for this proposal type
                // Format: **ProposalType**: content or **Withdraw**: content
                const formatRegex = new RegExp(`^\\*\\*(?:${config.formats.join('|')}|Withdraw)\\*\\*:`, 'i');
                if (formatRegex.test(content.trim())) {
                    // Detect withdrawal proposals which follow special processing
                    const isWithdrawal = /^\*\*Withdraw\*\*:/i.test(content.trim());
                    console.log(`Matched proposal type: ${type}${isWithdrawal ? ' (withdrawal)' : ''}`);
                    return { type, config, isWithdrawal };
                }
            }
        }
        console.log(`No matching proposal type found for channel ${channelId}`);
        return null;
    }
 
    isValidProposalFormat(channelId, content) {
        const proposalMatch = this.getProposalType(channelId, content);
        const isValid = proposalMatch !== null;
        console.log(`Checking proposal format for: "${content.substring(0, 50)}..." in channel ${channelId} - Valid: ${isValid}`);
        return isValid;
    }
 
    // Generate formatted vote message for proposals that advance to voting
    // Creates clear, standardized vote messages with instructions and deadlines
    createVoteMessage(originalMessage, proposalType, config, isWithdrawal = false) {
        const author = originalMessage.author;
        const proposalContent = originalMessage.content;
        const endTime = Date.now() + config.voteDuration;
        const withdrawalText = isWithdrawal ? 'WITHDRAWAL ' : '';
        
        // Create comprehensive vote message with all necessary information
        // Includes original proposal, voting instructions, and deadline
        return `šŸ—³ļø **${proposalType.toUpperCase()} ${withdrawalText}VOTING PHASE**
 
**Proposed by:** ${author.tag}
**Type:** ${proposalType}${isWithdrawal ? ' (withdrawal)' : ''}
**Original Proposal:**
${proposalContent}
 
**Instructions:**
${isWithdrawal ? 
    'āœ… React with āœ… to SUPPORT withdrawing this resolution\nāŒ React with āŒ to OPPOSE withdrawal (keep the resolution)' :
    'āœ… React with āœ… to SUPPORT this proposal\nāŒ React with āŒ to OPPOSE this proposal'
}
 
**Voting ends:** <t:${Math.floor(endTime / 1000)}:F>
 
React below to cast your vote!`;
    }
 
    extractOriginalResolution(resolutionContent) {
        // Extract the original proposal text from a resolution message
        const resolutionMatch = resolutionContent.match(/\*\*Resolution:\*\*\s*(.+?)(?:\n\*|$)/s);
        if (resolutionMatch) {
            return resolutionMatch[1].trim();
        }
        
        // Fallback: return the whole resolution content
        return resolutionContent;
    }
}
 
module.exports = ProposalParser;