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