Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
7 changes: 7 additions & 0 deletions src/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ interface CacheEntries {
lobbyIds: string[];
onDemandChannels: string[];
quoiFeurChannels: string[];
cookieHunterChannels: string[];
currentHuntMessageId: string;
cookieHunterDailyCount: Record<string, number>;
cookieHunterDailyLogChannels: string[];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about adding a bit of jsdoc on top of some of these entries?

it may not be obvious what we are storing here without looking at the code

is cookieHunterDailyCount a discord user id <> score record?
what is cookieHunterDailyLogChannels ?

cookieHunterScoreboard: Record<string, number>;
milkJokerUserId: string;
recurringMessages: { id: string; channelId: string; frequency: Frequency; message: string }[];
}

Expand Down Expand Up @@ -65,3 +71,4 @@ class CacheImpl implements Cache<CacheEntries> {
}

export const cache = new CacheImpl();
export type { CacheEntries };
72 changes: 72 additions & 0 deletions src/helpers/channels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
ChannelType,
type ChatInputCommandInteraction,
type DMChannel,
type NonThreadGuildBasedChannel,
} from 'discord.js';

import { cache, type CacheEntries } from '../core/cache';

type ChannelArrayCacheKey = Pick<
CacheEntries,
'quoiFeurChannels' | 'cookieHunterChannels' | 'cookieHunterDailyLogChannels'
>;

export const addChannelInCache = async (
interaction: ChatInputCommandInteraction,
featureName: string,
cacheKey: keyof ChannelArrayCacheKey,
) => {
const { channel } = interaction;
if (!channel || !channel.isTextBased() || channel.type !== ChannelType.GuildText) return;

const channels = await cache.get(cacheKey, []);
if (channels.includes(channel.id)) {
await interaction.reply({
content: `${featureName} is already enabled in this channel`,
ephemeral: true,
});
return;
}

await cache.set(cacheKey, [...channels, channel.id]);
await interaction.reply({ content: `${featureName} enabled in this channel`, ephemeral: true });
};

export const removeChannelFromChache = async (
interaction: ChatInputCommandInteraction,
featureName: string,
cacheKey: keyof ChannelArrayCacheKey,
) => {
const { channel } = interaction;
if (!channel || !channel.isTextBased() || channel.type !== ChannelType.GuildText) return;

const channels = await cache.get(cacheKey, []);
if (!channels.includes(channel.id)) {
await interaction.reply({
content: `${featureName} is not enabled in this channel`,
ephemeral: true,
});
return;
}

await cache.set(
cacheKey,
channels.filter((channelId) => channelId !== channel.id),
);
await interaction.reply({ content: `${featureName} disabled in this channel`, ephemeral: true });
};

export const cleanCacheOnChannelDelete = async (
channel: DMChannel | NonThreadGuildBasedChannel,
cacheKey: keyof ChannelArrayCacheKey,
) => {
const { id } = channel;
const channels = await cache.get(cacheKey, []);
if (!channels.includes(id)) return;

await cache.set(
cacheKey,
channels.filter((channelId) => channelId !== id),
);
};
1 change: 1 addition & 0 deletions src/helpers/timeConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ONE_MINUTE = 1 * 60 * 1000;
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createAllModules } from './core/createEnvForModule';
import { env } from './core/env';
import { getIntentsFromModules } from './core/getIntentsFromModules';
import { loadModules } from './core/loadModules';
import { cookieHunter } from './modules/cookieHunter/cookieHunter.module';
import { coolLinksManagement } from './modules/coolLinksManagement/coolLinksManagement.module';
import { fart } from './modules/fart/fart.module';
import { fixEmbedTwitterVideo } from './modules/fixEmbedTwitterVideo/fixEmbedTwitterVideo.module';
Expand All @@ -18,6 +19,7 @@ const modules = [
quoiFeur,
recurringMessage,
fixEmbedTwitterVideo,
cookieHunter,
];

const createdModules = await createAllModules(modules);
Expand Down
187 changes: 187 additions & 0 deletions src/modules/cookieHunter/cookieHunter.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { CronJob } from 'cron';
import type {
ChatInputCommandInteraction,
Client,
Message,
MessageReaction,
PartialMessageReaction,
PartialUser,
User,
} from 'discord.js';

import { cache } from '../../core/cache';
import { ONE_MINUTE } from '../../helpers/timeConstants';

const IT_IS_SNACK_TIME = '0 30 16 * * *'; // 4:30pm every day

let jobCurrentlyRunning: CronJob | null = null;

