Added Counting Feature

This commit is contained in:
Ahmad 2025-03-02 05:47:53 -05:00
parent de599534f0
commit 95143d8c93
No known key found for this signature in database
GPG key ID: 8FD8A93530D182BF
7 changed files with 343 additions and 3 deletions

View file

@ -6,7 +6,8 @@
"redisConnectionString": "REDIS_CONNECTION_STRING",
"channels": {
"welcome": "WELCOME_CHANNEL_ID",
"logs": "LOG_CHAANNEL_ID"
"logs": "LOG_CHANNEL_ID",
"counting": "COUNTING_CHANNEL_ID"
},
"roles": {
"joinRoles": [

View file

@ -0,0 +1,117 @@
import {
SlashCommandBuilder,
EmbedBuilder,
PermissionsBitField,
} from 'discord.js';
import { SubcommandCommand } from '../../types/CommandTypes.js';
import { getCountingData, setCount } from '../../util/countingManager.js';
import { loadConfig } from '../../util/configLoader.js';
const command: SubcommandCommand = {
data: new SlashCommandBuilder()
.setName('counting')
.setDescription('Commands related to the counting channel')
.addSubcommand((subcommand) =>
subcommand
.setName('status')
.setDescription('Check the current counting status'),
)
.addSubcommand((subcommand) =>
subcommand
.setName('setcount')
.setDescription(
'Set the current count to a specific number (Admin only)',
)
.addIntegerOption((option) =>
option
.setName('count')
.setDescription('The number to set as the current count')
.setRequired(true)
.setMinValue(0),
),
),
execute: async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'status') {
const countingData = await getCountingData();
const countingChannelId = loadConfig().channels.counting;
const embed = new EmbedBuilder()
.setTitle('Counting Channel Status')
.setColor(0x0099ff)
.addFields(
{
name: 'Current Count',
value: countingData.currentCount.toString(),
inline: true,
},
{
name: 'Next Number',
value: (countingData.currentCount + 1).toString(),
inline: true,
},
{
name: 'Highest Count',
value: countingData.highestCount.toString(),
inline: true,
},
{
name: 'Total Correct Counts',
value: countingData.totalCorrect.toString(),
inline: true,
},
{
name: 'Counting Channel',
value: `<#${countingChannelId}>`,
inline: true,
},
)
.setFooter({ text: 'Remember: No user can count twice in a row!' })
.setTimestamp();
if (countingData.lastUserId) {
embed.addFields({
name: 'Last Counter',
value: `<@${countingData.lastUserId}>`,
inline: true,
});
}
await interaction.reply({ embeds: [embed] });
} else if (subcommand === 'setcount') {
if (
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
content: 'You need administrator permissions to use this command.',
flags: ['Ephemeral'],
});
return;
}
const count = interaction.options.getInteger('count');
if (count === null) {
await interaction.reply({
content: 'Invalid count specified.',
flags: ['Ephemeral'],
});
return;
}
await setCount(count);
await interaction.reply({
content: `Count has been set to **${count}**. The next number should be **${count + 1}**.`,
flags: ['Ephemeral'],
});
}
},
};
export default command;

View file

