Add code to make the stuff work
This commit is contained in:
parent
a18b73fe52
commit
3d9feabe88
15 changed files with 517 additions and 0 deletions
14
src/constants.ts
Normal file
14
src/constants.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
export var DB_DEFAULTS: database = {
|
||||||
|
bracket: {
|
||||||
|
msg: "",
|
||||||
|
quotes: [],
|
||||||
|
votes: {},
|
||||||
|
users: {},
|
||||||
|
},
|
||||||
|
webhook: {
|
||||||
|
id: "",
|
||||||
|
token: ""
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export var DISCORD_API_URI: string = `https://discord.com/api/v8`;
|
||||||
29
src/endpoints/discord/auth/callback.ts
Normal file
29
src/endpoints/discord/auth/callback.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Request, ResponseToolkit } from "@hapi/hapi";
|
||||||
|
import { config, db } from "@/main";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
method: `GET`, path: `/discord/auth/callback`,
|
||||||
|
async handler(request: Request, h: ResponseToolkit) {
|
||||||
|
console.log(`Authentication finishing!`)
|
||||||
|
let code = request.query.code;
|
||||||
|
|
||||||
|
let data = new URLSearchParams();
|
||||||
|
data.set(`client_id`, config.discord.client_id);
|
||||||
|
data.set(`client_secret`, config.discord.secret);
|
||||||
|
data.set(`grant_type`, `authorization_code`);
|
||||||
|
data.set(`code`, code);
|
||||||
|
data.set(`redirect_uri`, config.discord.auth_redirect);
|
||||||
|
|
||||||
|
let r = await axios.post(`https://discord.com/api/v8/oauth2/token`, data, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': `application/x-www-form-urlencoded`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
db.webhook.token = r.data.webhook.token;
|
||||||
|
db.webhook.id = r.data.webhook.id;
|
||||||
|
|
||||||
|
return r.data;
|
||||||
|
},
|
||||||
|
}
|
||||||
11
src/endpoints/discord/auth/redirect.ts
Normal file
11
src/endpoints/discord/auth/redirect.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Request, ResponseToolkit } from "@hapi/hapi";
|
||||||
|
import { config } from "@/main";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
method: `GET`, path: `/discord/auth`,
|
||||||
|
async handler(request: Request, h: ResponseToolkit) {
|
||||||
|
return h.redirect(
|
||||||
|
`https://discord.com/api/oauth2/authorize?client_id=${config.discord.client_id}&redirect_uri=${encodeURIComponent(config.discord.auth_redirect)}&response_type=code&scope=webhook.incoming`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
75
src/endpoints/discord/webhook.ts
Normal file
75
src/endpoints/discord/webhook.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { selectQuote } from "@/utils/components/dropdowns/select_quote";
|
||||||
|
import { deleteVote } from "@/utils/components/buttons/delete_vote";
|
||||||
|
import { countVotes } from "@/utils/components/buttons/count_votes";
|
||||||
|
import { showUserVote } from "@/utils/components/buttons/my_vote";
|
||||||
|
import { Request, ResponseToolkit } from "@hapi/hapi";
|
||||||
|
import { config } from "@/main";
|
||||||
|
import boom from "@hapi/boom";
|
||||||
|
import nacl from "tweetnacl";
|
||||||
|
|
||||||
|
import { viewDB } from "@/utils/components/buttons/view_db";
|
||||||
|
|
||||||
|
|
||||||
|
async function handleButton(data: any): Promise<object> {
|
||||||
|
switch (data.data.custom_id) {
|
||||||
|
case "deleteVote":
|
||||||
|
return await deleteVote(data);
|
||||||
|
case "showCount":
|
||||||
|
return await countVotes(data);
|
||||||
|
case "viewDB":
|
||||||
|
return await viewDB(data);
|
||||||
|
case "showMyVote":
|
||||||
|
return await showUserVote(data);
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
type: 4,
|
||||||
|
data: {
|
||||||
|
content: `Unknown button, how did you trigger this response? 0\_o`,
|
||||||
|
flags: 1 << 6,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
method: `POST`, path: `/discord/webhook`,
|
||||||
|
async handler(request: Request, h: ResponseToolkit) {
|
||||||
|
let sig = request.headers[`x-signature-ed25519`];
|
||||||
|
let timestamp = request.headers[`x-signature-timestamp`];
|
||||||
|
let body: any = request.payload;
|
||||||
|
|
||||||
|
|
||||||
|
// Verify the body against Discord's stuff
|
||||||
|
let verified = nacl.sign.detached.verify(
|
||||||
|
Buffer.from(timestamp + JSON.stringify(body)),
|
||||||
|
Buffer.from(sig, `hex`),
|
||||||
|
Buffer.from(config.discord.public_key, `hex`)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!verified) {
|
||||||
|
return boom.unauthorized(`invalid request signature`);
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (body.type) {
|
||||||
|
case 1:
|
||||||
|
return { type: 1 };
|
||||||
|
case 3:
|
||||||
|
|
||||||
|
// Parse the data properly
|
||||||
|
if (body.data.component_type === 3) {
|
||||||
|
return await selectQuote(body)
|
||||||
|
} else if (body.data.component_type === 2) {
|
||||||
|
return await handleButton(body)
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 4,
|
||||||
|
data: {
|
||||||
|
content: `Unknown component type.`,
|
||||||
|
flags: 1 << 6,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return boom.badRequest()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
122
src/endpoints/management/create_bracket.ts
Normal file
122
src/endpoints/management/create_bracket.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { Request, ResponseToolkit } from "@hapi/hapi";
|
||||||
|
import { DISCORD_API_URI } from "@/constants";
|
||||||
|
import { getQuote } from "@/utils/quotes";
|
||||||
|
import { config, db } from "@/main";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
method: `GET`, path: `/bracket/finish`,
|
||||||
|
async handler(request: Request, h: ResponseToolkit) {
|
||||||
|
|
||||||
|
if (!db.bracket.msg) {
|
||||||
|
var quotes = await getQuote(config.discord.quote_max);
|
||||||
|
} else {
|
||||||
|
// Delete the old message from Discord while processing the new one
|
||||||
|
let wh = db.webhook;
|
||||||
|
await axios.delete(`${DISCORD_API_URI}/webhooks/${wh.id}/${wh.token}/messages/${db.bracket.msg}`);
|
||||||
|
|
||||||
|
// Save the previous bracket to the history file
|
||||||
|
let pastBrackets = JSON.parse(
|
||||||
|
await fs.readFile(config.server.bracket_history, `utf-8`)
|
||||||
|
);
|
||||||
|
pastBrackets.push({
|
||||||
|
quotes: db.bracket.quotes,
|
||||||
|
votes: db.bracket.votes,
|
||||||
|
});
|
||||||
|
await fs.writeFile(config.server.bracket_history, JSON.stringify(pastBrackets));
|
||||||
|
|
||||||
|
|
||||||
|
// Calculate the winners from the previous bracket
|
||||||
|
let r = await request.server.inject(`/bracket/winners`);
|
||||||
|
var quotes: string[] = JSON.parse(r.payload).winners;
|
||||||
|
var winner_count = quotes.length;
|
||||||
|
|
||||||
|
// Get any new quotes for the bracket
|
||||||
|
quotes.push(...(await getQuote(config.discord.quote_max - quotes.length)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the database for the new bracket
|
||||||
|
db.bracket.quotes = quotes;
|
||||||
|
db.bracket.votes = {};
|
||||||
|
db.bracket.users = {};
|
||||||
|
db.bracket.msg = "";
|
||||||
|
|
||||||
|
|
||||||
|
let message = {
|
||||||
|
content: `New Quote Bracket!`,
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
description: `Note: If **more than ${Math.floor(config.discord.quote_max / 2)}** of the quotes tie, they will all be eliminated, otherwise, the ones that tie will move on to the next bracket.`,
|
||||||
|
fields: quotes.map((quote, i) => { return {
|
||||||
|
name: `${i < winner_count ? '👑 ' : ''}Quote: ${i + 1}`,
|
||||||
|
value: quote,
|
||||||
|
}}),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
custom_id: `quote`,
|
||||||
|
placeholder: `Choose Your Favourite Quote`,
|
||||||
|
options:quotes.map((_, i) => {
|
||||||
|
return {
|
||||||
|
label: `Quote ${i + 1}`,
|
||||||
|
value: i,
|
||||||
|
emoji: i < winner_count ? {
|
||||||
|
name: `👑`
|
||||||
|
} : null
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
style: 1,
|
||||||
|
label: `What Did I Vote For?`,
|
||||||
|
custom_id: `showMyVote`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
style: 4,
|
||||||
|
label: `Remove Vote`,
|
||||||
|
custom_id: `deleteVote`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.discord.dev_buttons) {
|
||||||
|
message.components.push({
|
||||||
|
type: 1,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
style: 1,
|
||||||
|
label: `See Count`,
|
||||||
|
custom_id: `showCount`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
style: 1,
|
||||||
|
label: `See Database Object`,
|
||||||
|
custom_id: `viewDB`,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = `${DISCORD_API_URI}/webhooks/${db.webhook.id}/${db.webhook.token}`;
|
||||||
|
let r = await axios.post(url, message, { params: { wait: true } });
|
||||||
|
db.bracket.msg = r.data.id;
|
||||||
|
return { status: r.status }
|
||||||
|
},
|
||||||
|
}
|
||||||
26
src/endpoints/management/get_winners.ts
Normal file
26
src/endpoints/management/get_winners.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { config, db } from "@/main"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
method: `GET`, path: `/bracket/winners`,
|
||||||
|
async handler() {
|
||||||
|
let winners: string[] = [];
|
||||||
|
let highest = -1;
|
||||||
|
|
||||||
|
// Find the winners of the quote
|
||||||
|
for (var i in db.bracket.quotes) {
|
||||||
|
if (db.bracket.votes[i] > highest) {
|
||||||
|
winners = [ db.bracket.quotes[i] ];
|
||||||
|
highest = db.bracket.votes[i];
|
||||||
|
} else if (db.bracket.votes[i] === highest) {
|
||||||
|
winners.push(db.bracket.quotes[i]);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure that the all elimination limit didn't get hit
|
||||||
|
if (winners.length > Math.floor(config.discord.quote_max / 2)) {
|
||||||
|
return { winners: [] };
|
||||||
|
};
|
||||||
|
|
||||||
|
return { winners };
|
||||||
|
},
|
||||||
|
}
|
||||||
61
src/main.ts
61
src/main.ts
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Filepath alias resolution
|
||||||
|
import "module-alias/register";
|
||||||
|
|
||||||
|
// Begin personal code
|
||||||
|
import { DB_DEFAULTS } from "@/constants";
|
||||||
|
import { Server } from "@hapi/hapi";
|
||||||
|
import path from "path";
|
||||||
|
import glob from "glob";
|
||||||
|
import toml from "toml";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
// load the config
|
||||||
|
if (!fs.existsSync(`config.toml`)) {
|
||||||
|
console.log(`Please fill out the config and then try starting the server again.`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
export const config: config = toml.parse(fs.readFileSync(`config.toml`, `utf-8`));
|
||||||
|
|
||||||
|
|
||||||
|
// Load the database
|
||||||
|
if (!fs.existsSync(config.server.db_file)) {
|
||||||
|
console.log(`Can't find database file, creating default`);
|
||||||
|
fs.writeFileSync(config.server.db_file, JSON.stringify(DB_DEFAULTS));
|
||||||
|
};
|
||||||
|
export var db: database = JSON.parse(fs.readFileSync(config.server.db_file, `utf-8`));
|
||||||
|
|
||||||
|
|
||||||
|
process.on(`SIGINT`, () => {
|
||||||
|
console.log(`Saving database`);
|
||||||
|
fs.writeFileSync(config.server.db_file, JSON.stringify(db));
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
|
||||||
|
const server = new Server({
|
||||||
|
port: config.server.port,
|
||||||
|
debug: {
|
||||||
|
request: [ `error` ]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Register all the routes
|
||||||
|
let files = glob.sync(
|
||||||
|
`endpoints/**/!(*.map)`,
|
||||||
|
{ cwd: __dirname, nodir: true}
|
||||||
|
);
|
||||||
|
for (var file of files) {
|
||||||
|
let route = (await import(path.join(__dirname, file))).default;
|
||||||
|
console.log(`Registering route: ${route.method} ${route.path}`);
|
||||||
|
server.route(route);
|
||||||
|
};
|
||||||
|
|
||||||
|
server.start().then(() => {
|
||||||
|
console.log(`Server listening on ${config.server.host}:${config.server.port}`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
21
src/types/config.d.ts
vendored
Normal file
21
src/types/config.d.ts
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
interface config {
|
||||||
|
discord: {
|
||||||
|
quote_max: number;
|
||||||
|
client_id: string;
|
||||||
|
secret: string;
|
||||||
|
public_key: string;
|
||||||
|
auth_redirect: string;
|
||||||
|
dev_buttons: boolean;
|
||||||
|
};
|
||||||
|
server: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
db_file: string;
|
||||||
|
quote_history: string;
|
||||||
|
bracket_history: string;
|
||||||
|
};
|
||||||
|
quote: {
|
||||||
|
api_base: string;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
12
src/types/database.d.ts
vendored
Normal file
12
src/types/database.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
interface database {
|
||||||
|
webhook: {
|
||||||
|
id: string;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
bracket: {
|
||||||
|
msg: string;
|
||||||
|
quotes: string[];
|
||||||
|
votes: { [index: number]: number };
|
||||||
|
users: { [index: string]: number };
|
||||||
|
};
|
||||||
|
}
|
||||||
17
src/utils/components/buttons/count_votes.ts
Normal file
17
src/utils/components/buttons/count_votes.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { db } from "@/main";
|
||||||
|
|
||||||
|
|
||||||
|
export async function countVotes(data: any): Promise<object> {
|
||||||
|
|
||||||
|
let response = `Quote Votes:`;
|
||||||
|
for (var i in db.bracket.quotes) {
|
||||||
|
response += `\n${db.bracket.votes[i] ?? 0} votes for \`${db.bracket.quotes[i]}\``;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
type: 4,
|
||||||
|
data: {
|
||||||
|
content: response,
|
||||||
|
flags: 1 << 6,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
30
src/utils/components/buttons/delete_vote.ts
Normal file
30
src/utils/components/buttons/delete_vote.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { db } from "@/main";
|
||||||
|
|
||||||
|
|
||||||
|
export async function deleteVote(data: any): Promise<object> {
|
||||||
|
let userID = data.member.user.id;
|
||||||
|
|
||||||
|
if (!db.bracket.users[userID]) {
|
||||||
|
return {
|
||||||
|
type: 4,
|
||||||
|
data: {
|
||||||
|
content: `You haven't voted in the bracket, so you can't delete your vote.`,
|
||||||
|
flags: 1 << 6,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subtract user's vote from total
|
||||||
|
let vote = db.bracket.users[userID];
|
||||||
|
--db.bracket.votes[vote];
|
||||||
|
|
||||||
|
delete db.bracket.users[userID];
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 4,
|
||||||
|
data: {
|
||||||
|
content: `Your vote has been deleted.`,
|
||||||
|
flags: 1 << 6,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
19
src/utils/components/buttons/my_vote.ts
Normal file
19
src/utils/components/buttons/my_vote.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { db } from "@/main";
|
||||||
|
|
||||||
|
export async function showUserVote(data: any): Promise<object> {
|
||||||
|
let vote = db.bracket.users[data.member.user.id];
|
||||||
|
let quote = db.bracket.quotes[vote];
|
||||||
|
|
||||||
|
let response = `You currently haven't voted for a quote!`;
|
||||||
|
if (quote) {
|
||||||
|
response = `Your vote is for:\n> ${quote}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 4,
|
||||||
|
data: {
|
||||||
|
content: response,
|
||||||
|
flags: 1 << 6,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
12
src/utils/components/buttons/view_db.ts
Normal file
12
src/utils/components/buttons/view_db.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { db } from "@/main";
|
||||||
|
|
||||||
|
|
||||||
|
export async function viewDB(data: any): Promise<object> {
|
||||||
|
return {
|
||||||
|
type: 4,
|
||||||
|
data: {
|
||||||
|
content: `\`\`\`json\n${JSON.stringify(db.bracket, null, ' ')}\`\`\``,
|
||||||
|
flags: 1 << 6,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
40
src/utils/components/dropdowns/select_quote.ts
Normal file
40
src/utils/components/dropdowns/select_quote.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { db } from "@/main";
|
||||||
|
|
||||||
|
export async function selectQuote(data: any): Promise<object> {
|
||||||
|
let vote = parseInt(data.data.values[0]);
|
||||||
|
let userID = data.member.user.id;
|
||||||
|
let oldVote = db.bracket.users[userID];
|
||||||
|
|
||||||
|
// Set quote to 0 if it hasn't been voted for yet
|
||||||
|
if (!db.bracket.votes[vote]) {
|
||||||
|
db.bracket.votes[vote] = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
++db.bracket.votes[vote];
|
||||||
|
db.bracket.users[userID] = vote;
|
||||||
|
|
||||||
|
// User changed their vote
|
||||||
|
if (oldVote != null) {
|
||||||
|
|
||||||
|
--db.bracket.votes[oldVote];
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 4,
|
||||||
|
data: {
|
||||||
|
content: `Your vote has been changed from:\n> ${db.bracket.quotes[oldVote]}\nto:\n> ${db.bracket.quotes[vote]}`,
|
||||||
|
flags: 1 << 6,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// User voted for the first time
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
type: 4,
|
||||||
|
data: {
|
||||||
|
content: `Your vote has been recorded for:\n> ${db.bracket.quotes[vote]}`,
|
||||||
|
flags: 1 << 6,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
28
src/utils/quotes.ts
Normal file
28
src/utils/quotes.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
|
import { config } from "../main";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export async function getQuote(count = 1) {
|
||||||
|
let r = await axios.get(
|
||||||
|
config.quote.api_base,
|
||||||
|
{ params: { token: config.quote.token } }
|
||||||
|
);
|
||||||
|
let quoteList = r.data.split(`\n`);
|
||||||
|
let history: string[] = JSON.parse(readFileSync(config.server.quote_history, `utf-8`));
|
||||||
|
|
||||||
|
// Populate the quotes list
|
||||||
|
let quotes: string[] = [];
|
||||||
|
do {
|
||||||
|
let quote = quoteList[Math.floor(Math.random() * quoteList.length)];
|
||||||
|
|
||||||
|
if (!quotes.includes(quote) && !history.includes(quote)) {
|
||||||
|
quotes.push(quote);
|
||||||
|
};
|
||||||
|
} while (quotes.length < count);
|
||||||
|
|
||||||
|
history.push(...quotes)
|
||||||
|
|
||||||
|
writeFileSync(config.server.quote_history, JSON.stringify(history));
|
||||||
|
|
||||||
|
return quotes;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue