diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..ae0b429 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,74 @@ +const axios = require("axios").default; +import { + QUOTE_URL, + DISCORD_WEBHOOK, + DISCORD_API_BASE, + DISCORD_CHANNEL_ID, + DISCORD_OAUTH_TOKEN, + DISCORD_WEBHOOK_USERNAME +} from "./config"; +import { LOAD_USED_QUOTES } from "./database"; + + + +export const GET_QUOTE = async (count = 1): Promise => { + return axios.get( + QUOTE_URL + ).then((response: any) => { + let used_quotes = LOAD_USED_QUOTES(); + let quotes = response.data.split("\n"); + let quote = quotes[Math.floor(Math.random() * quotes.length)]; + + while (used_quotes.includes(quote)) { + quote = quotes[Math.floor(Math.random() * quotes.length)]; + }; + used_quotes.push(quote); + + return quote + }) + .catch((err: any) => {throw err}); +} + + +export const GET_MESSAGE = async (id: string): Promise => { + return axios.get( + `${DISCORD_API_BASE}/channels/${DISCORD_CHANNEL_ID}/messages/${id}`, + { + headers: { + Authorization: `Bot ${DISCORD_OAUTH_TOKEN}` + } + } + ) + .then((response:any) => { + response = response.data; + + if (response.reactions) { + return { + reactions: response.reactions, + quote_a: response.embeds[0].fields[0], + quote_b: response.embeds[0].fields[1] + } + } else { + return { + reactions: [], + quote_a: response.embeds[0].fields[0], + quote_b: response.embeds[0].fields[1] + } + } + }).catch((err:any) => {throw err}); +}; + + + +export const WEBHOOK_PUSH = async (embed: any): Promise => { + return await axios.post( + `${DISCORD_WEBHOOK}?wait=true`, + { + username: DISCORD_WEBHOOK_USERNAME, + embeds: [embed] + } + ) + .then((response: any) => { + return response.data.id; + }).catch((err:any) => {throw err}); +}; \ No newline at end of file diff --git a/src/config.template.ts b/src/config.template.ts new file mode 100644 index 0000000..fd06e5b --- /dev/null +++ b/src/config.template.ts @@ -0,0 +1,30 @@ +// A bot OAuth token that has access to the channels we are looking in. +export const DISCORD_OAUTH_TOKEN: string = ``; + +// The Discord channel ID that the bot will be posting in +export const DISCORD_CHANNEL_ID: string = ``; + +// The Discord webhook that we will be posting into +export const DISCORD_WEBHOOK: string = ``; + +// The name of the webhook in Discord, if left undefined it will use the +// default name from within Discord +export const DISCORD_WEBHOOK_USERNAME: string|undefined = undefined; + +// The file names for the database operations +export const DB_NAME: string = `used_quotes.json` +export const MSG_ID_FILE: string = `msg_id` + +// The twitch.center URL for the quote list +export const QUOTE_URL: string = ``; + + +// The base API endpoint for Discord +export const DISCORD_API_BASE: string = `https://discordapp.com/api/v6` + + +// The details for the Discord emoji +export const EMOJI_A_ID: string = `701289852146286633`; +export const EMOJI_B_ID: string = `701289851923988540`; +export const EMOJI_A_NAME: string = `star`; +export const EMOJI_B_NAME: string = `like`; \ No newline at end of file diff --git a/src/database.ts b/src/database.ts new file mode 100644 index 0000000..3477144 --- /dev/null +++ b/src/database.ts @@ -0,0 +1,21 @@ +import { MSG_ID_FILE, DB_NAME } from "./config"; +import * as fs from "fs"; + +export const LOAD_MSG_ID = (): string => { + return fs.readFileSync(`./${MSG_ID_FILE}`, `utf8`) +}; + + +export const WRITE_MSG_ID = (new_id: string) => { + return fs.writeFileSync(`./${MSG_ID_FILE}`, new_id) +}; + + +export const LOAD_USED_QUOTES = (): string[] => { + return JSON.parse(fs.readFileSync(`./${DB_NAME}`, `utf`)); +}; + + +export const WRITE_USED_QUOTES = (data: string[]) => { + fs.writeFileSync(`${DB_NAME}`, JSON.stringify(data)); +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..032e549 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,100 @@ +import { LOAD_MSG_ID, WRITE_MSG_ID } from "./database"; +import { GET_MESSAGE, GET_QUOTE, WEBHOOK_PUSH } from "./api"; +import { EMOJI_A_ID, EMOJI_B_ID, EMOJI_B_NAME, EMOJI_A_NAME } from "./config"; + +const MAIN = async () => { + + // Load message ID from file + let msg_id: string = LOAD_MSG_ID(); + + // Get message data + let msg: msg_meta = await GET_MESSAGE(msg_id); + + + + // Compare values of reaction counts + let emoji_a: reaction|undefined = msg.reactions.find( + (reaction: reaction) => {return reaction.emoji.id === EMOJI_A_ID} + ); + let emoji_b: reaction|undefined = msg.reactions.find( + (reaction: reaction) => {return reaction.emoji.id === EMOJI_B_ID} + ); + + + // Both quotes have had a reaction + let winning_emoji: reaction|"TIE"|"NO_DATA"; + if (emoji_a !== undefined && emoji_b !== undefined) { + if (emoji_a!.count === emoji_b!.count) { + winning_emoji = "TIE"; + } + else if (emoji_a!.count > emoji_b!.count) { + winning_emoji = emoji_a!; + } + else if (emoji_a!.count < emoji_b!.count) { + winning_emoji = emoji_b!; + }; + } + + // Only emoji_a has had a reaction + else if (emoji_a !== undefined && emoji_b === undefined) { winning_emoji = emoji_a!; } + + // Only emoji_b has had a reaction + else if (emoji_a === undefined && emoji_b !== undefined) { winning_emoji = emoji_b!; } + + // Neither emoji has had a reaction + else { winning_emoji = "NO_DATA"; } + + winning_emoji = winning_emoji!; + + + var new_quote_a: string, new_quote_b: string; + // Get new quotes + switch (winning_emoji) { + + // Discard both previous quotes + case "TIE": + case "NO_DATA": + new_quote_a = await GET_QUOTE(); + new_quote_b = await GET_QUOTE(); + break; + + // Only get one new quote + default: + winning_emoji = winning_emoji as reaction; + let split_str: string = " - **" + if (winning_emoji.emoji.id === EMOJI_A_ID) { + new_quote_a = msg.quote_a.value.split(split_str, 2)[1].slice(0, -2); + } else { + new_quote_a = msg.quote_b.value.split(split_str, 2)[1].slice(0, -2); + }; + new_quote_b = await GET_QUOTE(); + break; + }; + + + // Construct new embed + let embed: any = { + title: `Quote Bracketeering`, + description: `Vote for your favourite quote by reacting to this message with the proper emoji!\n\n\n**Note:** If both quotes end in the same number of votes they will __**BOTH**__ be eliminated.`, + color: 43520, + fields: [ + { + name: `Quote A:`, + value: `<:${EMOJI_A_NAME}:${EMOJI_A_ID}> - **${new_quote_a}**`, + inline: false + }, + { + name: `Quote B:`, + value: `<:${EMOJI_B_NAME}:${EMOJI_B_ID}> - **${new_quote_b}**`, + inline: false + } + ] + } + // Post to webhook and store new message ID + let new_msg_id = await WEBHOOK_PUSH(embed); + + WRITE_MSG_ID(new_msg_id); +}; + + +MAIN(); \ No newline at end of file diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..48f2e12 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,26 @@ +type snowflake = number; + +interface emoji { + name: string; + id: string; +} + +interface reaction { + emoji: emoji; + count: number; + me: boolean; +} + + +interface embed_field { + name: string; + value: string; + inline: boolean; +} + + +interface msg_meta { + reactions: reaction[]; + quote_a: embed_field; + quote_b: embed_field; +} \ No newline at end of file