Skip to content

Commit

Permalink
Merge pull request #43 from iluxonchik/feature/govbot-0.0.13
Browse files Browse the repository at this point in the history
govbot 0.0.13 - Show Proposer Usernames, Correct Vote Counting
  • Loading branch information
iluxonchik authored Sep 6, 2024
2 parents c15c9b6 + 847c149 commit 3a0cdb9
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 36 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.12",
"version": "0.0.13",
"description": "Discord bot for collective decision making for Mina Protocol",
"main": "index.js",
"directories": {
Expand Down
65 changes: 50 additions & 15 deletions src/channels/admin/actions/CountVotesAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { VoteCountingLogic } from '../../../logic/VoteCountingLogic';
import { EndUserError } from '../../../Errors';
import { AnyModalMessageComponent } from '../../../types/common';
import { DiscordStatus } from '../../DiscordStatus';
import { Client } from 'discord.js';

export class CountVotesAction extends Action {
public allSubActions(): Action[] {
Expand Down Expand Up @@ -66,28 +67,62 @@ export class CountVotesAction extends Action {
}

private async handleCountVotes(interaction: TrackedInteraction): Promise<void> {
await interaction.interaction.deferReply();

const fundingRoundId = ArgumentOracle.getNamedArgument(interaction, ArgumentOracle.COMMON_ARGS.FUNDING_ROUND_ID);
const phase = ArgumentOracle.getNamedArgument(interaction, ArgumentOracle.COMMON_ARGS.PHASE, 0);

const voteResults = await VoteCountingLogic.countVotes(parseInt(fundingRoundId), phase);

const embed = new EmbedBuilder()
const progressEmbed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle(`Vote Count Results - ${phase.charAt(0).toUpperCase() + phase.slice(1)} Phase`)
.setDescription('Here are the vote counts for each project:');
.setTitle('Counting Votes')
.setDescription('Please wait while our quantum computers count all possible vote outcomes...');

voteResults.forEach((result, index) => {
let voteInfo = `Yes Votes: ${result.yesVotes}\nNo Votes: ${result.noVotes}`;
if (phase === 'deliberation' && result.approvedModifiedVotes !== undefined) {
voteInfo += `\nApproved Modified Votes: ${result.approvedModifiedVotes}`;
}
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`,
);
await interaction.interaction.editReply({ embeds: [progressEmbed] });
}, 5000);

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

embed.addFields({
name: `${index + 1}. ${result.projectName} (ID: ${result.projectId})`,
value: `Proposer: ${result.proposerDuid}\n${voteInfo}`,
clearInterval(updateInterval);

const resultEmbed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle(`Vote Count Results - ${phase.charAt(0).toUpperCase() + phase.slice(1)} Phase`)
.setDescription('Here are the vote counts for each project:');

voteResults.forEach((result, index) => {
let voteInfo = `Yes Votes: ${result.yesVotes}\nNo Votes: ${result.noVotes}`;
if (phase === 'deliberation' && result.approvedModifiedVotes !== undefined) {
voteInfo += `\nApproved Modified Votes: ${result.approvedModifiedVotes}`;
}

let voterInfo = `Yes Voters: ${result.yesVoters.join(', ')}\nNo Voters: ${result.noVoters.join(', ')}`;
if (phase === 'deliberation' && result.approvedModifiedVoters) {
voterInfo += `\nApproved Modified Voters: ${result.approvedModifiedVoters.join(', ')}`;
}

resultEmbed.addFields({
name: `${index + 1}. ${result.projectName} (ID: ${result.projectId})`,
value: `Proposer: ${result.proposerUsername}\n${voteInfo}\n\n${voterInfo}`,
});
});
});

await interaction.update({ embeds: [embed], components: [] });
await interaction.interaction.editReply({ embeds: [resultEmbed], components: [] });
} catch (error) {
clearInterval(updateInterval);
if (error instanceof EndUserError) {
throw error;
} else {
throw new EndUserError('An unexpected error occurred while counting votes.');
}
}
}
}
123 changes: 103 additions & 20 deletions src/logic/VoteCountingLogic.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { FundingRound, Proposal, SMEConsiderationVoteLog, CommitteeDeliberationVoteLog } from '../models';
import { EndUserError } from '../Errors';
import { CommitteeDeliberationVoteChoice } from '../types';
import { TrackedInteraction } from '../core/BaseClasses';
import logger from '../logging';
import { Op } from 'sequelize';

interface VoteResult {
projectId: number;
projectName: string;
proposerDuid: string;
proposerUsername: string;
yesVotes: number;
noVotes: number;
approvedModifiedVotes?: number; // Only for deliberation phase
approvedModifiedVotes?: number;
yesVoters: string[];
noVoters: string[];
approvedModifiedVoters?: string[];
}

export class VoteCountingLogic {
public static async countVotes(fundingRoundId: number, phase: string): Promise<VoteResult[]> {
public static async countVotes(fundingRoundId: number, phase: string, trackedInteraction: TrackedInteraction): Promise<VoteResult[]> {
const fundingRound = await FundingRound.findByPk(fundingRoundId, { include: [Proposal] });
if (!fundingRound) {
throw new EndUserError('Funding round not found');
Expand All @@ -24,10 +30,10 @@ export class VoteCountingLogic {

switch (phase) {
case 'consideration':
voteResults = await this.countConsiderationVotes(proposals);
voteResults = await this.countConsiderationVotes(proposals, trackedInteraction);
break;
case 'deliberation':
voteResults = await this.countDeliberationVotes(proposals);
voteResults = await this.countDeliberationVotes(proposals, trackedInteraction);
break;
case 'voting':
throw new EndUserError('Voting phase vote counting is not yet implemented');
Expand All @@ -47,47 +53,124 @@ export class VoteCountingLogic {
});
}

private static async countConsiderationVotes(proposals: Proposal[]): Promise<VoteResult[]> {
private static async getUsername(trackedInteraction: TrackedInteraction, duid: string): Promise<string> {
try {
const user = await trackedInteraction.interaction.client.users.fetch(duid);
return user.username;
} catch (error) {
logger.error(`Error fetching user ${duid}:`, error);
return duid; // Fallback to using DUID if username can't be fetched
}
}

private static async getVoterUsernames(trackedInteraction: TrackedInteraction, duids: string[]): Promise<string[]> {
const usernames: string[] = [];
for (const duid of duids) {
try {
const username: string = await this.getUsername(trackedInteraction, duid);
usernames.push(username);
} catch (error) {
logger.error(`Error fetching user ${duid}:`, error);
usernames.push(duid); // Fallback to using DUID if username can't be fetched
}
}
return usernames;
}

private static async countConsiderationVotes(proposals: Proposal[], trackedInteraction: TrackedInteraction): Promise<VoteResult[]> {
return Promise.all(
proposals.map(async (proposal) => {
const yesVotes = await SMEConsiderationVoteLog.count({
where: { proposalId: proposal.id, isPass: true },
const allVotes = await SMEConsiderationVoteLog.findAll({
where: { proposalId: proposal.id },
order: [['createdAt', 'DESC']],
});
const noVotes = await SMEConsiderationVoteLog.count({
where: { proposalId: proposal.id, isPass: false },

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

allVotes.forEach((vote) => {
if (!latestVotes.has(vote.duid)) {
latestVotes.set(vote.duid, vote.isPass);
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,
proposerDuid: proposal.proposerDuid,
proposerUsername,
yesVotes,
noVotes,
yesVoters: yesVoterUsernames,
noVoters: noVoterUsernames,
};
}),
);
}

private static async countDeliberationVotes(proposals: Proposal[]): Promise<VoteResult[]> {
private static async countDeliberationVotes(proposals: Proposal[], trackedInteraction: TrackedInteraction): Promise<VoteResult[]> {
return Promise.all(
proposals.map(async (proposal) => {
const yesVotes = await CommitteeDeliberationVoteLog.count({
where: { proposalId: proposal.id, vote: CommitteeDeliberationVoteChoice.APPROVED },
const allVotes = await CommitteeDeliberationVoteLog.findAll({
where: { proposalId: proposal.id },
order: [['createdAt', 'DESC']],
});
const noVotes = await CommitteeDeliberationVoteLog.count({
where: { proposalId: proposal.id, vote: CommitteeDeliberationVoteChoice.REJECTED },
});
const approvedModifiedVotes = await CommitteeDeliberationVoteLog.count({
where: { proposalId: proposal.id, vote: CommitteeDeliberationVoteChoice.APPROVED_MODIFIED },

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

allVotes.forEach((vote) => {
if (!latestVotes.has(vote.duid)) {
latestVotes.set(vote.duid, vote.vote);
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,
proposerDuid: proposal.proposerDuid,
proposerUsername,
yesVotes,
noVotes,
approvedModifiedVotes,
yesVoters: yesVoterUsernames,
noVoters: noVoterUsernames,
approvedModifiedVoters: approvedModifiedVoterUsernames,
};
}),
);
Expand Down

0 comments on commit 3a0cdb9

Please sign in to comment.