185 lines
No EOL
9 KiB
TypeScript
185 lines
No EOL
9 KiB
TypeScript
import {REST, Routes, Client, GatewayIntentBits, CommandInteraction, EmbedBuilder, CommandInteractionOption, CommandInteractionOptionResolver } from "discord.js";
|
|
import * as fs from "node:fs";
|
|
import commands from "./commands.js";
|
|
import {MakePrediction} from "./predictor.js";
|
|
import Election from "./election.js";
|
|
import CandidateEmojis from "./candidateEmojis.js";
|
|
import Elections from "./elections/elections.js";
|
|
import VoteType from "./VoteType.js";
|
|
|
|
const configraw = fs.readFileSync("config.json", "utf-8");
|
|
const config = JSON.parse(configraw);
|
|
|
|
if (!config.token) {
|
|
console.error("Please provide a Token and Client ID in config.json");
|
|
process.exit(1);
|
|
}
|
|
|
|
(async () => {
|
|
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
|
|
|
client.on('ready', async () => {
|
|
console.log(`Logged in as ${client.user!.tag}!`);
|
|
await publishSlashCommands(client.application!.id);
|
|
});
|
|
|
|
client.on('interactionCreate', async i => {
|
|
if (i instanceof CommandInteraction) {
|
|
await i.deferReply();
|
|
switch (i.commandName) {
|
|
case "approval":
|
|
// TODO: Replace this with API!
|
|
let poll_data = JSON.parse(fs.readFileSync('data/approval_rating.json', "utf-8"));
|
|
let polls = "";
|
|
var source = i.options.get('source')?.value as string
|
|
switch (source) {
|
|
case "thehill":
|
|
// source: https://elections2024.thehill.com/national/biden-approval-rating/
|
|
polls = `${poll_data.thehill.description}:\n\n`;
|
|
Object.keys(poll_data.thehill.polls).forEach(poll => {
|
|
polls += `:red_circle: **${poll_data.thehill.polls[poll].pollster}** - (**${poll_data.thehill.polls[poll].date}**), ${poll_data.thehill.polls[poll].sample}, **${poll_data.thehill.polls[poll].approve}**% approve, **${poll_data.thehill.polls[poll].disapprove}**% disapprove (**${poll_data.thehill.polls[poll].spread}**)\n`;
|
|
});
|
|
break;
|
|
case "fivethirtyeight":
|
|
// source: https://projects.fivethirtyeight.com/biden-approval-rating/
|
|
polls = `${poll_data.fivethirtyeight.description}:\n\n`;
|
|
Object.keys(poll_data.fivethirtyeight.polls).forEach(poll => {
|
|
polls += `:red_circle: **${poll_data.fivethirtyeight.polls[poll].pollster}** - (**${poll_data.fivethirtyeight.polls[poll].date}**), ${poll_data.fivethirtyeight.polls[poll].sample}, **${poll_data.fivethirtyeight.polls[poll].approve}**% approve, **${poll_data.fivethirtyeight.polls[poll].disapprove}**% disapprove (adjusted: **${poll_data.fivethirtyeight.polls[poll].approve_adjusted}**% approve, **${poll_data.fivethirtyeight.polls[poll].disapprove_adjusted}**% disapprove) (**${poll_data.fivethirtyeight.polls[poll].spread}**)\n`;
|
|
});
|
|
break;
|
|
default:
|
|
// source: https://www.realclearpolling.com/polls/approval/joe-biden/approval-rating
|
|
polls = `${poll_data.rcp.description}:\n\n`;
|
|
Object.keys(poll_data.rcp.polls).forEach(poll => {
|
|
polls += `:red_circle: **${poll_data.rcp.polls[poll].pollster}** - (**${poll_data.rcp.polls[poll].date}**), ${poll_data.rcp.polls[poll].sample}, **${poll_data.rcp.polls[poll].approve}**% approve, **${poll_data.rcp.polls[poll].disapprove}**% disapprove (spread: **${poll_data.rcp.polls[poll].spread}**)\n`;
|
|
});
|
|
break;
|
|
}
|
|
await i.editReply(`${polls}`);
|
|
break;
|
|
|
|
case "test":
|
|
var country = i.options.get('country')?.value as string
|
|
var election_type = i.options.get('type')?.value as string
|
|
var year = Number(i.options.get('year')?.value)
|
|
let query = await fetch(`http://127.0.0.1:3000/api/v1/election/${country}/${election_type}/${year}`); // temp test url
|
|
let results = await query.json();
|
|
|
|
if (query.ok) {
|
|
if(results[year] != undefined) {
|
|
var embed = new EmbedBuilder()
|
|
.setTitle(` ${results[year][0].election_name}`)
|
|
.setDescription("API test")
|
|
.addFields(Object.values(results[year][0].candidates).map((candidate: any) => ({
|
|
name: `${candidate.winner ? ":white_check_mark:" : ""} ${CandidateEmojis[candidate.party] ?? ""} ${candidate.name} ${candidate.incumbent ? "(I)" : ""} (${candidate.party})`,
|
|
value: `${results[year][0].election_type === "electoral" ? `${candidate.electoral_votes} electoral votes\n` : ""}${results[year][0].election_type === "parliament" ? `${candidate.seats_won} seats\n` : ""}${candidate.states_carried !== null && candidate.states_carried !== undefined ? `${candidate.states_carried} states carried\n` : ""}${candidate.delegates !== null && candidate.delegates !== undefined ? `${candidate.delegates} delegates\n` : ""}${candidate.votes !== null && candidate.votes !== undefined ? `${candidate.votes.toLocaleString()} votes (${candidate.percent}%)` : ""}`,
|
|
inline: true
|
|
})))
|
|
.setTimestamp();
|
|
|
|
if(results[year][0].has_map === true) {
|
|
const map = await fetch(`http://127.0.0.1:3000/api/v1/election/${country}/${election_type}/${year}/map`); // temp test url
|
|
if (map.ok) {
|
|
const arrayBuffer = await map.arrayBuffer();
|
|
const buffer = Buffer.from(arrayBuffer);
|
|
embed.setImage("attachment://map.png");
|
|
await i.editReply({embeds: [embed], files: [{attachment: buffer, name: "map.png"}]});
|
|
break;
|
|
} else {
|
|
await i.editReply({embeds: [embed]});
|
|
break;
|
|
}
|
|
} else {
|
|
await i.editReply({embeds: [embed]});
|
|
break;
|
|
}
|
|
} else {
|
|
await i.editReply("Election not found");
|
|
break;
|
|
}
|
|
} else {
|
|
await i.editReply("An error occurred fetching the data");
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case "simulate":
|
|
var electionname = (i.options as CommandInteractionOptionResolver).getSubcommand();
|
|
var overlayimage = (i.options as CommandInteractionOptionResolver).getAttachment('image_overlay');
|
|
var overlayopacity = Number(i.options.get('image_opacity')?.value);
|
|
|
|
var election = structuredClone(Elections[electionname]);
|
|
for (const candidate of election.candidates) {
|
|
var option = (i.options as CommandInteractionOptionResolver).getString(candidate.party.toLowerCase().replace(/ /g, "_") + "_candidate");
|
|
if (option) {
|
|
candidate.name = option;
|
|
}
|
|
var bias = (i.options as CommandInteractionOptionResolver).getNumber(candidate.party.toLowerCase().replace(/ /g, "_") + "_bias");
|
|
if (bias) {
|
|
for (const state of Object.keys(election.states)) {
|
|
election.states[state].odds[candidate.party] += bias;
|
|
}
|
|
}
|
|
}
|
|
|
|
var newcandidate = (i.options as CommandInteractionOptionResolver).getString("add_candidate");
|
|
if (newcandidate) {
|
|
var party = (i.options as CommandInteractionOptionResolver).getString("with_party") || "Independent";
|
|
var color = (i.options as CommandInteractionOptionResolver).getString("with_color") || "#bfab22";
|
|
var odds = (i.options as CommandInteractionOptionResolver).getNumber("with_odds") || 0.33;
|
|
if (!/^#[0-9A-Fa-f]{3,6}$/.test(color)) {
|
|
await i.editReply("Please provide a valid hex color code");
|
|
return;
|
|
}
|
|
if (election.candidates.some(c => c.party === party || c.name === newcandidate)) {
|
|
await i.editReply("A candidate with that name or party already exists");
|
|
return
|
|
}
|
|
election.candidates.push({
|
|
name: newcandidate,
|
|
party: party,
|
|
color: color
|
|
});
|
|
for (const state of Object.keys(election.states))
|
|
election.states[state].odds[party] = odds;
|
|
}
|
|
if (overlayimage !== null) {
|
|
if (overlayimage.size > 5000000) {
|
|
await i.editReply("Keep images under 5 MB please!");
|
|
return;
|
|
}
|
|
if (overlayimage.contentType?.substring(0, 5) != "image") {
|
|
await i.editReply("Invalid image!");
|
|
return;
|
|
}
|
|
}
|
|
if (overlayopacity > 100 || overlayopacity < 0) {
|
|
await i.editReply("Invalid opacity!");
|
|
return;
|
|
}
|
|
var result = await MakePrediction(election, overlayimage?.url, overlayopacity ? Math.round((overlayopacity / 100) * 255) : 255);
|
|
var embed = new EmbedBuilder()
|
|
.setTitle(election.title)
|
|
.setDescription(election.description.replace("$WINNER", result.winner))
|
|
.addFields(result.candidates.map(c => {
|
|
return {
|
|
name: `${(result.winner === c.name ? ":white_check_mark:" : "")} ${c.name} (${c.party})`,
|
|
value: `${election.voteType === VoteType.Electoral ? `${c.electoralVotes} electoral votes\n` : ""}${c.votes.toLocaleString()} votes (${((c.votes / result.totalVotes) * 100).toFixed(2)}%)`,
|
|
inline: true
|
|
}
|
|
}))
|
|
.setImage("attachment://election.png")
|
|
.setTimestamp();
|
|
await i.editReply({embeds: [embed], files: [{attachment: result.png, name: "election.png"}]});
|
|
}
|
|
}
|
|
});
|
|
|
|
client.login(config.token);
|
|
})();
|
|
|
|
async function publishSlashCommands(clientid : string) {
|
|
const rest = new REST({ version: '10' }).setToken(config.token);
|
|
await rest.put(Routes.applicationCommands(clientid), {body: commands});
|
|
console.log("Successfully registered slash commands");
|
|
} |