Skip to content

Commit

Permalink
Merge pull request #44 from iluxonchik/feature/govbot-0.0.14
Browse files Browse the repository at this point in the history
Feature/govbot 0.0.14
  • Loading branch information
iluxonchik authored Sep 11, 2024
2 parents 3a0cdb9 + 831fde1 commit f7938f0
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 6 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mina-govbot",
"version": "0.0.13",
"version": "0.0.14",
"description": "Discord bot for collective decision making for Mina Protocol",
"main": "index.js",
"directories": {
Expand Down
18 changes: 13 additions & 5 deletions src/channels/admin/actions/CountVotesAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,19 @@ export class CountVotesAction extends Action {
const progressEmbed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle('Counting Votes')
.setDescription('Please wait while our quantum computers count all possible vote outcomes...');
.setDescription('Please wait while we count the votes and gather reasoning...');

await interaction.interaction.editReply({ embeds: [progressEmbed], components: [] });

let updateCounter = 0;
const updateInterval = setInterval(async () => {
updateCounter++;
progressEmbed.setDescription(
`Please wait while our quantum computers count all possible vote outcomes...\nTime elapsed: ${updateCounter * 5} seconds`,
);
progressEmbed.setDescription(`Please wait while we count the votes and gather reasoning...\nTime elapsed: ${updateCounter * 5} seconds`);
await interaction.interaction.editReply({ embeds: [progressEmbed] });
}, 5000);

try {
const voteResults = await VoteCountingLogic.countVotes(parseInt(fundingRoundId), phase, interaction);
const voteResults = await VoteCountingLogic.countVotesWithReasoning(parseInt(fundingRoundId), phase, interaction);

clearInterval(updateInterval);

Expand All @@ -116,6 +114,16 @@ export class CountVotesAction extends Action {
});

await interaction.interaction.editReply({ embeds: [resultEmbed], components: [] });

// Send vote reasoning as follow-up messages with embeds
const reasoningEmbeds = VoteCountingLogic.formatVoteReasoningMessage(voteResults);
if (reasoningEmbeds.length > 0) {
for (const embed of reasoningEmbeds) {
await interaction.interaction.followUp({ embeds: [embed], ephemeral: true });
}
} else {
await interaction.interaction.followUp({ content: 'No vote reasoning available for any projects.', ephemeral: true });
}
} catch (error) {
clearInterval(updateInterval);
if (error instanceof EndUserError) {
Expand Down
212 changes: 212 additions & 0 deletions src/logic/VoteCountingLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CommitteeDeliberationVoteChoice } from '../types';
import { TrackedInteraction } from '../core/BaseClasses';
import logger from '../logging';
import { Op } from 'sequelize';
import { EmbedBuilder } from 'discord.js';

interface VoteResult {
projectId: number;
Expand All @@ -17,6 +18,19 @@ interface VoteResult {
approvedModifiedVoters?: string[];
}

interface VoteResultWithReasoning extends VoteResult {
deliberationVotes: {
voterUsername: string;
vote: CommitteeDeliberationVoteChoice;
reason: string | null;
}[];
considerationVotes: {
voterUsername: string;
isPass: boolean;
reason: string | null;
}[];
}

export class VoteCountingLogic {
public static async countVotes(fundingRoundId: number, phase: string, trackedInteraction: TrackedInteraction): Promise<VoteResult[]> {
const fundingRound = await FundingRound.findByPk(fundingRoundId, { include: [Proposal] });
Expand Down Expand Up @@ -175,4 +189,202 @@ export class VoteCountingLogic {
}),
);
}

public static async countVotesWithReasoning(
fundingRoundId: number,
phase: string,
trackedInteraction: TrackedInteraction,
): Promise<VoteResultWithReasoning[]> {
const fundingRound = await FundingRound.findByPk(fundingRoundId, { include: [Proposal] });
if (!fundingRound) {
throw new EndUserError('Funding round not found');
}

const { FundingRoundLogic } = await import('../channels/admin/screens/FundingRoundLogic');
const proposals = await FundingRoundLogic.getProposalsForFundingRound(fundingRoundId);
let voteResults: VoteResultWithReasoning[] = [];

switch (phase) {
case 'consideration':
voteResults = await this.countConsiderationVotesWithReasoning(proposals, trackedInteraction);
break;
case 'deliberation':
voteResults = await this.countDeliberationVotesWithReasoning(proposals, trackedInteraction);
break;
case 'voting':
throw new EndUserError('Voting phase vote counting is not yet implemented');
default:
throw new EndUserError(`Invalid phase selected: ${phase}`);
}

return voteResults.sort((a, b) => {
if (a.yesVotes !== b.yesVotes) {
return b.yesVotes - a.yesVotes;
}
if (a.noVotes !== b.noVotes) {
return b.noVotes - a.noVotes;
}
return (b.approvedModifiedVotes || 0) - (a.approvedModifiedVotes || 0);
});
}

private static async countConsiderationVotesWithReasoning(
proposals: Proposal[],
trackedInteraction: TrackedInteraction,
): Promise<VoteResultWithReasoning[]> {
return Promise.all(
proposals.map(async (proposal) => {
const allVotes = await SMEConsiderationVoteLog.findAll({
where: { proposalId: proposal.id },
order: [['createdAt', 'DESC']],
});

const latestVotes = new Map<string, boolean>();
const yesVoters: string[] = [];
const noVoters: string[] = [];
const considerationVotes: VoteResultWithReasoning['considerationVotes'] = [];

for (const vote of allVotes) {
if (!latestVotes.has(vote.duid)) {
latestVotes.set(vote.duid, vote.isPass);
const voterUsername = await this.getUsername(trackedInteraction, vote.duid);
considerationVotes.push({
voterUsername,
isPass: vote.isPass,
reason: vote.reason,
});
if (vote.isPass) {
yesVoters.push(vote.duid);
} else {
noVoters.push(vote.duid);
}
}
}

const yesVotes = yesVoters.length;
const noVotes = noVoters.length;

const yesVoterUsernames = await this.getVoterUsernames(trackedInteraction, yesVoters);
const noVoterUsernames = await this.getVoterUsernames(trackedInteraction, noVoters);

const proposerUsername = await this.getUsername(trackedInteraction, proposal.proposerDuid);

return {
projectId: proposal.id,
projectName: proposal.name,
proposerUsername,
yesVotes,
noVotes,
yesVoters: yesVoterUsernames,
noVoters: noVoterUsernames,
deliberationVotes: [],
considerationVotes,
};
}),
);
}

private static async countDeliberationVotesWithReasoning(
proposals: Proposal[],
trackedInteraction: TrackedInteraction,
): Promise<VoteResultWithReasoning[]> {
return Promise.all(
proposals.map(async (proposal) => {
const allVotes = await CommitteeDeliberationVoteLog.findAll({
where: { proposalId: proposal.id },
order: [['createdAt', 'DESC']],
});

const latestVotes = new Map<string, CommitteeDeliberationVoteChoice>();
const yesVoters: string[] = [];
const noVoters: string[] = [];
const approvedModifiedVoters: string[] = [];
const deliberationVotes: VoteResultWithReasoning['deliberationVotes'] = [];

for (const vote of allVotes) {
if (!latestVotes.has(vote.duid)) {
latestVotes.set(vote.duid, vote.vote);
const voterUsername = await this.getUsername(trackedInteraction, vote.duid);
deliberationVotes.push({
voterUsername,
vote: vote.vote,
reason: vote.reason,
});
switch (vote.vote) {
case CommitteeDeliberationVoteChoice.APPROVED:
yesVoters.push(vote.duid);
break;
case CommitteeDeliberationVoteChoice.REJECTED:
noVoters.push(vote.duid);
break;
case CommitteeDeliberationVoteChoice.APPROVED_MODIFIED:
approvedModifiedVoters.push(vote.duid);
break;
}
}
}

const yesVotes = yesVoters.length;
const noVotes = noVoters.length;
const approvedModifiedVotes = approvedModifiedVoters.length;

const yesVoterUsernames = await this.getVoterUsernames(trackedInteraction, yesVoters);
const noVoterUsernames = await this.getVoterUsernames(trackedInteraction, noVoters);
const approvedModifiedVoterUsernames = await this.getVoterUsernames(trackedInteraction, approvedModifiedVoters);

const proposerUsername = await this.getUsername(trackedInteraction, proposal.proposerDuid);

return {
projectId: proposal.id,
projectName: proposal.name,
proposerUsername,
yesVotes,
noVotes,
approvedModifiedVotes,
yesVoters: yesVoterUsernames,
noVoters: noVoterUsernames,
approvedModifiedVoters: approvedModifiedVoterUsernames,
deliberationVotes,
considerationVotes: [],
};
}),
);
}

public static formatVoteReasoningMessage(voteResults: VoteResultWithReasoning[]): EmbedBuilder[] {
const embeds: EmbedBuilder[] = [];

for (const result of voteResults) {
if (result.deliberationVotes.length === 0 && result.considerationVotes.length === 0) {
continue; // Skip projects with no votes
}

const embed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle(`Vote Reasoning - ${result.projectName} (ID: ${result.projectId})`)
.setDescription(`Proposer: ${result.proposerUsername}`);

if (result.deliberationVotes.length > 0) {
let deliberationField = '';
for (const vote of result.deliberationVotes) {
deliberationField += `**${vote.voterUsername}**: ${vote.vote}\n`;
deliberationField += `Reasoning: ${vote.reason || 'No reason provided'}\n\n`;
}
embed.addFields({ name: 'Deliberation Phase Votes', value: deliberationField.trim() });
}

if (result.considerationVotes.length > 0) {
let considerationField = '';
for (const vote of result.considerationVotes) {
considerationField += `**${vote.voterUsername}**: ${vote.isPass ? 'Yes' : 'No'}\n`;
considerationField += `Reasoning: ${vote.reason || 'No reason provided'}\n\n`;
}
embed.addFields({ name: 'Consideration Phase Votes', value: considerationField.trim() });
}

embeds.push(embed);
}

return embeds;
}
}

0 comments on commit f7938f0

Please sign in to comment.