Skip to content

Add !r command to auto-react to questions #34

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"dependencies": {
"better-sqlite3": "^8.4.0",
"crypto": "^1.0.1",
"discord.js": "14.11.0",
"discord.js": "14.16.0",
"dotenv": "^16.3.1",
"radash": "^11.0.0"
},
Expand Down
6 changes: 3 additions & 3 deletions src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import handleAuthorCommand from './handlers/authorCommandHandler';

const userProgressMap = new Map<string, UserProgress>();

const client = new Client({
export const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
Expand Down Expand Up @@ -41,7 +41,7 @@ client.on('messageCreate', async (message) => {
} else if (message.content === '!category') {
await handleCategoryCommand(message);
} else if (message.content === '!author') {
await handleAuthorCommand(message);
await handleAuthorCommand(message);
} else {
let setUserProgress = userProgressMap.set.bind(userProgressMap);
let deleteUserProgress = userProgressMap.delete.bind(userProgressMap);
Expand Down Expand Up @@ -71,4 +71,4 @@ client.on('interactionCreate', async (interaction: Interaction) => {
}
});

client.login(config.DISCORD_TOKEN);
client.login(config.DISCORD_TOKEN);
6 changes: 3 additions & 3 deletions src/handlers/authorCommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default async function handleAuthorCommand(message:Message<boolean>) {
author_id: d.author_id
}));
const tossupTable = getTable(
[ 'Total', 'Total Plays', 'Conv. %', 'Neg %', 'Avg. Buzz', 'First Buzz', AUTHOR ],
[ 'Total', 'Total Plays', 'Conv. %', 'Neg %', 'Avg. Buzz', 'First Buzz', AUTHOR ],
categoryData
);
const bonusAuthorData = getBonusAuthorData(message.guildId!).map(d => Object.values({
Expand All @@ -29,11 +29,11 @@ export default async function handleAuthorCommand(message:Message<boolean>) {
author_id: d.author_id
}));
const bonusTable = getTable(
[ 'Total', 'Total Plays', 'PPB', 'E%', 'M%', 'H%', AUTHOR ],
[ 'Total', 'Total Plays', 'PPB', 'E%', 'M%', 'H%', AUTHOR ],
bonusAuthorData
);

await message.reply(`## Tossups\n${tossupTable}`);
await message.reply(`## Bonuses\n${bonusTable}`);
}
}
}
24 changes: 16 additions & 8 deletions src/handlers/bonusHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,32 +47,40 @@ export default async function handleBonusPlaytest(message: Message<boolean>, cli
await message.author.send(getSilentMessage(removeBonusValue(removeSpoilers(userProgress.parts[index] || ''))));
} else {
const key = KeySingleton.getInstance().getKey(message);
const resultChannel = getServerChannels(userProgress.serverId).find(s => s.channel_id === userProgress.channelId);
let resultMessage = `<@${message.author.id}> `;
const resultChannel = getServerChannels(userProgress.serverId).find(s => (s.channel_id === userProgress.channelId && s.channel_type === 1));
let resultMessage = ``;
let prepartMessages: string[] = [];
let partMessages: string[] = [];
let totalPoints = 0;

results.forEach((r: any, i: number) => {
let answer = shortenAnswerline(userProgress.answers[i]);
let prepartMessage = '';
let partMessage = '';

if (r.points > 0) {
totalPoints += r.points;
prepartMessage += "✅";
partMessage += `got ||${answer}||`;
} else if (!r.passed) {
prepartMessage += "❌";
partMessage += `missed ||${answer}||`;
} else {
prepartMessage += "⭕";
partMessage += `passed ||${answer}||`;
}

partMessage += (r.note ? ` (answer given: "||${r.note}||")` : '')

prepartMessages.push(prepartMessage);
partMessage += (r.note ? ` (answer given: "||${r.note}||")` : '');
partMessages.push(partMessage);
saveBonusDirect(userProgress.serverId, userProgress.questionId, userProgress.authorId, message.author.id, i + 1, r.points, r.note, key);
});

resultMessage += partMessages.join(', ') + ` for a total of ${totalPoints} points`;
resultMessage += prepartMessages.join(' ');
resultMessage += ` <@${message.author.id}> scored ${totalPoints} points: `;
resultMessage += partMessages.join(', ');

const threadName = `Results for ${userProgress.authorName}'s bonus "${getToFirstIndicator(userProgress.leadin)}"`;
const threadName = `B | ${userProgress.authorName} | "${getToFirstIndicator(userProgress.leadin)}"`;
const resultsChannel = client.channels.cache.get(resultChannel!.result_channel_id) as TextChannel;
const playtestingChannel = client.channels.cache.get(userProgress.channelId) as TextChannel;
const thread = await getThreadAndUpdateSummary(userProgress, threadName.slice(0, 100), resultsChannel, playtestingChannel);
Expand All @@ -81,7 +89,7 @@ export default async function handleBonusPlaytest(message: Message<boolean>, cli

deleteUserProgress(message.author.id);

await message.author.send(getEmbeddedMessage(`Thanks, your result has been sent to <#${thread.id}>`, true));
await message.author.send(getEmbeddedMessage(`Your result has been sent to <#${thread.id}>.`, true));
}
}
}
}
4 changes: 2 additions & 2 deletions src/handlers/buttonClickHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default async function handleButtonClick(interaction: Interaction, userPr
const bonusMatch = questionMessage.content.match(BONUS_REGEX);
const tossupMatch = questionMessage.content.match(TOSSUP_REGEX);
const authorName = questionMessage.member?.displayName ?? questionMessage.author.username;

if (userProgress.get(interaction.user.id)) {
await interaction.user.send(getEmbeddedMessage("You tried to start playtesting a question but have a different question reading in progress. Please complete that reading or type `x` to end it, then try again."));
} else if (bonusMatch) {
Expand Down Expand Up @@ -65,4 +65,4 @@ export default async function handleButtonClick(interaction: Interaction, userPr
}
}

}
}
6 changes: 3 additions & 3 deletions src/handlers/categoryCommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default async function handleCategoryCommand(message:Message<boolean>) {
category: d.category
}));
const tossupTable = getTable(
[ 'Total', 'Total Plays', 'Conv. %', 'Neg %', 'Avg. Buzz', 'First Buzz', CATEGORY ],
[ 'Total', 'Total Plays', 'Conv. %', 'Neg %', 'Avg. Buzz', 'First Buzz', CATEGORY ],
categoryData
);
const bonusCategoryData = getBonusCategoryData(message.guildId!).map(d => Object.values({
Expand All @@ -30,11 +30,11 @@ export default async function handleCategoryCommand(message:Message<boolean>) {
category: d.category
}));
const bonusTable = getTable(
[ 'Total', 'Total Plays', 'PPB', 'E%', 'M%', 'H%', CATEGORY],
[ 'Total', 'Total Plays', 'PPB', 'E%', 'M%', 'H%', CATEGORY],
bonusCategoryData
);

await message.reply(`## Tossups\n${tossupTable}`);
await message.reply(`## Bonuses\n${bonusTable}`);
}
}
}
42 changes: 33 additions & 9 deletions src/handlers/configHandler.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
import { Message } from "discord.js";
import { Message, TextChannel } from "discord.js";
import { SECRET_ROLE } from "src/constants";
import { saveServerChannelsFromMessage } from "src/utils";
import { saveAsyncServerChannelsFromMessage, saveBulkServerChannelsFromMessage, deleteServerChannelsCommand } from "src/utils";

