-
Notifications
You must be signed in to change notification settings - Fork 13
feat: cookie hunter #140
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
base: master
Are you sure you want to change the base?
feat: cookie hunter #140
Changes from all commits
306b654
2cab2df
a9b06c4
12410cf
9f6f7d2
5e323f9
2e90167
af40697
3d3ad2f
3a24fbe
c7d86a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
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' | ||
>; | ||
|
||
type FeatureName = 'Cookie Hunter' | 'Cookie Hunter Daily logs' | 'Quoi-Feur'; | ||
|
||
export const addChannelInCache = async ( | ||
interaction: ChatInputCommandInteraction, | ||
featureName: FeatureName, | ||
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: FeatureName, | ||
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), | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const ONE_MINUTE = 1 * 60 * 1000; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
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 Promise.all([ | ||
cache.set('currentHuntMessageId', cookieMessage.id), | ||
cache.set('cookieHunterDailyCount', {}), | ||
cookieMessage.react('🥛'), | ||
cookieMessage.react('🍪'), // 1 point for grandma here, she beats everyone who doesn't find her | ||
]); | ||
setTimeout(() => void dailyHuntEnd(client, cookieMessage), ONE_MINUTE); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()) continue; | ||
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]); | ||
|
||
return [ | ||
baseMessage, | ||
`Nombre de cookies total mangés : ${cookieEatenCount}`, | ||
`**Classement des chasseurs de cookies du jour**`, | ||
dailyRank.map(([userId, count]) => `<@${userId}>: ${count}`).join('\n'), | ||
milkJokerUserId | ||
? `<@${milkJokerUserId}> a accompagné ses cookies d'un grand verre de lait 🥛` | ||
: null, | ||
`Sacré bande de gourmands !`, | ||
] | ||
.filter(Boolean) | ||
.join('\n'); | ||
}; | ||
|
||
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 Promise.all([ | ||
cache.delete('milkJokerUserId'), | ||
cache.delete('currentHuntMessageId'), | ||
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is not random ? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 === '🥛' && !user.bot) { | ||
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); | ||
}; |
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'], | ||
}); |
There was a problem hiding this comment.
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
?