From 516b0d9b4e8452f51ca59f2444ea94cf6b73102d Mon Sep 17 00:00:00 2001 From: ani-per_github Date: Thu, 28 Nov 2024 16:55:50 -0500 Subject: [PATCH 1/5] Add `!r` command to auto-react to questions --- src/handlers/newQuestionHandler.ts | 96 +++++++++++++++++++----------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/src/handlers/newQuestionHandler.ts b/src/handlers/newQuestionHandler.ts index d26699a..2cdc9a2 100644 --- a/src/handlers/newQuestionHandler.ts +++ b/src/handlers/newQuestionHandler.ts @@ -6,26 +6,26 @@ import { buildButtonMessage, getCategoryCount, getServerChannels, getTossupParts 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 }); @@ -34,6 +34,26 @@ async function handleThread(message:Message, isBonus: boolean, question:string, } } +async function handleReacts(message:Message, isBonus: boolean) { + if (isBonus) { + try { + // const alphaEmoji = (letter: string) => (String.fromCodePoint(127462 + parseInt(letter, 36) - 10)); + await message.react("<:easy_part:1311811914916954243>"); + await message.react("<:medium_part:1311811928192192543>"); + await message.react("<:hard_part:1311811943727632399>"); + } catch (error) { + console.error('One of the emojis failed to react:', error); + } + } else { + try { + await message.react("<:ten_points:1311811889314926652>"); + await message.react("<:zero_points:1311811903625887786>"); + } catch (error) { + console.error('One of the emojis failed to react:', error); + } + } +} + export default async function handleNewQuestion(message:Message) { const bonusMatch = message.content.match(BONUS_REGEX); const tossupMatch = message.content.match(TOSSUP_REGEX); @@ -44,36 +64,40 @@ export default async function handleNewQuestion(message:Message) { 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 (message.content.includes('!r')) { // If we only want reacts for playtesting, don't need to create a thread and save results + 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); + } } -} \ No newline at end of file +} From c384d757eb9946e2c261e732e64add3c5de544fb Mon Sep 17 00:00:00 2001 From: ani-per_github Date: Thu, 28 Nov 2024 17:24:45 -0500 Subject: [PATCH 2/5] Add Tossup and Bonus emojis for aggregate tracking --- src/handlers/newQuestionHandler.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/handlers/newQuestionHandler.ts b/src/handlers/newQuestionHandler.ts index 2cdc9a2..49ef885 100644 --- a/src/handlers/newQuestionHandler.ts +++ b/src/handlers/newQuestionHandler.ts @@ -38,6 +38,7 @@ async function handleReacts(message:Message, isBonus: boolean) { if (isBonus) { try { // const alphaEmoji = (letter: string) => (String.fromCodePoint(127462 + parseInt(letter, 36) - 10)); + await message.react("<:bonus:1311819077504733274>"); await message.react("<:easy_part:1311811914916954243>"); await message.react("<:medium_part:1311811928192192543>"); await message.react("<:hard_part:1311811943727632399>"); @@ -46,6 +47,7 @@ async function handleReacts(message:Message, isBonus: boolean) { } } else { try { + await message.react("<:tossup:1311819091987791904>"); await message.react("<:ten_points:1311811889314926652>"); await message.react("<:zero_points:1311811903625887786>"); } catch (error) { From a865e95cf1c68d165a5c6f5016c6e1a4935f8e93 Mon Sep 17 00:00:00 2001 From: ani-per_github Date: Fri, 29 Nov 2024 01:46:48 -0500 Subject: [PATCH 3/5] Properly auto-react with application emojis (requires bump to DiscordJS version 14.16) --- package.json | 2 +- src/bot.ts | 6 ++-- src/handlers/newQuestionHandler.ts | 46 +++++++++++++++++------------- src/utils/index.ts | 10 +++---- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index dc67e53..4119894 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/bot.ts b/src/bot.ts index 90b3964..ad95cf2 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -11,7 +11,7 @@ import handleAuthorCommand from './handlers/authorCommandHandler'; const userProgressMap = new Map(); -const client = new Client({ +export const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, @@ -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); @@ -71,4 +71,4 @@ client.on('interactionCreate', async (interaction: Interaction) => { } }); -client.login(config.DISCORD_TOKEN); \ No newline at end of file +client.login(config.DISCORD_TOKEN); diff --git a/src/handlers/newQuestionHandler.ts b/src/handlers/newQuestionHandler.ts index 49ef885..b10a3f8 100644 --- a/src/handlers/newQuestionHandler.ts +++ b/src/handlers/newQuestionHandler.ts @@ -1,7 +1,8 @@ -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) @@ -35,25 +36,30 @@ async function handleThread(message:Message, isBonus: boolean, question:string, } async function handleReacts(message:Message, isBonus: boolean) { - if (isBonus) { - try { - // const alphaEmoji = (letter: string) => (String.fromCodePoint(127462 + parseInt(letter, 36) - 10)); - await message.react("<:bonus:1311819077504733274>"); - await message.react("<:easy_part:1311811914916954243>"); - await message.react("<:medium_part:1311811928192192543>"); - await message.react("<:hard_part:1311811943727632399>"); - } catch (error) { - console.error('One of the emojis failed to react:', error); - } - } else { - try { - await message.react("<:tossup:1311819091987791904>"); - await message.react("<:ten_points:1311811889314926652>"); - await message.react("<:zero_points:1311811903625887786>"); - } catch (error) { - console.error('One of the emojis failed to react:', error); - } - } + 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"]; + } + // 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) { diff --git a/src/utils/index.ts b/src/utils/index.ts index 32be30b..fcd47fb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,6 @@ -import { - ActionRowBuilder, BaseMessageOptions, ButtonBuilder, ButtonStyle, Collection, EmbedBuilder, - Guild, Message, MessageCreateOptions, MessageFlags, TextChannel +import { + ActionRowBuilder, BaseMessageOptions, ButtonBuilder, ButtonStyle, Collection, EmbedBuilder, + Guild, Message, MessageCreateOptions, MessageFlags, TextChannel } from "discord.js"; import Database from 'better-sqlite3'; import { encrypt } from "./crypto"; @@ -265,7 +265,7 @@ export const getToFirstIndicator = (clue:string) => { const thisIndex = words.findIndex(w => w.toLocaleLowerCase() === 'this' || w.toLocaleLowerCase() === 'these'); const defaultSize = 30; - // if "this" or "these" is in the string and isn't the first word, + // if "this" or "these" is in the string and isn't the first word, // truncate after first pronoun: https://github.com/JemCasey/playtesting-bot/issues/8 if (thisIndex > 0) { const endIndex = thisIndex + 2; @@ -281,4 +281,4 @@ export function getCategoryCount(authorId: string, serverId: string | undefined, return (getBonusCategoryCountQuery.get(authorId, serverId, category) as any).category_count as number; else return (getTossupCategoryCountQuery.get(authorId, serverId, category) as any).category_count as number; -} \ No newline at end of file +} From 860454865246a4542a93188e025d48164e75fd12 Mon Sep 17 00:00:00 2001 From: ani-per_github Date: Fri, 29 Nov 2024 01:51:05 -0500 Subject: [PATCH 4/5] Add neg react/emoji --- src/handlers/newQuestionHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/newQuestionHandler.ts b/src/handlers/newQuestionHandler.ts index b10a3f8..e5d1cc7 100644 --- a/src/handlers/newQuestionHandler.ts +++ b/src/handlers/newQuestionHandler.ts @@ -41,7 +41,7 @@ async function handleReacts(message:Message, isBonus: boolean) { if (isBonus) { reacts = ["bonus", "easy_part", "medium_part", "hard_part"]; } else { - reacts = ["tossup", "ten_points", "zero_points"]; + reacts = ["tossup", "ten_points", "zero_points", "neg"]; } // const emojiList = emojis.map((e, x) => `${x} = ${e} | ${e.name}`).join("\n"); // console.log(emojiList); From b88283686f064e348bb26925df931f6bee58f4c7 Mon Sep 17 00:00:00 2001 From: ani-per_github Date: Sat, 30 Nov 2024 17:05:29 -0500 Subject: [PATCH 5/5] Rework framework for bulk playtesting w. reacts by adding separate server channel type; improve results interface by adding emojis; better thread names; auto-add author of question to results thread --- src/handlers/authorCommandHandler.ts | 6 ++-- src/handlers/bonusHandler.ts | 24 ++++++++++----- src/handlers/buttonClickHandler.ts | 4 +-- src/handlers/categoryCommandHandler.ts | 6 ++-- src/handlers/configHandler.ts | 42 ++++++++++++++++++++------ src/handlers/newQuestionHandler.ts | 6 ++-- src/handlers/tossupHandler.ts | 24 +++++++-------- src/init-schema.ts | 5 +-- src/utils/index.ts | 33 +++++++++++++++----- 9 files changed, 102 insertions(+), 48 deletions(-) diff --git a/src/handlers/authorCommandHandler.ts b/src/handlers/authorCommandHandler.ts index 4eda8e1..bcabc80 100644 --- a/src/handlers/authorCommandHandler.ts +++ b/src/handlers/authorCommandHandler.ts @@ -16,7 +16,7 @@ export default async function handleAuthorCommand(message:Message) { 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({ @@ -29,11 +29,11 @@ export default async function handleAuthorCommand(message:Message) { 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}`); } -} \ No newline at end of file +} diff --git a/src/handlers/bonusHandler.ts b/src/handlers/bonusHandler.ts index a18f1d6..3633578 100644 --- a/src/handlers/bonusHandler.ts +++ b/src/handlers/bonusHandler.ts @@ -47,32 +47,40 @@ export default async function handleBonusPlaytest(message: Message, 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); @@ -81,7 +89,7 @@ export default async function handleBonusPlaytest(message: Message, 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)); } } -} \ No newline at end of file +} diff --git a/src/handlers/buttonClickHandler.ts b/src/handlers/buttonClickHandler.ts index b567c58..5fb4786 100644 --- a/src/handlers/buttonClickHandler.ts +++ b/src/handlers/buttonClickHandler.ts @@ -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) { @@ -65,4 +65,4 @@ export default async function handleButtonClick(interaction: Interaction, userPr } } -} \ No newline at end of file +} diff --git a/src/handlers/categoryCommandHandler.ts b/src/handlers/categoryCommandHandler.ts index f9ed2f8..951c3a8 100644 --- a/src/handlers/categoryCommandHandler.ts +++ b/src/handlers/categoryCommandHandler.ts @@ -17,7 +17,7 @@ export default async function handleCategoryCommand(message:Message) { 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({ @@ -30,11 +30,11 @@ export default async function handleCategoryCommand(message:Message) { 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}`); } -} \ No newline at end of file +} diff --git a/src/handlers/configHandler.ts b/src/handlers/configHandler.ts index 51dbbdb..6031f3a 100644 --- a/src/handlers/configHandler.ts +++ b/src/handlers/configHandler.ts @@ -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) { - 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) => 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) => 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."); } -} \ No newline at end of file +} diff --git a/src/handlers/newQuestionHandler.ts b/src/handlers/newQuestionHandler.ts index e5d1cc7..852777e 100644 --- a/src/handlers/newQuestionHandler.ts +++ b/src/handlers/newQuestionHandler.ts @@ -68,11 +68,13 @@ export default async function handleNewQuestion(message:Message) { 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 (message.content.includes('!r')) { // If we only want reacts for playtesting, don't need to create a thread and save results + if (msgChannel.channel_type === 2) { await handleReacts(message, !!bonusMatch); } else { if (bonusMatch) { diff --git a/src/handlers/tossupHandler.ts b/src/handlers/tossupHandler.ts index 5ec8fc3..6be5fbc 100644 --- a/src/handlers/tossupHandler.ts +++ b/src/handlers/tossupHandler.ts @@ -9,7 +9,7 @@ export default async function handleTossupPlaytest(message: Message, cl } else if ((!userProgress.buzzed && !userProgress.grade && message.content.toLowerCase().startsWith('n')) || (userProgress.buzzed && message.content.toLowerCase().startsWith('w'))) { const index = userProgress.index + 1; const guess = message.content.match(/\((.+)\)/); - + setUserProgress(message.author.id, { ...userProgress, buzzed: false, @@ -24,13 +24,13 @@ export default async function handleTossupPlaytest(message: Message, cl if (userProgress.questionParts.length > index) await message.author.send(getSilentMessage(userProgress.questionParts[index])); if (userProgress.questionParts.length - 1 <= index) - await message.author.send(getEmbeddedMessage("You've reached the end of the question. Please buzz by typing `b`/`buzz` or end by typing `e`/`end`", true)); + await message.author.send(getEmbeddedMessage("You've reached the end of the question. Please buzz by typing `b`/`buzz` or end by typing `e`/`end`.", true)); } else if (message.content.toLowerCase().startsWith("b")) { setUserProgress(message.author.id, { ...userProgress, buzzed: true }); - await message.author.send(getEmbeddedMessage("Reveal answer? Type `y`/`yes` to see answer or `w`/`withdraw` to withdraw and continue playing", true)); + await message.author.send(getEmbeddedMessage("Reveal answer? Type `y`/`yes` to see answer or `w`/`withdraw` to withdraw and continue playing.", true)); } else if (message.content.toLowerCase().startsWith("y") && userProgress.buzzed) { setUserProgress(message.author.id, { ...userProgress, @@ -39,7 +39,7 @@ export default async function handleTossupPlaytest(message: Message, cl }); await message.author.send(getSilentMessage(`ANSWER: ${removeSpoilers(userProgress.answer)}`)); - await message.author.send(getEmbeddedMessage("Were you correct? Type `y`/`yes` or `n`/`no`. If you'd like to indicate your answer, you can put it in parenthesis at the end of your message, e.g. `y (foo)`", true)); + await message.author.send(getEmbeddedMessage("Were you correct? Type `y`/`yes` or `n`/`no`. To indicate your answer, you can put it in parentheses at the end of your message - e.g. `y (foo)`.", true)); } else if (message.content.toLowerCase().startsWith('e') || ((message.content.toLowerCase().startsWith('y') || message.content.toLowerCase().startsWith('n')) && userProgress.grade)) { if (message.content.toLowerCase().startsWith('e')) { userProgress.index = userProgress.questionParts.length - 1; @@ -47,7 +47,7 @@ export default async function handleTossupPlaytest(message: Message, cl const key = KeySingleton.getInstance().getKey(message); const note = message.content.match(/\((.+)\)/); - const resultChannel = getServerChannels(userProgress.serverId).find(s => s.channel_id === userProgress.channelId); + const resultChannel = getServerChannels(userProgress.serverId).find(s => (s.channel_id === userProgress.channelId && s.channel_type === 1)); let resultMessage = ''; let buzzIndex = userProgress.index >= userProgress.questionParts.length ? userProgress.questionParts.length - 1 : userProgress.index; let value = message.content.toLowerCase().startsWith('y') ? 10 : (buzzIndex >= userProgress.questionParts.length - 1 ? 0 : -5); @@ -59,10 +59,10 @@ export default async function handleTossupPlaytest(message: Message, cl await message.author.send(getSilentMessage(`ANSWER: ${removeSpoilers(userProgress.answer)}`)); if (message.content.toLowerCase().startsWith('e')) { - resultMessage = `<@${message.author.id}> did not buzz on ||${shortenAnswerline(userProgress.answer)}||` + resultMessage = `⭕ <@${message.author.id}>`; } else { - resultMessage = `<@${message.author.id}> ${value > 0 ? "buzzed correctly" : "buzzed incorrectly"} at "||${userProgress.questionParts[buzzIndex]}||"${note ? `; answer given was "||${sanitizedNote}||"` : ''}` - + (userProgress.guesses?.length > 0 ? `— was thinking ${userProgress.guesses.map(g => `||${g.guess}|| at clue #${g.index + 1}`).join(', ')}`: '') + resultMessage = `${value > 0 ? "✅" : "❌"} <@${message.author.id}> @ "||${userProgress.questionParts[buzzIndex]}||"${note ? `; answer: "||${sanitizedNote}||"` : ''}` + + (userProgress.guesses?.length > 0 ? ` — was thinking \"${userProgress.guesses.map(g => `||${g.guess}|| @ clue #${g.index + 1}`).join(', ')}\"`: ''); } while (countIndex-- > 0) @@ -70,7 +70,7 @@ export default async function handleTossupPlaytest(message: Message, cl saveBuzz(userProgress.serverId, userProgress.questionId, userProgress.authorId, message.author.id, buzzIndex, charactersRevealed, value, sanitizedNote, key); - const threadName = `Buzzes for ${userProgress.authorName}'s tossup "${getToFirstIndicator(userProgress.questionParts[0])}"`; + const threadName = `T | ${userProgress.authorName} | "${getToFirstIndicator(userProgress.questionParts[0])}"`; 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); @@ -79,8 +79,8 @@ export default async function handleTossupPlaytest(message: Message, cl 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)); } else { - await message.author.send(getEmbeddedMessage("Command not recognized", true)); + await message.author.send(getEmbeddedMessage("Command not recognized.", true)); } -} \ No newline at end of file +} diff --git a/src/init-schema.ts b/src/init-schema.ts index c64a44a..19dd6e2 100644 --- a/src/init-schema.ts +++ b/src/init-schema.ts @@ -6,7 +6,8 @@ db.exec(` id INTEGER PRIMARY KEY AUTOINCREMENT, server_id TEXT, channel_id TEXT, - result_channel_id TEXT + result_channel_id TEXT, + channel_type INT ) `); @@ -68,4 +69,4 @@ CREATE TABLE IF NOT EXISTS bonus_direct ( value INT, answer_given TEXT ) -`) \ No newline at end of file +`) diff --git a/src/utils/index.ts b/src/utils/index.ts index fcd47fb..ed39326 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -9,8 +9,8 @@ import { getBonusSummaryData } from "./queries"; const db = new Database('database.db'); -const deleteServerChannelsCommand = db.prepare('DELETE FROM server_channel WHERE server_id = ?'); -const insertServerChannelCommand = db.prepare('INSERT INTO server_channel (server_id, channel_id, result_channel_id) VALUES (?, ?, ?)'); +export const deleteServerChannelsCommand = db.prepare('DELETE FROM server_channel WHERE server_id = ?'); +const insertServerChannelCommand = db.prepare('INSERT INTO server_channel (server_id, channel_id, result_channel_id, channel_type) VALUES (?, ?, ?, ?)'); const getServerChannelsQuery = db.prepare('SELECT * FROM server_channel WHERE server_id = ?'); const insertBuzzCommand = db.prepare('INSERT INTO buzz (server_id, question_id, author_id, user_id, clue_index, characters_revealed, value, answer_given) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'); const insertBonusDirectCommand = db.prepare('INSERT INTO bonus_direct (server_id, question_id, author_id, user_id, part, value, answer_given) VALUES (?, ?, ?, ?, ?, ?, ?)'); @@ -35,6 +35,7 @@ export const formatDecimal = (value: number | null | undefined, fractionDigits:n export enum ServerChannelType { Playtesting = 1, + Reacts = 2, Results } @@ -47,6 +48,7 @@ export type ServerChannel = { server_id: string; channel_id: string; result_channel_id: string; + channel_type: number; } export type QuestionResult = { @@ -141,18 +143,29 @@ export const saveBonusDirect = (serverId: string, questionId: string, authorId: insertBonusDirectCommand.run(serverId, questionId, authorId, userId, part, value, answerGiven ? encrypt(answerGiven, key) : null); } -export const saveServerChannelsFromMessage = (collected: Collection>, server: Guild) => { +export const saveAsyncServerChannelsFromMessage = (collected: Collection>, server: Guild) => { let tags = collected?.first()?.content.split(' ') || []; - deleteServerChannelsCommand.run(server.id); - tags.forEach((tag) => { - const [_, channelId, resultsChannelId] = tag.match(/<#(\d+)>\/<#(\d+)>/) || []; + const [_, channelId, resultsChannelId] = tag.match(/<#(\d+)>\s*\/\s*<#(\d+)>/) || []; const channel = server.channels.cache.find((channel) => channel.id === channelId)?.id; const resultsChannel = server.channels.cache.find((channel) => channel.id === channelId)?.id; if (channel && resultsChannel) { - insertServerChannelCommand.run(server.id, channelId, resultsChannelId); + insertServerChannelCommand.run(server.id, channelId, resultsChannelId, 1); + } + }); +} + +export const saveBulkServerChannelsFromMessage = (collected: Collection>, server: Guild) => { + let tags = collected?.first()?.content.split(' ') || []; + + tags.forEach((tag) => { + const [_, channelId] = tag.match(/<#(\d+)>/) || []; + const channel = server.channels.cache.find((channel) => channel.id === channelId)?.id; + + if (channel) { + insertServerChannelCommand.run(server.id, channelId, "", 2); } }); } @@ -186,6 +199,12 @@ export const getThreadAndUpdateSummary = async (userProgress: UserProgress, thre }); updateThreadId(userProgress.questionId, userProgress.type, thread.id); + try { + await thread.members.add(userProgress.authorId); + } catch (error) { + console.error(`Error adding member to thread: ${error}`); + } + const buttonMessage = await playtestingChannel.messages.fetch(userProgress.buttonMessageId); if (buttonMessage)