Skip to content
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

Emojis in async results threads; allow setting packet name for bulk playtesting; bulk playtesting reacts per-packet indexing; auto-add users to thread based on category roles; fix various other issues #38

Open
wants to merge 57 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
b6499e0
Add emojis for tossup summary (required summary function to become as…
ani-per Dec 10, 2024
2a54877
Improve config directions
ani-per Dec 10, 2024
897e2f3
Merge branch 'main' of ap_gh:ani-per/playtesting-bot
ani-per Dec 10, 2024
2f534a0
Make :bonus_0: default in bulk reacts for bonuses
ani-per Dec 11, 2024
2399e82
Compartmentalize emoji utilities; improve tossup and bonus summaries
ani-per Dec 11, 2024
fa96f07
Add auto-threading and semi-auto-threading for bulk playtesting
ani-per Dec 11, 2024
91ac14e
Prepare for auto-tagging roles in discussion threads; prepare for set…
ani-per Dec 11, 2024
027d9a1
Add category role tagging in threads; improvements to thread titles
ani-per Dec 12, 2024
3f94683
allow setting packet name for bulk playtesting
ani-per Dec 12, 2024
64af368
Add echo channels w. buttons for bulk playtesting; improve config wor…
ani-per Dec 13, 2024
f43614a
Make settings server-wise; add answerlines to question echo; fix ques…
ani-per Dec 14, 2024
742b5a3
Update README; add reaction emoji zip file
ani-per Dec 15, 2024
1848739
Add auto-thread command in question examples
ani-per Dec 15, 2024
b8953a3
Upgrade to higher version of `better-sqlite3` for Node 23; add `ts-no…
ani-per Dec 15, 2024
1b52b2a
Add formatting for example tossups/bonuses
ani-per Dec 15, 2024
3b5af26
Merge branch 'main' of ap_gh:ani-per/playtesting-bot
ani-per Dec 15, 2024
08114f3
Add example images
ani-per Dec 15, 2024
1a0bb85
Update README
ani-per Dec 16, 2024
04b4f07
Fix paster dingus wording in README
ani-per Dec 16, 2024
8c0908b
Add privileged members intent to client and README
ani-per Dec 16, 2024
041b13d
Replace ineffective tags in threads with loop-adding users with detec…
ani-per Dec 16, 2024
11f3829
improve thread names; fix auto-adding users to threads based on roles
ani-per Dec 16, 2024
27094bb
Update README
ani-per Dec 17, 2024
b7aea95
Distinguish character limits for async and bulk playtesting
ani-per Dec 17, 2024
0bf93d3
Collection of threadding fixes (auto-add, naming)
ani-per Dec 17, 2024
1f64f13
Improve auto-adding
ani-per Dec 17, 2024
e8c7430
Replace local server setting with new server setting DB table
ani-per Dec 18, 2024
5c309bf
Use only first name in author name in thread
ani-per Dec 18, 2024
a9cacb6
Add bulk react tally function, recognize powers; remove echo setting
ani-per Dec 20, 2024
780ee89
Minor update for README
ani-per Dec 20, 2024
ebf3e96
Fix tally play count issue due to improper fetching/caching
ani-per Dec 21, 2024
e3dbf69
Minor fixes
ani-per Dec 23, 2024
cbee242
Fix power handling
ani-per Dec 25, 2024
59b5c1a
Update permissions integer in invite URL in README
ani-per Dec 25, 2024
56799b3
Update example images
ani-per Dec 25, 2024
0f3c917
Clarify Node version in README
ani-per Dec 25, 2024
805c29d
Add configuration example image
ani-per Dec 26, 2024
e8eb6ef
Update examples
ani-per Dec 26, 2024
dfe8b6a
Minor fixes
ani-per Dec 26, 2024
646fcc2
Fix failure to print guesses when ending questions (issue #22 from or…
ani-per Dec 26, 2024
dcac787
Minor changes to tossup summary structure
ani-per Dec 26, 2024
1c7fdbd
Minor changes to README
ani-per Dec 26, 2024
f9e28f8
Update async tossup results example image to reflect new tossup summa…
ani-per Dec 26, 2024
6e2be47
Fix config logic
ani-per Dec 26, 2024
e4ac494
Fix token instructions in README
ani-per Dec 27, 2024
e07e6cb
Add undo command; use question author tag name in async results threa…
ani-per Dec 27, 2024
899ebb4
Implement undo in bonus reading
ani-per Dec 27, 2024
7accc4c
Update config example image
ani-per Dec 27, 2024
e0237dd
Merge branch 'main' of ap_gh:ani-per/playtesting-bot
ani-per Dec 27, 2024
2ef3d33
Update async tossup results image
ani-per Dec 27, 2024
18b23b1
Revamp button creation to fix #18 by adding "Create Discussion Thread…
ani-per Dec 27, 2024
7bdbff6
Fix #39; add ellipsis in more thread titles
ani-per Dec 30, 2024
a2324cf
Make bot direction language consistent; fix potential bug in Create T…
ani-per Dec 30, 2024
17081d7
Add troubleshooting section to README
ani-per Dec 30, 2024
2d4252b
Arrange points reacts on tossups to be in decreasing order
ani-per Dec 30, 2024
2a37ba7
Catch deleted messages error in handling for bulk playtesting
ani-per Jan 8, 2025
c0df119
Add discussion thread link in internal playtesting DMs; fix spacing i…
ani-per Jan 24, 2025
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
259 changes: 201 additions & 58 deletions README.md

Large diffs are not rendered by default.

Binary file added examples/async-question.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/async-results-bonus.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/async-results-tossup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/bulk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/config.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/echo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"author": "",
"license": "ISC",
"dependencies": {
"better-sqlite3": "^8.4.0",
"crypto": "^1.0.1",
"discord.js": "14.16.0",
"better-sqlite3": "^11.7.0",
"discord.js": "^14.16.3",
"ts-node": "^10.9.2",
"dotenv": "^16.3.1",
"radash": "^11.0.0"
},
Expand Down
Binary file added react_emojis.zip
Binary file not shown.
121 changes: 102 additions & 19 deletions src/bot.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,135 @@
import { Client, GatewayIntentBits, Partials, ChannelType, Interaction } from 'discord.js';
import { Client, GatewayIntentBits, Partials, ChannelType, Interaction, TextChannel } from "discord.js";
import { config } from "./config";
import handleTossupPlaytest from './handlers/tossupHandler';
import handleBonusPlaytest from './handlers/bonusHandler';
import handleNewQuestion from './handlers/newQuestionHandler';
import handleConfig from './handlers/configHandler';
import handleButtonClick from './handlers/buttonClickHandler';
import handleCategoryCommand from './handlers/categoryCommandHandler';
import { QuestionType, UserBonusProgress, UserProgress, UserTossupProgress } from './utils';
import handleAuthorCommand from './handlers/authorCommandHandler';
import handleTossupPlaytest from "./handlers/tossupHandler";
import handleBonusPlaytest from "./handlers/bonusHandler";
import handleNewQuestion from "./handlers/newQuestionHandler";
import handleConfig from "./handlers/configHandler";
import handleButtonClick from "./handlers/buttonClickHandler";
import handleCategoryCommand from "./handlers/categoryCommandHandler";
import handleTally from "./handlers/bulkQuestionHandler";
import { QuestionType, UserBonusProgress, UserProgress, UserTossupProgress, getBulkQuestions, getBulkQuestionsInPacket, getServerChannels, getServerSettings, updatePacketName } from "./utils";
import handleAuthorCommand from "./handlers/authorCommandHandler";

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

const packetCommands = ["packet", "round", "read", "end"];
const tallyCommands = ["tally", "count", "end"];

export const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages
GatewayIntentBits.DirectMessages,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessageReactions
],
partials: [
Partials.Channel,
Partials.Message
Partials.Message,
Partials.Reaction
],
allowedMentions: {
parse: []
}
});

client.once('ready', () => {
client.once("ready", () => {
console.log(`Logged in as ${client.user?.tag}`);
});

client.on('messageCreate', async (message) => {
client.on("messageCreate", async (message) => {
try {
if (message.author.id === config.DISCORD_APPLICATION_ID)
return;

if (message.content === '!config') {
if (message.content.startsWith("!config")) {
await handleConfig(message);
} else if (message.content === '!category') {
} else if ([...packetCommands, ...tallyCommands].some(v => message.content.startsWith("!" + v))) {
let currentServerSetting = getServerSettings(message.guild!.id).find(ss => ss.server_id == message.guild!.id);
let currentPacket = currentServerSetting?.packet_name;
if (tallyCommands.some(v => message.content.startsWith("!" + v))) {
let currentServerSetting = getServerSettings(message.guild!.id).find(ss => ss.server_id == message.guild!.id);
let currentPacket = currentServerSetting?.packet_name;
let splits = message.content.split(" ");
if (splits.length > 1) {
let desiredPacketCommand = splits.slice(-1)[0];
if (desiredPacketCommand) {
if (desiredPacketCommand.includes("all")) {
[...new Set(getBulkQuestions(message.guild!.id).map(u => u.packet_name))].forEach(async packet => {
let desiredPacketBulkQuestions = getBulkQuestionsInPacket(message.guild!.id, packet);
if (desiredPacketBulkQuestions) {
await handleTally(message.guild!.id, packet, message);
} else {
message.reply(`No questions in packet ${packet} yet.`);
}
});
} else {
let desiredPacket = desiredPacketCommand.trim().substring(0, 2);
let desiredPacketBulkQuestions = getBulkQuestionsInPacket(message.guild!.id, desiredPacket);
if (desiredPacketBulkQuestions) {
await handleTally(message.guild!.id, desiredPacket, message);
} else {
message.reply(`No questions in packet ${desiredPacket} yet.`);
}
}
} else {
message.reply("Invalid packet name.");
}
} else {
if (currentPacket) {
await handleTally(message.guild!.id, currentPacket, message);
} else {
message.reply("Packet not configured yet.");
}
}
}
if (packetCommands.some(v => message.content.startsWith("!" + v))) {
let splits = message.content.split(" ");
let endPacket = message.content.startsWith("!end");
if (splits.length > 1 || endPacket) {
let desiredPacketCommand = splits.slice(-1)[0];
if (desiredPacketCommand || endPacket) {
if (desiredPacketCommand.includes("reset") || desiredPacketCommand.includes("clear") || endPacket) {
updatePacketName(message.guild!.id, "");
message.reply(`Packet ${endPacket ? "finished" : "cleared"}.`);
} else {
let desiredPacket = desiredPacketCommand.trim().substring(0, 2);
let newPacketName = updatePacketName(message.guild!.id, desiredPacket);
message.reply(`Now reading packet ${newPacketName}.`);
const echoChannelId = getServerChannels(message.guild!.id).find(c => (c.channel_type === 3))?.channel_id;
if (echoChannelId) {
const echoChannel = (client.channels.cache.get(echoChannelId) as TextChannel);
echoChannel.send(`# Packet ${newPacketName}`);
}
}
} else {
if (message.content.startsWith("!packet") || message.content.startsWith("!round")) {
if (currentPacket) {
message.reply(`The current packet is ${currentPacket}.`);
} else {
message.reply("Packet not configured yet.");
}
}
}
} else {
if (message.content.startsWith("!packet") || message.content.startsWith("!round")) {
if (currentPacket) {
message.reply(`The current packet is ${currentPacket}.`);
} else {
message.reply("Packet not configured yet.");
}
}
}
}
} else if (message.content.startsWith("!category")) {
await handleCategoryCommand(message);
} else if (message.content === '!author') {
} else if (message.content.startsWith("!author")) {
await handleAuthorCommand(message);
} else {
let setUserProgress = userProgressMap.set.bind(userProgressMap);
let deleteUserProgress = userProgressMap.delete.bind(userProgressMap);

if (message.channel.type !== ChannelType.DM && message.content.includes('ANSWER:')) {
if (message.channel.type !== ChannelType.DM && message.content.includes("ANSWER:")) {
await handleNewQuestion(message);
} else if (message.channel.type === ChannelType.DM) {
let userProgress = userProgressMap.get(message.author.id)
Expand All @@ -63,7 +146,7 @@ client.on('messageCreate', async (message) => {
}
});

client.on('interactionCreate', async (interaction: Interaction) => {
client.on("interactionCreate", async (interaction: Interaction) => {
try {
await handleButtonClick(interaction, userProgressMap, userProgressMap.set.bind(userProgressMap));
} catch (e) {
Expand Down
5 changes: 4 additions & 1 deletion src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ export const BONUS_DIFFICULTY_REGEX = /.*\[10(?:\|\|)?(\w{1})(?:\|\|)?\].*/;
export const SECRET_ROLE = "secret";

export const CATEGORY = 'category';
export const AUTHOR = 'author';
export const AUTHOR = 'author';

export const asyncCharLimit = 60;
export const bulkCharLimit = 35;
98 changes: 59 additions & 39 deletions src/handlers/bonusHandler.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,67 @@
import { Client, Message, TextChannel } from "discord.js";
import { asyncCharLimit } from "src/constants";
import KeySingleton from "src/services/keySingleton";
import { UserBonusProgress, getEmbeddedMessage, getServerChannels, getSilentMessage, getThreadAndUpdateSummary, getToFirstIndicator, removeBonusValue, removeSpoilers, saveBonusDirect, shortenAnswerline } from "src/utils";
import { UserBonusProgress, getEmbeddedMessage, getServerChannels, getSilentMessage, getResultsThreadAndUpdateSummary, getToFirstIndicator, removeQuestionNumber, removeBonusValue, removeSpoilers, saveBonusDirect, shortenAnswerline, cleanThreadName, stripFormatting } from "src/utils";
import { getEmojiList } from "src/utils/emojis";

export default async function handleBonusPlaytest(message: Message<boolean>, client: Client<boolean>, userProgress: UserBonusProgress, setUserProgress: (key: any, value: any) => void, deleteUserProgress: (key: any) => void) {
let validGradingResponse = userProgress.grade && (message.content.toLowerCase().startsWith('y') || message.content.toLowerCase().startsWith('n'));
let validGradingResponse = userProgress.grade && (message.content.toLowerCase().startsWith("y") || message.content.toLowerCase().startsWith("n"));

if (message.content.toLowerCase().startsWith('x')) {
if (message.content.toLowerCase().startsWith("x")) {
deleteUserProgress(message.author.id);
await message.author.send(getEmbeddedMessage("Ended bonus reading.", true));

return;
}

if (!userProgress.grade && (message.content.toLowerCase().startsWith('d') || message.content.toLowerCase().startsWith('p'))) {
if (message.content.toLowerCase().startsWith("u") && userProgress.index < userProgress.parts.length) {
let index: number;
if (userProgress.grade) {
index = userProgress.index;
} else {
index = userProgress.index - 1;
}
if (userProgress.grade || userProgress.index > 0) {
setUserProgress(message.author.id, {
...userProgress,
grade: false,
index,
results: userProgress.results.filter(n => n.note.index !== index)
});

let partToShow = removeBonusValue(removeSpoilers(userProgress.parts[index] || ""));
if (index === 0) {
partToShow = removeBonusValue(removeSpoilers(userProgress.leadin || "")) + "\n" + partToShow;
}
await message.author.send(getSilentMessage(partToShow));
} else {
await message.author.send(getEmbeddedMessage("You can't go back; this was the first bonus part.", true));
}
}

if (!userProgress.grade && (message.content.toLowerCase().startsWith("d") || message.content.toLowerCase().startsWith("p"))) {
await message.author.send(getSilentMessage(`ANSWER: ${removeSpoilers(userProgress.answers![userProgress.index])}`));
}

if (!userProgress.grade && message.content.toLowerCase().startsWith('d')) {
if (!userProgress.grade && message.content.toLowerCase().startsWith("d")) {
setUserProgress(message.author.id, {
...userProgress,
grade: true
});

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)`. To undo your direct, type `u`/`undo`.", true));
}

if (validGradingResponse || (!userProgress.grade && message.content.toLowerCase().startsWith('p'))) {
if (validGradingResponse || (!userProgress.grade && message.content.toLowerCase().startsWith("p"))) {
const index = userProgress.index + 1;
const note = message.content.match(/\((.+)\)/);
const results = [
...userProgress.results, {
points: message.content.toLowerCase().startsWith('y') ? 10 : 0,
passed: message.content.toLowerCase().startsWith('p'),
note: note ? note[1] : null
points: message.content.toLowerCase().startsWith("y") ? 10 : 0,
passed: message.content.toLowerCase().startsWith("p"),
note: note ? {text: note[1], index: userProgress.index} : {text: null, index: userProgress.index}
}
];
const index = userProgress.index + 1;

setUserProgress(message.author.id, {
...userProgress,
Expand All @@ -43,20 +70,20 @@ export default async function handleBonusPlaytest(message: Message<boolean>, cli
results
});

if (userProgress.parts.length > index) {
await message.author.send(getSilentMessage(removeBonusValue(removeSpoilers(userProgress.parts[index] || ''))));
if (index < userProgress.parts.length) {
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 && s.channel_type === 1));
let resultMessage = ``;
let resultMessage = "";
let points_emoji_names: string[] = [];
let emoji_summary: string[] = [];
let partMessages: string[] = [];
let totalPoints = 0;
let answer_emoji = (await getEmojiList(["answer"]))[0] || "answer:";

results.forEach(async function(r: any, i: number) {
results.forEach(async function (r: any, i: number) {
let answer = shortenAnswerline(userProgress.answers[i]);
let partMessage = '';
let partMessage = "";
var points_emoji_name = "";

if (r.points > 0) {
Expand All @@ -73,40 +100,33 @@ export default async function handleBonusPlaytest(message: Message<boolean>, cli
points_emoji_name += userProgress.difficulties[i]?.toUpperCase()

points_emoji_names.push(points_emoji_name);
partMessage += (r.note ? ` (answer: "||${r.note}||")` : '');
partMessage += (r.note?.text ? ` (${answer_emoji} "||${r.note.text}||")` : "");
partMessages.push(partMessage);
saveBonusDirect(userProgress.serverId, userProgress.questionId, userProgress.authorId, message.author.id, i + 1, r.points, r.note, key);
saveBonusDirect(userProgress.serverId, userProgress.questionId, userProgress.posterId, message.author.id, i + 1, r.points, r.note?.text || null, key);
});

await client.application?.emojis.fetch().then(function(emojis) {
try {
points_emoji_names.forEach(function(points_emoji_name) {
// console.log(`Searching for emoji: ${points_emoji_name}`);
var points_emoji = emojis.find(emoji => emoji.name === points_emoji_name);
// console.log(`Found emoji: ${points_emoji}`);
if (points_emoji) {
emoji_summary.push(`${points_emoji}`);
}
});
} catch (error) {
console.error("One or more of the bonus points emojis failed to fetch:", error);
}
});
let emoji_summary = await getEmojiList(points_emoji_names);

resultMessage += emoji_summary.join(' ');
resultMessage += ` <@${message.author.id}> scored ${totalPoints} points: `;
resultMessage += partMessages.join(', ');
resultMessage += emoji_summary.join(" ");
resultMessage += ` ${totalPoints} <@${message.author.id}> `;
resultMessage += partMessages.join(", ");

const threadName = `B | ${userProgress.authorName} | "${getToFirstIndicator(userProgress.leadin)}"`;
const fallbackName = cleanThreadName(getToFirstIndicator(stripFormatting(removeQuestionNumber(userProgress.leadin)), asyncCharLimit));
const threadName = `B | ${userProgress?.authorName || userProgress?.posterName || ""} | ${fallbackName}`;
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);
const thread = await getResultsThreadAndUpdateSummary(userProgress, threadName.replaceAll(/\s\s+/g, " ").trim().slice(0, 100), resultsChannel, playtestingChannel);

await thread.send(resultMessage);

deleteUserProgress(message.author.id);

await message.author.send(getEmbeddedMessage(`Your result has been sent to <#${thread.id}>.`, true));
await message.author.send(getEmbeddedMessage(`Your result has been sent to this thread: <#${thread.id}>.`, true));

const questionMessage = await playtestingChannel.messages.fetch(userProgress.questionId);
if (questionMessage.hasThread) {
await message.author.send(getEmbeddedMessage(`See the discussion thread: <#${questionMessage?.thread?.id}>.`, true));
}
}
}
}
Loading