@ -14,7 +14,7 @@ class RedisError extends Error {
}
}
redis.on('error', (error) => {
redis.on('error', (error: Error) => {
console.error('Redis connection error:', error);
throw new RedisError('Failed to connect to Redis instance: ', error);
});

View file

@ -1,6 +1,12 @@
import { AuditLogEvent, Events, Message, PartialMessage } from 'discord.js';
import { Event } from '../types/EventTypes.js';
import { loadConfig } from '../util/configLoader.js';
import {
addCountingReactions,
processCountingMessage,
resetCounting,
} from '../util/countingManager.js';
import logAction from '../util/logging/logAction.js';
export const messageDelete: Event<typeof Events.MessageDelete> = {
@ -62,4 +68,56 @@ export const messageUpdate: Event<typeof Events.MessageUpdate> = {
},
};
export default [messageDelete, messageUpdate];
export const messageCreate: Event<typeof Events.MessageCreate> = {
name: Events.MessageCreate,
execute: async (message: Message) => {
try {
if (message.author.bot) return;
const countingChannelId = loadConfig().channels.counting;
const countingChannel =
message.guild?.channels.cache.get(countingChannelId);
if (!countingChannel || message.channel.id !== countingChannelId) return;
if (!countingChannel.isTextBased()) {
console.error('Counting channel not found or is not a text channel');
return;
}
const result = await processCountingMessage(message);
if (result.isValid) {
await addCountingReactions(message, result.milestoneType || 'normal');
} else {
let errorMessage: string;
switch (result.reason) {
case 'not_a_number':
errorMessage = `${message.author}, that's not a valid number! The count has been reset. The next number should be **1**.`;
break;
case 'too_high':
errorMessage = `${message.author}, too high! The count was **${(result?.expectedCount ?? 0) - 1}** and the next number should have been **${result.expectedCount}**. The count has been reset.`;
break;
case 'too_low':
errorMessage = `${message.author}, too low! The count was **${(result?.expectedCount ?? 0) - 1}** and the next number should have been **${result.expectedCount}**. The count has been reset.`;
break;
case 'same_user':
errorMessage = `${message.author}, you can't count twice in a row! The count has been reset. The next number should be **1**.`;
break;
default:
errorMessage = `${message.author}, something went wrong with the count. The count has been reset. The next number should be **1**.`;
}
await resetCounting();
await countingChannel.send(errorMessage);
await message.react('❌');
}
} catch (error) {
console.error('Error handling message create:', error);
}
},
};
export default [messageCreate, messageDelete, messageUpdate];

View file

@ -2,6 +2,7 @@ import {
CommandInteraction,
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js';
export interface Command {
@ -13,3 +14,8 @@ export interface OptionsCommand {
data: SlashCommandOptionsOnlyBuilder;
execute: (interaction: CommandInteraction) => Promise<void>;
}
export interface SubcommandCommand {
data: SlashCommandSubcommandsOnlyBuilder;
execute: (interaction: CommandInteraction) => Promise<void>;
}

View file

@ -7,6 +7,7 @@ export interface Config {
channels: {
welcome: string;
logs: string;
counting: string;
};
roles: {
joinRoles: string[];

157
src/util/countingManager.ts Normal file
View file

@ -0,0 +1,157 @@
import { Message } from 'discord.js';
import { getJson, setJson } from '../db/redis.js';
interface CountingData {
currentCount: number;
lastUserId: string | null;
highestCount: number;
totalCorrect: number;
}
const MILESTONE_REACTIONS = {
normal: '✅',
multiples25: '✨',
multiples50: '⭐',
multiples100: '🎉',
};
export async function initializeCountingData(): Promise<CountingData> {
const exists = await getJson<CountingData>('counting');
if (exists) return exists;
const initialData: CountingData = {
currentCount: 0,
lastUserId: null,
highestCount: 0,
totalCorrect: 0,
};
await setJson<CountingData>('counting', initialData);
return initialData;
}
export async function getCountingData(): Promise<CountingData> {
const data = await getJson<CountingData>('counting');
if (!data) {
return initializeCountingData();
}
return data;
}
export async function updateCountingData(
data: Partial<CountingData>,
): Promise<void> {
const currentData = await getCountingData();
const updatedData = { ...currentData, ...data };
await setJson<CountingData>('counting', updatedData);
}
export async function resetCounting(): Promise<void> {
await updateCountingData({
currentCount: 0,
lastUserId: null,
});
return;
}
export async function processCountingMessage(message: Message): Promise<{
isValid: boolean;
expectedCount?: number;
isMilestone?: boolean;
milestoneType?: keyof typeof MILESTONE_REACTIONS;
reason?: string;
}> {
try {
const countingData = await getCountingData();
const content = message.content.trim();
const count = Number(content);
if (isNaN(count) || !Number.isInteger(count)) {
return {
isValid: false,
expectedCount: countingData.currentCount + 1,
reason: 'not_a_number',
};
}
const expectedCount = countingData.currentCount + 1;
if (count !== expectedCount) {
return {
isValid: false,
expectedCount,
reason: count > expectedCount ? 'too_high' : 'too_low',
};
}
if (countingData.lastUserId === message.author.id) {
return { isValid: false, expectedCount, reason: 'same_user' };
}
const newCount = countingData.currentCount + 1;
const newHighestCount = Math.max(newCount, countingData.highestCount);
await updateCountingData({
currentCount: newCount,
lastUserId: message.author.id,
highestCount: newHighestCount,
totalCorrect: countingData.totalCorrect + 1,
});
let isMilestone = false;
let milestoneType: keyof typeof MILESTONE_REACTIONS = 'normal';
if (newCount % 100 === 0) {
isMilestone = true;
milestoneType = 'multiples100';
} else if (newCount % 50 === 0) {
isMilestone = true;
milestoneType = 'multiples50';
} else if (newCount % 25 === 0) {
isMilestone = true;
milestoneType = 'multiples25';
}
return {
isValid: true,
expectedCount: newCount + 1,
isMilestone,
milestoneType,
};
} catch (error) {
console.error('Error processing counting message:', error);
return { isValid: false, reason: 'error' };
}
}
export async function addCountingReactions(
message: Message,
milestoneType: keyof typeof MILESTONE_REACTIONS,
): Promise<void> {
try {
await message.react(MILESTONE_REACTIONS[milestoneType]);
if (milestoneType === 'multiples100') {
await message.react('💯');
}
} catch (error) {
console.error('Error adding counting reactions:', error);
}
}
export async function getCountingStatus(): Promise<string> {
const data = await getCountingData();
return `Current count: ${data.currentCount}\nHighest count ever: ${data.highestCount}\nTotal correct counts: ${data.totalCorrect}`;
}
export async function setCount(count: number): Promise<void> {
if (!Number.isInteger(count) || count < 0) {
throw new Error('Count must be a non-negative integer.');
}
await updateCountingData({
currentCount: count,
lastUserId: null,
});
}