export default async function handleConfig(message:Message<boolean>) {
await message.channel.send('Please list any channels that will be used for playtesting in a message of the format `#playtesting-channel / #playtesting-results-channel #playtesting-channel-2 / #playtesting-results-channel-2`. NOTE: multiple playtesting channels can share a playtesting-results-channel');
const msgChannel = ( await message.channel.fetch() as TextChannel );

await msgChannel.send('First, configure the channels used for internal, asynchronous playtesting - where the results should be saved to a separate channel.');
await msgChannel.send('List these channels in the form: `#playtesting-channel/#playtesting-results-channel #playtesting-channel-2/#playtesting-results-channel-2`.');
await msgChannel.send('Make sure to add exactly one space between each set of playtesting and results channels. Note: Multiple playtesting channels can share a `playtesting-results-channel`.');

try {
let filter = (m: Message<boolean>) => m.author.id === message.author.id
let collected = await message.channel.awaitMessages({
let collected = await msgChannel.awaitMessages({
filter,
max: 1
});

saveServerChannelsFromMessage(collected, message.guild!);
deleteServerChannelsCommand.run(message.guild!.id);

saveAsyncServerChannelsFromMessage(collected, message.guild!);

await msgChannel.send('Configuration saved successfully.');
await msgChannel.send(`If you would like question answers and player notes to be encrypted in the bot's database, please create a role called \`${SECRET_ROLE}\`.`);

await msgChannel.send('Now, list the channels used for bulk playtesting - where playtesters will use reactions to indicate their results.');
await msgChannel.send('List these channels in the form: `#playtesting-channel #playtesting-channel-2`.');
await msgChannel.send('Do not repeat any channels from asynchronous playtesting. Make sure to add exactly one space between set of playtesting channels.');

try {
let filter = (m: Message<boolean>) => m.author.id === message.author.id
let collected = await msgChannel.awaitMessages({
filter,
max: 1
});

saveBulkServerChannelsFromMessage(collected, message.guild!);

await message.channel.send('Configuration saved successfully.');
await message.channel.send(`If you would like question answers and player notes to be encrypted in the bot's database, please create a role called \`${SECRET_ROLE}\`.`);
await msgChannel.send('Configuration saved successfully.');
} catch {
await msgChannel.send("An error occurred, please try again.");
}
} catch {
await message.channel.send("An error occurred, please try again");
await msgChannel.send("An error occurred, please try again.");
}
}
}
110 changes: 72 additions & 38 deletions src/handlers/newQuestionHandler.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
import { Message } from "discord.js";
import { Message, Application } from "discord.js";
import { BONUS_DIFFICULTY_REGEX, BONUS_REGEX, TOSSUP_REGEX } from "src/constants";
import KeySingleton from "src/services/keySingleton";
import { buildButtonMessage, getCategoryCount, getServerChannels, getTossupParts, removeSpoilers, saveBonus, saveTossup, shortenAnswerline } from "src/utils";
import { client } from "src/bot";

const extractCategory = (metadata:string | undefined) => {
if (!metadata)
return "";

metadata = removeSpoilers(metadata);
let results = metadata.match(/([A-Z]{2,3}), (.*)/);

if (results)
return results[2].trim();

results = metadata.match(/(.*), ([A-Z]{2,3})/);

if (results)
return results[1].trim();

return "";
}

async function handleThread(message:Message, isBonus: boolean, question:string, metadata:string) {
if (message.content.includes('!t')) {
const thread = await message.startThread({
name: metadata ?
`${removeSpoilers(metadata)} - ${isBonus ? "Bonus" : "Tossup"} ${getCategoryCount(message.author.id, message.guild?.id, extractCategory(metadata), isBonus)}`
name: metadata ?
`${removeSpoilers(metadata)} - ${isBonus ? "Bonus" : "Tossup"} ${getCategoryCount(message.author.id, message.guild?.id, extractCategory(metadata), isBonus)}`
: `"${question.substring(0, 30)}..."`,
autoArchiveDuration: 60
});
Expand All @@ -34,46 +35,79 @@ async function handleThread(message:Message, isBonus: boolean, question:string,
}
}

async function handleReacts(message:Message, isBonus: boolean) {
client.application?.emojis.fetch().then(function(emojis) {
var reacts;
if (isBonus) {
reacts = ["bonus", "easy_part", "medium_part", "hard_part"];
} else {
reacts = ["tossup", "ten_points", "zero_points", "neg"];
}
// const emojiList = emojis.map((e, x) => `${x} = ${e} | ${e.name}`).join("\n");
// console.log(emojiList);
try {
reacts.forEach(function(react) {
// console.log(`Searching for react: ${react}`);
var react_emoji = emojis.find(emoji => emoji.name === react);
// console.log(`Found emoji: ${react_emoji}`);
if (react_emoji) {
message.react(react_emoji?.id);
// console.log(`Reacted with ${react_emoji.id}`);
}
});
} catch (error) {
console.error("One of the emojis failed to react:", error);
}
});

}

export default async function handleNewQuestion(message:Message<boolean>) {
const bonusMatch = message.content.match(BONUS_REGEX);
const tossupMatch = message.content.match(TOSSUP_REGEX);
const playtestingChannels = getServerChannels(message.guild!.id);
const key = KeySingleton.getInstance().getKey(message);

if (playtestingChannels.find(c => c.channel_id === message.channel.id) && (bonusMatch || tossupMatch)) {
const msgChannel = playtestingChannels.find(c => c.channel_id === message.channel.id);

if (msgChannel && (bonusMatch || tossupMatch)) {
let threadQuestionText = '';
let threadMetadata = '';

if (bonusMatch) {
const [_, __, part1, answer1, part2, answer2, part3, answer3, metadata, difficultyPart1, difficultyPart2, difficultyPart3] = bonusMatch;
const difficulty1Match = part1.match(BONUS_DIFFICULTY_REGEX) || [];
const difficulty2Match = part2.match(BONUS_DIFFICULTY_REGEX) || [];
const difficulty3Match = part3.match(BONUS_DIFFICULTY_REGEX) || [];
threadQuestionText = part1;
threadMetadata = metadata;

saveBonus(message.id, message.guildId!, message.author.id, extractCategory(metadata), [
{ part: 1, answer: shortenAnswerline(answer1), difficulty: difficultyPart1 || difficulty1Match[1] || null},
{ part: 2, answer: shortenAnswerline(answer2), difficulty: difficultyPart2 || difficulty2Match[1] || null},
{ part: 3, answer: shortenAnswerline(answer3), difficulty: difficultyPart3 || difficulty3Match[1] || null}
], key);
} else if (tossupMatch) {
const [_, question, answer, metadata] = tossupMatch;
const tossupParts = getTossupParts(question);
const questionLength = tossupParts.reduce((a, b) => {
return a + b.length;
}, 0);
threadQuestionText = question;
threadMetadata = metadata;

// if a tossup was sent that has 2 or fewer spoiler tagged sections, assume that it's not meant to be played
if (tossupParts.length <= 2)
return;

saveTossup(message.id, message.guildId!, message.author.id, questionLength, extractCategory(metadata), shortenAnswerline(answer), key);
}
if (msgChannel.channel_type === 2) {
await handleReacts(message, !!bonusMatch);
} else {
if (bonusMatch) {
const [_, __, part1, answer1, part2, answer2, part3, answer3, metadata, difficultyPart1, difficultyPart2, difficultyPart3] = bonusMatch;
const difficulty1Match = part1.match(BONUS_DIFFICULTY_REGEX) || [];
const difficulty2Match = part2.match(BONUS_DIFFICULTY_REGEX) || [];
const difficulty3Match = part3.match(BONUS_DIFFICULTY_REGEX) || [];
threadQuestionText = part1;
threadMetadata = metadata;

saveBonus(message.id, message.guildId!, message.author.id, extractCategory(metadata), [
{ part: 1, answer: shortenAnswerline(answer1), difficulty: difficultyPart1 || difficulty1Match[1] || null},
{ part: 2, answer: shortenAnswerline(answer2), difficulty: difficultyPart2 || difficulty2Match[1] || null},
{ part: 3, answer: shortenAnswerline(answer3), difficulty: difficultyPart3 || difficulty3Match[1] || null}
], key);
} else if (tossupMatch) {
const [_, question, answer, metadata] = tossupMatch;
const tossupParts = getTossupParts(question);
const questionLength = tossupParts.reduce((a, b) => {
return a + b.length;
}, 0);
threadQuestionText = question;
threadMetadata = metadata;

// if a tossup was sent that has 2 or fewer spoiler tagged sections, assume that it's not meant to be played
if (tossupParts.length <= 2)
return;

await message.reply(buildButtonMessage(!!bonusMatch));
await handleThread(message, !!bonusMatch, threadQuestionText, threadMetadata);
saveTossup(message.id, message.guildId!, message.author.id, questionLength, extractCategory(metadata), shortenAnswerline(answer), key);
}

await message.reply(buildButtonMessage(!!bonusMatch));
await handleThread(message, !!bonusMatch, threadQuestionText, threadMetadata);
}
}
}
}
Loading