Skip to content

Commit

Permalink
Merge pull request #37 from iluxonchik/feature/govbot-0.0.9
Browse files Browse the repository at this point in the history
Feature/govbot 0.0.9
  • Loading branch information
iluxonchik authored Aug 26, 2024
2 parents e2b05fa + 64e9d07 commit e0ec505
Show file tree
Hide file tree
Showing 30 changed files with 2,426 additions and 2,109 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.8",
"version": "0.0.9",
"description": "Discord bot for collective decision making for Mina Protocol",
"main": "index.js",
"directories": {
Expand Down
21 changes: 17 additions & 4 deletions src/CustomIDOracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class CustomIDOracle {
const customId = parts.join(this.SEPARATOR);

if (customId.length > this.MAX_LENGTH) {
throw new EndUserError(`CustomId length of ${customId.length} exceeds the maximum allowed value of ${this.MAX_LENGTH} characters.`);
throw new EndUserError(`CustomId length of ${customId.length} exceeds the maximum allowed value of ${this.MAX_LENGTH} characters: ${customId}`);
}

return customId;
Expand All @@ -61,7 +61,7 @@ export class CustomIDOracle {
const customId = parts.join(this.SEPARATOR);

if (customId.length > this.MAX_LENGTH) {
throw new EndUserError(`CustomId length of ${customId.length} exceeds the maximum allowed value of ${this.MAX_LENGTH} characters.`);
throw new EndUserError(`CustomId length of ${customId.length} exceeds the maximum allowed value of ${this.MAX_LENGTH} characters: ${customId}`);
}

return customId;
Expand All @@ -82,6 +82,19 @@ export class CustomIDOracle {
return outputCustomId;
}

static addArgumentsToActionCustomDashboardId(dashboardId: string, action: Action, operation?: string, ...args: string[]): string {
if (args.length % 2 !== 0) {
throw new EndUserError('Arguments must be key-value pairs');
}
const customId = this.customIdFromRawParts(dashboardId, action.screen.ID, action.ID, operation, ...args);

if (customId.length > this.MAX_LENGTH) {
throw new EndUserError(`Custom ID exceeds maximum length of ${this.MAX_LENGTH} characters by ${customId.length - this.MAX_LENGTH} characters`);
}

return customId;
}

static getNamedArgument(customId: string, argName: string): string | undefined {
const args = this.getArguments(customId);
for (let i = 0; i < args.length; i += 2) {
Expand Down Expand Up @@ -142,8 +155,8 @@ export class CustomIDOracle {
export class ArgumentOracle {

static COMMON_ARGS = {
FUNDING_ROUND_ID: 'fundingRoundId',
PHASE: 'phase',
FUNDING_ROUND_ID: 'frId',
PHASE: 'ph',
}

static isArgumentEquals(intreaction: TrackedInteraction, argName: string, value: string): boolean {
Expand Down
4 changes: 4 additions & 0 deletions src/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ export class NotFoundEndUserError extends EndUserError {


export class EndUserInfo extends GovBotError {
}

export class NotFoundEndUserInfo extends EndUserInfo {

}
15 changes: 4 additions & 11 deletions src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,6 @@ client.once('ready', async () => {
logger.error('Propose channel not found');
}

// Render initial screen in #vote channel
const voteChannel = guild.channels.cache.find(channel => channel.name === 'vote') as TextChannel | undefined;
if (voteChannel) {
await voteDashboard.homeScreen.renderToTextChannel(voteChannel);
} else {
logger.error('Vote channel not found');
}

// Render initial screen in #deliberate channel
const deliberateChannel = guild.channels.cache.find(channel => channel.name === 'deliberate') as TextChannel | undefined;
Expand All @@ -124,7 +117,7 @@ client.once('ready', async () => {
});

client.on('interactionCreate', async (interaction: Interaction<CacheType>) => {
logger.info("Start handling interaction");
logger.info(`Start handling interaction: ${interaction.isMessageComponent() ? interaction.customId : 'N/A'}`);
try {

if (!interaction.isButton() && !interaction.isStringSelectMenu() && !interaction.isModalSubmit() && !interaction.isMessageComponent()) {
Expand All @@ -140,8 +133,8 @@ client.on('interactionCreate', async (interaction: Interaction<CacheType>) => {

try {
logger.error(error);
const trackedInteratction = new TrackedInteraction(interaction as AnyInteraction);
await DiscordStatus.handleException(trackedInteratction, error);
const trackedInteraction = new TrackedInteraction(interaction as AnyInteraction);
await DiscordStatus.handleException(trackedInteraction, error);

} catch (error) {
logger.error(`Unrecoverable error: ${error}`);
Expand All @@ -152,4 +145,4 @@ client.on('interactionCreate', async (interaction: Interaction<CacheType>) => {

});

client.login(process.env.DISCORD_TOKEN);
client.login(process.env.DISCORD_TOKEN);
3 changes: 1 addition & 2 deletions src/channels/DiscordStatus.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Message } from "discord.js";
import { TrackedInteraction } from "../core/BaseClasses";
import { EndUserError, EndUserInfo, GovBotError } from "../Errors";
import logger from "../logging";
import { EndUserError, EndUserInfo} from "../Errors";


export class DiscordStatus {
Expand Down
78 changes: 63 additions & 15 deletions src/channels/admin/screens/FundingRoundLogic.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import sequelize from '../../../config/database';
import { TrackedInteraction } from '../../../core/BaseClasses';
import { EndUserError } from '../../../Errors';
import logger from '../../../logging';
import { FundingRound, Topic, ConsiderationPhase, DeliberationPhase, FundingVotingPhase, SMEGroup, SMEGroupMembership, FundingRoundDeliberationCommitteeSelection, FundingRoundApprovalVote, TopicSMEGroupProposalCreationLimiter, Proposal } from '../../../models';
import { FundingRound, Topic, ConsiderationPhase, DeliberationPhase, FundingVotingPhase, SMEGroup, SMEGroupMembership, FundingRoundDeliberationCommitteeSelection, FundingRoundApprovalVote, TopicSMEGroupProposalCreationLimiter, Proposal, TopicCommittee } from '../../../models';
import { FundingRoundMI, FundingRoundMIPhase, FundingRoundMIPhaseValue } from '../../../models/Interface';
import { FundingRoundAttributes, FundingRoundStatus, FundingRoundPhase, ProposalStatus } from '../../../types';
import { Op, Transaction } from 'sequelize';
Expand Down Expand Up @@ -351,19 +352,28 @@ export class FundingRoundLogic {
});
}

static async getEligibleVotingRounds(): Promise<FundingRound[]> {
static async getEligibleVotingRounds(interaction: TrackedInteraction): Promise<FundingRound[]> {
const duid: string = interaction.discordUserId;
const now = new Date();
const allFindingRoundsInVoting = await FundingRound.findAll({
where: {
status: FundingRoundStatus.VOTING,
votingOpenUntil: {
[Op.gte]: now,
},
},
});
const onlyReadyFundingRounds = allFindingRoundsInVoting.filter( value => value.isReady())
return onlyReadyFundingRounds;
}

const userFundingRounds = await FundingRoundLogic.getFundingRoundsForUser(duid);

const eligibleFundingRounds = userFundingRounds.filter((fr) =>
fr.status === FundingRoundStatus.VOTING &&
fr.votingOpenUntil >= now
);

const readyFundingRounds = await Promise.all(
eligibleFundingRounds.map(async (fr) => ({
fundingRound: fr,
isReady: await fr.isReady()
}))
);

return readyFundingRounds
.filter(({ isReady }) => isReady)
.map(({ fundingRound }) => fundingRound);
}

static async hasUserVotedOnFundingRound(userId: string, fundingRoundId: number): Promise<boolean> {
const vote = await FundingRoundApprovalVote.findOne({
Expand Down Expand Up @@ -846,12 +856,50 @@ export class FundingRoundLogic {
}

static async setTopic(fundingRoundId: number, topicId: number): Promise<FundingRound> {
const { TopicLogic } = await import('../../admin/screens/ManageTopicLogicScreen');
const { TopicLogic } = await import("../../../logic/TopicLogic");

const fundingRound = await this.getFundingRoundByIdOrError(fundingRoundId);
const topic = await TopicLogic.getByIdOrError(topicId);

return await fundingRound.update({ topicId: topic.id });
}
}

static async getFundingRoundsForUser(duid: string): Promise<FundingRound[]> {
// Get all SMEGroups the user belongs to
const userSMEGroups = await SMEGroupMembership.findAll({
where: { duid },
attributes: ['smeGroupId']
});

const userSMEGroupIds = userSMEGroups.map(group => group.smeGroupId);

// Find all TopicCommittees associated with the user's SMEGroups
const relevantTopicCommittees = await TopicCommittee.findAll({
where: {
smeGroupId: {
[Op.in]: userSMEGroupIds
}
},
attributes: ['topicId']
});

const relevantTopicIds = relevantTopicCommittees.map(committee => committee.topicId);

// Find all FundingRounds associated with these Topics
const fundingRounds = await FundingRound.findAll({
where: {
topicId: {
[Op.in]: relevantTopicIds
}
},
include: [
{
model: Topic,
as: 'topic'
}
]
});

return fundingRounds;
}
}
47 changes: 35 additions & 12 deletions src/channels/admin/screens/ManageFundingRoundsScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import { ArgumentOracle, CustomIDOracle } from '../../../CustomIDOracle';
import { ConsiderationPhase, DeliberationPhase, FundingRound, FundingVotingPhase, SMEGroup, Topic, TopicCommittee } from '../../../models';
import { InteractionProperties } from '../../../core/Interaction';
import { PaginationComponent } from '../../../components/PaginationComponent';
import { FundingRoundPhase, FundingRoundStatus } from '../../../types';
import { TopicLogic } from './ManageTopicLogicScreen';
import logger from '../../../logging';
import { EndUserError, NotFoundEndUserError } from '../../../Errors';
import { DiscordStatus } from '../../DiscordStatus';
import { FundingRoundMI, FundingRoundMIPhaseValue } from '../../../models/Interface';
import { InputDate } from '../../../dates/Input';
import { ExclusionConstraintError } from 'sequelize';
import { ApproveRejectFundingRoundPaginator, EditFundingRoundPaginator, FundingRoundPaginator, RemoveCommiteeFundingRoundPaginator, SetCommitteeFundingRoundPaginator } from '../../../components/FundingRoundPaginator';
import { TopicLogic } from '../../../logic/TopicLogic';



Expand Down Expand Up @@ -188,7 +186,7 @@ export class CreateOrEditFundingRoundAction extends Action {
}

private async handleShowProgress(interaction: TrackedInteraction): Promise<void> {
const fundingRoundId = ArgumentOracle.getNamedArgument(interaction, 'fundingRoundId', 0);
const fundingRoundId = ArgumentOracle.getNamedArgument(interaction, 'frId', 0);
const fundingRound = await FundingRoundLogic.getFundingRoundByIdOrError(parseInt(fundingRoundId));
const topic: Topic = await fundingRound.getTopic();
const onlyShowPhases: boolean = ArgumentOracle.isArgumentEquals(interaction, CreateOrEditFundingRoundAction.BOOLEANS.ARGUMENTS.ONLY_SHOW_PHASES, CreateOrEditFundingRoundAction.BOOLEANS.TRUE_VALUE);
Expand All @@ -204,7 +202,7 @@ export class CreateOrEditFundingRoundAction extends Action {
.setDescription(`Status: ${progress}`)
.addFields(
{ name: 'Topic', value: fundingRound.topicId ? `✅\n${topic.name}` : '❌', inline: true },
{ name: 'Core Information', value: fundingRound.name && fundingRound.description && fundingRound.budget ? `✅\nName: ${fundingRound.name}\nDescription: ${fundingRound.description}\nBudget: ${fundingRound.budget}` : '❌', inline: true },
{ name: 'Core Information', value: fundingRound.name && fundingRound.description && fundingRound.budget &&fundingRound.stakingLedgerEpoch ? `✅\nName: ${fundingRound.name}\nDescription: ${fundingRound.description}\nBudget: ${fundingRound.budget}\Epoch For Voting: ${fundingRound.stakingLedgerEpoch}` : '❌', inline: true },
{ name: 'Funding Round Dates', value: fundingRound.startAt && fundingRound.endAt && fundingRound.votingOpenUntil ? this.formatStringForRound(fundingRound) : '❌', inline: true },
{ name: 'Consideration Phase', value: considerationPhase ? this.formatStringForPhase(considerationPhase) : '❌', inline: true },
{ name: 'Deliberation Phase', value: deliberationPhase ? this.formatStringForPhase(deliberationPhase) : '❌', inline: true },
Expand Down Expand Up @@ -297,7 +295,8 @@ export class SelectTopicAction extends PaginationComponent {
}

protected async getItemsForPage(interaction: TrackedInteraction, page: number): Promise<Topic[]> {
const topics = await TopicLogic.getAllTopics();
const duid: string = interaction.discordUserId;
const topics = await TopicLogic.getTopicsForSMEMember(duid);
return topics.slice(page * 25, (page + 1) * 25);
}

Expand Down Expand Up @@ -465,9 +464,9 @@ export class CoreInformationAction extends Action {
parsedTopicId = topicId as string;
}


const customId: string = fundingRoundId ? CustomIDOracle.addArgumentsToAction(this, CoreInformationAction.OPERATIONS.SUBMIT_FORM, CoreInformationAction.INPUT_IDS.TOPIC, parsedTopicId, ArgumentOracle.COMMON_ARGS.FUNDING_ROUND_ID, fundingRoundId) : CustomIDOracle.addArgumentsToAction(this, CoreInformationAction.OPERATIONS.SUBMIT_FORM, CoreInformationAction.INPUT_IDS.TOPIC, parsedTopicId);
const modal = new ModalBuilder()
.setCustomId(CustomIDOracle.addArgumentsToAction(this, CoreInformationAction.OPERATIONS.SUBMIT_FORM, CoreInformationAction.INPUT_IDS.TOPIC, parsedTopicId))
.setCustomId(customId)
.setTitle('Funding Round Core Information');

const nameInput = new TextInputBuilder()
Expand All @@ -493,7 +492,7 @@ export class CoreInformationAction extends Action {

const stakingLedgerEpochInput = new TextInputBuilder()
.setCustomId(CoreInformationAction.INPUT_IDS.STAKING_LEDGER_EPOCH)
.setLabel('Staking Ledger Epoch Number')
.setLabel('Staking Ledger Epoch For Voting')
.setStyle(TextInputStyle.Short)
.setValue(stakingLedgerEpochValue)
.setRequired(true);
Expand All @@ -518,8 +517,32 @@ export class CoreInformationAction extends Action {
const budget = parseFloat(modalInteraction.fields.getTextInputValue(CoreInformationAction.INPUT_IDS.BUDGET));
const stakingLedgerEpoch = parseInt(modalInteraction.fields.getTextInputValue(CoreInformationAction.INPUT_IDS.STAKING_LEDGER_EPOCH));

const fundingRoung: FundingRound = await FundingRoundLogic.newFundingRoundFromCoreInfo(name, description, parseInt(topicId), budget, stakingLedgerEpoch);
interaction.Context.set(ArgumentOracle.COMMON_ARGS.FUNDING_ROUND_ID, fundingRoung.id.toString());
let fundingRound: FundingRound;
let fundingRoundId: string | undefined;

try {
fundingRoundId = ArgumentOracle.getNamedArgument(interaction, ArgumentOracle.COMMON_ARGS.FUNDING_ROUND_ID);
} catch (error) {
if (!(error instanceof NotFoundEndUserError)) {
throw error;
}
}

if (fundingRoundId) {
// Update existing funding round
fundingRound = await FundingRoundLogic.updateFundingRound(parseInt(fundingRoundId), {
name,
description,
topicId: parseInt(topicId),
budget,
stakingLedgerEpoch
});
} else {
// Create new funding round
fundingRound = await FundingRoundLogic.newFundingRoundFromCoreInfo(name, description, parseInt(topicId), budget, stakingLedgerEpoch);
}

interaction.Context.set(ArgumentOracle.COMMON_ARGS.FUNDING_ROUND_ID, fundingRound.id.toString());

await (this.screen as ManageFundingRoundsScreen).createFundingRoundAction.handleOperation(
interaction,
Expand Down Expand Up @@ -556,7 +579,7 @@ export class SetPhaseAction extends Action {
};

public static readonly ARGUMENTS = {
PHASE: 'phase',
PHASE: 'ph',
}

public static PHASE_OPTIONS = {
Expand Down
Loading

0 comments on commit e0ec505

Please sign in to comment.