const sendMessageInRandomChannel = async (client: Client<true>) => {
const channel = await cache.get('cookieHunterChannels', []);
if (!channel.length) return;

const randomChannel = channel[Math.floor(Math.random() * channel.length)];
if (!randomChannel) return;

const channelToSend = await client.channels.fetch(randomChannel);

if (!channelToSend || !channelToSend.isTextBased()) return;
const cookieMessage = await channelToSend.send('**👵 Qui veut des cookies ?**');
await cache.set('currentHuntMessageId', cookieMessage.id);
await cache.set('cookieHunterDailyCount', {});
await cookieMessage.react('🥛');
await cookieMessage.react('🍪'); // 1 point for grandma here, she beats everyone who doesn't find her

setTimeout(() => void dailyHuntEnd(client, cookieMessage), ONE_MINUTE);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe overkill to manage this now, and i m not sure often the bot crash/restarts. but what happens if the bot crashes? the daily hunt will never stop

a simple fix would be to manage it in countCookies method. when some dude react, you check if the message was posted more than a minute ago and if so, you don't count the point and manually call dailyHuntEnd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I'm not a big fan of this setTimeout too... I will need to store the start time to do that but look like a good solution to avoid an infinite hunt

};

const handleMilkReaction = async (
reaction: MessageReaction | PartialMessageReaction,
user: User | PartialUser,
isMilkJokerAlreadyFound: boolean,
) => {
if (isMilkJokerAlreadyFound) {
await reaction.message.reply({
content: `Il est lent ce lait... 🥛`,
options: { ephemeral: true },
});
} else {
await cache.set('milkJokerUserId', user.id);
await reaction.message.reply({
content: `Premier arrivé, premier servit. Cul sec 🥛 !`,
options: { ephemeral: true },
});
}
};

const applyMilkJoker = async () => {
const milkJokerUserId = await cache.get('milkJokerUserId');
if (!milkJokerUserId) return;

const cookieHunterDailyCount = await cache.get('cookieHunterDailyCount', {});
const userDailyCount = cookieHunterDailyCount[milkJokerUserId] || 0;
const newDailyCount = { ...cookieHunterDailyCount, [milkJokerUserId]: userDailyCount * 2 };
await cache.set('cookieHunterDailyCount', newDailyCount);
};

const logDailyCount = async (client: Client<true>) => {
const dailyLogChannels = await cache.get('cookieHunterDailyLogChannels', []);
if (!dailyLogChannels.length) return;

const currentHuntMessageId = await cache.get('currentHuntMessageId');
if (!currentHuntMessageId)
throw new Error('Lost the hunt message id before logging the daily count');

const cookieHunterDailyCount = await cache.get('cookieHunterDailyCount', {});
const hunterCount = Object.keys(cookieHunterDailyCount).length - 1; // grandma is not a hunter

const milkJokerUserId = await cache.get('milkJokerUserId');

const resume = `**🍪 Résumé de la chasse aux cookies du jour**\n`;
const where = `Mamie a servi des cookies dans <#${currentHuntMessageId}>\n`;
const baseMessage = `${resume}${where}`;

const message =
hunterCount > 0
? getHuntersFoundGrandmaMessage(baseMessage, cookieHunterDailyCount, milkJokerUserId)
: `${baseMessage}**🍪 Personne n'a trouvé Mamie !**\nFaut dire qu'elle se cache bien (et que vous êtes nazes) !`;

for (const channelId of dailyLogChannels) {
const channel = await client.channels.fetch(channelId);
if (!channel || !channel.isTextBased()) return;
await channel.send(message);
}
};

const getHuntersFoundGrandmaMessage = (
baseMessage: string,
cookieHunterDailyCount: Record<string, number>,
milkJokerUserId: string | undefined,
) => {
const cookieEatenCount = Object.values(cookieHunterDailyCount).reduce(
(acc, count) => acc + count,
0,
);
const dailyRank = Object.entries(cookieHunterDailyCount).sort((a, b) => b[1] - a[1]);

const totalEaten = `Nombre de cookies total mangés : ${cookieEatenCount}\n`;
const ranking = `**Classement des chasseurs de cookies du jour**\n`;
const usersRanking = dailyRank.map(([userId, count]) => `<@${userId}>: ${count}\n`).join('\n');
const milkJoker = milkJokerUserId
? `<@${milkJokerUserId}> a accompagné ses cookies d'un grand verre de lait 🥛\n`
: '';
const lastWords = `Sacré bande de gourmands !`;

return `${baseMessage}${totalEaten}${ranking}${usersRanking}${milkJoker}${lastWords}`;
};

const updateGlobalScoreboard = async () => {
const cookieHunterDailyCount = await cache.get('cookieHunterDailyCount', {});
const coockieHunterScoreboard = await cache.get('cookieHunterScoreboard', {});
for (const [userId, count] of Object.entries(cookieHunterDailyCount)) {
coockieHunterScoreboard[userId] = (coockieHunterScoreboard[userId] || 0) + count;
}
await cache.set('cookieHunterScoreboard', coockieHunterScoreboard);
};

const dailyHuntEnd = async (client: Client<true>, cookieMessage: Message) => {
await cookieMessage.delete();
await applyMilkJoker();
await logDailyCount(client);
await updateGlobalScoreboard();
await cache.delete('milkJokerUserId');
await cache.delete('currentHuntMessageId');
await cache.delete('cookieHunterDailyCount');
};

export const startHunting = (client: Client<true>) => {
console.log('Cookie hunter started');
if (jobCurrentlyRunning !== null) {
// needed in case that the bot fire multiple ready event
jobCurrentlyRunning.stop();
}
jobCurrentlyRunning = new CronJob(
IT_IS_SNACK_TIME,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not random ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not for the V1

() => sendMessageInRandomChannel(client),
null,
true,
'Europe/Paris',
);
};

export const countCookies = async (
reaction: MessageReaction | PartialMessageReaction,
user: User | PartialUser,
) => {
const currentHuntMessageId = await cache.get('currentHuntMessageId');
if (
!currentHuntMessageId ||
reaction.message.id !== currentHuntMessageId ||
reaction.emoji.name === null ||
!['🍪', '🥛'].includes(reaction.emoji.name)
)
return;

const isMilkJokerAlreadyFound = Boolean(await cache.get('milkJokerUserId'));

if (reaction.emoji.name === '🥛') {
await handleMilkReaction(reaction, user, isMilkJokerAlreadyFound);
}

const cookieHunterDailyCount = await cache.get('cookieHunterDailyCount', {});
const userDailyCount = cookieHunterDailyCount[user.id] || 0;
const newDailyCount = { ...cookieHunterDailyCount, [user.id]: userDailyCount + 1 };
await cache.set('cookieHunterDailyCount', newDailyCount);
};

export const displayScoreboard = async (interaction: ChatInputCommandInteraction) => {
const cookieHunterScoreboard = await cache.get('cookieHunterScoreboard', {});
const ranking = Object.entries(cookieHunterScoreboard)
.sort((a, b) => b[1] - a[1])
.map(([userId, count], index) => `${index + 1}. <@${userId}>: ${count}`)
.join('\n');

const message = `**🍪 Classement général des chasseurs de cookies**\n${ranking}`;

await interaction.reply(message);
};
71 changes: 71 additions & 0 deletions src/modules/cookieHunter/cookieHunter.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';

import { createModule } from '../../core/createModule';
import {
addChannelInCache,
cleanCacheOnChannelDelete,
removeChannelFromChache,
} from '../../helpers/channels';
import { countCookies, displayScoreboard, startHunting } from './cookieHunter.helpers';

export const cookieHunter = createModule({
name: 'cookieHunter',
slashCommands: () => [
{
schema: new SlashCommandBuilder()
.setName('cookie-hunter')
.setDescription('Cookie hunting game for the server')
.addSubcommand((subcommand) =>
subcommand.setName('start').setDescription('Start the cookie hunt'),
)
.addSubcommand((subcommand) =>
subcommand.setName('enable').setDescription('Enable the cookie hunt in the channel'),
)
.addSubcommand((subcommand) =>
subcommand.setName('disable').setDescription('Disable the cookie hunt in the channel'),
)
.addSubcommand((subcommand) =>
subcommand.setName('add-daily-log').setDescription('Add daily log to the channel'),
)
.addSubcommand((subcommand) =>
subcommand.setName('remove-daily-log').setDescription('Add daily log to the channel'),
)
.addSubcommand((subcommand) =>
subcommand.setName('scoreboard').setDescription('Show the scoreboard'),
)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
.toJSON(),
handler: {
// eslint-disable-next-line @typescript-eslint/require-await
'start': async (interaction) => startHunting(interaction.client),
'enable': (interaction) =>
addChannelInCache(interaction, 'Cookie Hunter', 'cookieHunterChannels'),
'disable': (interaction) =>
removeChannelFromChache(interaction, 'Cookie Hunter', 'cookieHunterChannels'),
'add-daily-log': (interaction) =>
addChannelInCache(
interaction,
'Cookie Hunter Daily logs',
'cookieHunterDailyLogChannels',
),
'remove-daily-log': (interaction) =>
removeChannelFromChache(
interaction,
'Cookie Hunter Daily logs',
'cookieHunterDailyLogChannels',
),
'scoreboard': async (interaction) => displayScoreboard(interaction),
},
},
],
eventHandlers: () => ({
// eslint-disable-next-line @typescript-eslint/require-await
ready: async (client) => startHunting(client),
messageReactionAdd: countCookies,
channelDelete: async (channel) => {
await cleanCacheOnChannelDelete(channel, 'cookieHunterChannels');
await cleanCacheOnChannelDelete(channel, 'cookieHunterDailyLogChannels');
},
}),
intents: ['Guilds'],
});
Loading