diff --git a/package.json b/package.json index 5e180e0..aa434e3 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,14 @@ "author": "oatmealine", "license": "AGPL-3.0", "dependencies": { + "d3-array": "^3.2.4", "discord.js": "^14.11.0", "got": "^11.8.3", "parse-color": "^1.0.0", "random-seed": "^0.3.0" }, "devDependencies": { + "@types/d3-array": "^3.0.9", "@types/parse-color": "^1.0.1", "@typescript-eslint/eslint-plugin": "^5.27.1", "@typescript-eslint/parser": "^5.27.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abfc2a6..dc65c11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,6 +1,13 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: + d3-array: + specifier: ^3.2.4 + version: 3.2.4 discord.js: specifier: ^14.11.0 version: 14.11.0 @@ -15,6 +22,9 @@ dependencies: version: 0.3.0 devDependencies: + '@types/d3-array': + specifier: ^3.0.9 + version: 3.0.9 '@types/parse-color': specifier: ^1.0.1 version: 1.0.1 @@ -194,6 +204,10 @@ packages: '@types/responselike': 1.0.0 dev: false + /@types/d3-array@3.0.9: + resolution: {integrity: sha512-mZowFN3p64ajCJJ4riVYlOjNlBJv3hctgAY01pjw3qTnJePD8s9DZmYDzhHKvzfCYvdjwylkU38+Vdt7Cu2FDA==} + dev: true + /@types/http-cache-semantics@4.0.1: resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} dev: false @@ -497,6 +511,13 @@ packages: which: 2.0.2 dev: true + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -887,6 +908,11 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} diff --git a/src/commands/markov.ts b/src/commands/markov.ts index 24cc3bc..fd17c36 100644 --- a/src/commands/markov.ts +++ b/src/commands/markov.ts @@ -1,9 +1,5 @@ -import { GuildMember, Message, User, SlashCommandBuilder, Interaction } from 'discord.js'; - -const DEFAULT_EMOJI = 'đŸĒ™'; -const STOP_EMOJI = '⏚ī¸'; -const DONE_EMOJI = '👍'; -const BAD_EMOJI = '👎'; +import { GuildMember, SlashCommandBuilder, Interaction } from 'discord.js'; +import { getTextResponsePrettyPlease, randomWord, sendSegments, startGame } from '../lib/game'; const END_TEMPLATES = [ 'Alright! Here\'s the messages you all conjured:', @@ -12,35 +8,6 @@ const END_TEMPLATES = [ 'That does it! Here\'s what you\'ve all cooked up together:' ]; -const RANDOM_WORDS = [ - 'tarsorado', 'aboba', 'robtop', 'viprin', 'milk', 'milking station', 'radiation', 'extreme', 'glogging', 'glogged', - 'penis', 'deadlocked', 'cream', 'dragon cream', 'urine', 'communal', 'piss', 'matpat', 'big and round', 'easy', - 'cum', 'glue', 'tampon', 'contaminated water', 'centrifuge', 'inflation', 'plutonium', 'uranium', 'thorium', - 'imposter', 'sounding', 'đŸ’Ĩ', 'đŸĨĩ', '🎊', '!!!', '...', '???', '?..', '?!', '!', '?', 'balls itch', 'robert', - 'gas leak', 'among us', 'stick a finger', 'overclock', 'breed', 'gay sex', 'breedable', 'cock vore', 'appendix', - 'mukbang', 'edging', 'onlyfans', 'productive', 'mandelbrot', 'novosibirsk', 'oops!', 'farting', 'memory leak', - 'pepsi can' -]; - -function formatMessage(users: User[], time: number) { - return `Starting a Markov chain game (${users.length} player${users.length !== 1 ? 's' : ''})\n` - + users.map(user => `- ${user.toString()}`).join('\n') + '\n' - + (time <= 0 ? - '**Already started**' : - `Starting in **${Math.ceil(time / 1000)}s** - react ${STOP_EMOJI} to begin now` - ); -} - -function randomWord() { - return RANDOM_WORDS[Math.floor(Math.random() * RANDOM_WORDS.length)]; -} - -function* chunks(arr: unknown[], n: number) { - for (let i = 0; i < arr.length; i += n) { - yield arr.slice(i, i + n); - } -} - module.exports = { data: new SlashCommandBuilder() .setName('markov') @@ -64,58 +31,10 @@ module.exports = { execute: async (interaction: Interaction, member: GuildMember) => { if (!interaction.isChatInputCommand()) return; - let participants: User[] = [member.user]; const context = interaction.options.getInteger('context') || 3; const maxIterations = interaction.options.getInteger('iterations') || 10; - const duration = 25_000; - - const m = await interaction.reply({ - fetchReply: true, - content: formatMessage(participants, duration) - }); - - if (!(m instanceof Message)) return; - - const emoji = m.guild?.emojis.cache.random(); - await m.react(emoji || DEFAULT_EMOJI); - await m.react(STOP_EMOJI); - - const started = Date.now(); - const collector = m.createReactionCollector({ - filter: (reaction, user) => - !user.bot && ( - ( - (emoji ? reaction.emoji.id === emoji.id : reaction.emoji.name === DEFAULT_EMOJI) && - !participants.find(u => user.id === u.id) - ) || (reaction.emoji.name === STOP_EMOJI && user.id === member.id) - ), - time: duration, - dispose: true - }); - - const updateInterval = setInterval(() => { - m.edit(formatMessage(participants, duration - (Date.now() - started))); - }, 3_000); - - collector.on('collect', (reaction, user) => { - if (reaction.emoji.name === STOP_EMOJI) { - collector.stop(); - } else { - participants.push(user); - m.edit(formatMessage(participants, duration - (Date.now() - started))); - } - }); - - collector.on('remove', (_, user) => { - participants = participants.filter(u => u.id !== user.id); - m.edit(formatMessage(participants, duration - (Date.now() - started))); - }); - - collector.on('end', async () => { - clearInterval(updateInterval); - m.edit(formatMessage(participants, 0)); - + startGame(interaction, member.user, 'Markov game', async (participants, channel) => { let sentences: string[][] = Array(participants.length).fill(0); sentences = sentences.map(() => []); let iterations = 0; @@ -125,25 +44,10 @@ module.exports = { await Promise.all( participants.map(async (p, i) => { const sentence = sentences[(i + iterations) % sentences.length]; - const msg = await p.send((`Continue the following sentence: [${iterations}/${maxIterations}]\n\n> _${context < sentence.length ? 'â€Ļ' : ''}${sentence.length > 0 ? sentence.slice(-context).join(' ') : `start a sentence... (try working with: “${randomWord()}”)`}_` + (iterations === 0 ? '\n\n**Send a message to continue**' : '')).slice(0, 2000)); - try { - const collected = await msg.channel.awaitMessages({ - max: 1, - time: 45_000, - errors: ['time'], - filter: (msg) => { - const valid = msg.content !== '' && msg.content.length <= 2000; - if (!valid) msg.react(BAD_EMOJI); - return valid; - } - }); - const message = collected.first() as Message; - sentence.push(...message.content.split(' ')); - await message.react(DONE_EMOJI); - } catch (err) { - await p.send('Took too long... Surprise... Added...... :)'); - sentence.push(...randomWord().split(' ')); - } + const prompt = (`Continue the following sentence: [${iterations}/${maxIterations}]\n\n> _${context < sentence.length ? 'â€Ļ' : ''}${sentence.length > 0 ? sentence.slice(-context).join(' ') : `start a sentence... (try working with: “${randomWord()}”)`}_` + (iterations === 0 ? '\n\n**Send a message to continue**' : '')).slice(0, 2000); + + const resp = await getTextResponsePrettyPlease(p, prompt); + sentence.push(...resp.split(' ')); }) ); @@ -162,35 +66,7 @@ module.exports = { '\n\nThank you for participating :)' ]; - const content = []; - let contentBuffer = ''; - - while (segments.length > 0) { - const segment = segments.splice(0, 1)[0]; - const newMsg = contentBuffer + '\n' + segment; - if (newMsg.length > 2000) { - content.push(contentBuffer); - contentBuffer = ''; - - if (segment.length > 2000) { - content.push(...([...(chunks(segment.split(''), 2000))].map(s => s.join('')))); - } else { - contentBuffer = segment; - } - } else { - contentBuffer = newMsg; - } - } - if (contentBuffer !== '') content.push(contentBuffer); - - content.forEach(async content => { - await m.channel.send({ - content: content, - allowedMentions: { - parse: ['users'] - } - }); - }); + await sendSegments(segments, channel); }); } }; \ No newline at end of file diff --git a/src/commands/twosentencehorror.ts b/src/commands/twosentencehorror.ts new file mode 100644 index 0000000..298200d --- /dev/null +++ b/src/commands/twosentencehorror.ts @@ -0,0 +1,55 @@ +import { GuildMember, SlashCommandBuilder, Interaction } from 'discord.js'; +import { getTextResponsePrettyPlease, randomWord, sendSegments, startGame } from '../lib/game'; +import { shuffle } from 'd3-array'; +import { knownServers } from '../lib/knownServers'; + +module.exports = { + data: new SlashCommandBuilder() + .setName('twosentencehorror') + .setDescription('Communally create the worst horror stories known to man'), + + serverWhitelist: [...knownServers.fbi], + + execute: async (interaction: Interaction, member: GuildMember) => { + if (!interaction.isChatInputCommand()) return; + + startGame(interaction, member.user, 'Markov game', async (players, channel) => { + const firstPlayers = shuffle(players); + const secondPlayers = [...firstPlayers.slice(1), firstPlayers[0]]; // shift by 1 + + const firstHalves = await Promise.all(firstPlayers.map(async (firstPlayer) => { + let firstHalf = await getTextResponsePrettyPlease( + firstPlayer, + `Start a horror story... (1 sentence max!) (try working with: “${randomWord()}”)\n\n**Send a message to continue**`, + (msg) => !(msg.includes('. ') || msg.includes('! ') || msg.includes('? ')) + ); + + if (!(firstHalf.endsWith('.') || firstHalf.endsWith('!') || firstHalf.endsWith('?'))) firstHalf = firstHalf + '.'; + + return firstHalf; + })); + + const secondHalves = await Promise.all(secondPlayers.map(async (secondPlayer, i) => { + const firstHalf = firstHalves[i]; + + let secondHalf = await getTextResponsePrettyPlease( + secondPlayer, + `> _${firstHalf}_\nContinue this sentence with a twist!`, + (msg) => !(msg.includes('. ') || msg.includes('! ') || msg.includes('? ')) + ); + + if (!(secondHalf.endsWith('.') || secondHalf.endsWith('!') || secondHalf.endsWith('?'))) secondHalf = secondHalf + '.'; + + return secondHalf; + })); + + const segments = [ + 'Here\'s the bone-chilling stories you\'ve all created:\n\n', + ...firstHalves.map((firstHalf, i) => firstHalf + ' ' + secondHalves[i]), + '\n\nThank you for participating :)' + ]; + + await sendSegments(segments, channel); + }); + } +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 3a05a10..823ba0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,7 +48,8 @@ bot.on(Events.InteractionCreate, async (interaction) => { try { await command.execute(interaction, interaction.member); } catch (error) { - interaction.reply({ content: '`ERROR`', ephemeral: true }); + if (interaction.isRepliable() && !interaction.replied && !interaction.deferred) interaction.reply({ content: '`ERROR`', ephemeral: true }); + if (interaction.deferred) interaction.followUp('`ERROR`'); console.error(error); } }); diff --git a/src/lib/game.ts b/src/lib/game.ts new file mode 100644 index 0000000..8f3a962 --- /dev/null +++ b/src/lib/game.ts @@ -0,0 +1,153 @@ +import { Interaction, Message, TextBasedChannel, User } from 'discord.js'; + +export const RANDOM_WORDS = [ + 'tarsorado', 'aboba', 'robtop', 'viprin', 'milk', 'milking station', 'radiation', 'extreme', 'glogging', 'glogged', + 'penis', 'deadlocked', 'cream', 'dragon cream', 'urine', 'communal', 'piss', 'matpat', 'big and round', 'easy', + 'cum', 'glue', 'tampon', 'contaminated water', 'centrifuge', 'inflation', 'plutonium', 'uranium', 'thorium', + 'imposter', 'sounding', 'đŸ’Ĩ', 'đŸĨĩ', '🎊', '!!!', '...', '???', '?..', '?!', '!', '?', 'balls itch', 'robert', + 'gas leak', 'among us', 'stick a finger', 'overclock', 'breed', 'gay sex', 'breedable', 'cock vore', 'appendix', + 'mukbang', 'edging', 'onlyfans', 'productive', 'mandelbrot', 'novosibirsk', 'oops!', 'farting', 'memory leak', + 'pepsi can' +]; + +export function randomWord() { + return RANDOM_WORDS[Math.floor(Math.random() * RANDOM_WORDS.length)]; +} + +const DONE_EMOJI = '👍'; +const BAD_EMOJI = '👎'; + +const DEFAULT_EMOJI = 'đŸĒ™'; +const STOP_EMOJI = '⏚ī¸'; + +function formatMessage(users: User[], time: number, name: string, ended = false) { + return `Starting a **${name}** game (${users.length} player${users.length !== 1 ? 's' : ''})\n` + + users.map(user => `- ${user.toString()}`).join('\n') + '\n' + + (time <= 0 ? + (ended ? '**Already ended!**' : '**Already started**') : + `Starting in **${Math.ceil(time / 1000)}s** - react ${STOP_EMOJI} to begin now` + ); +} + +export async function startGame(interaction: Interaction, startingUser: User, name: string, callback: (players: User[], channel: TextBasedChannel) => Promise) { + if (!interaction.isChatInputCommand()) return; + + let participants: User[] = [startingUser]; + const duration = 25_000; + + const m = await interaction.reply({ + fetchReply: true, + content: formatMessage(participants, duration, name) + }); + + if (!(m instanceof Message)) return; + + const emoji = m.guild?.emojis.cache.random(); + await m.react(emoji || DEFAULT_EMOJI); + await m.react(STOP_EMOJI); + + const started = Date.now(); + const collector = m.createReactionCollector({ + filter: (reaction, user) => + !user.bot && ( + ( + (emoji ? reaction.emoji.id === emoji.id : reaction.emoji.name === DEFAULT_EMOJI) && + !participants.find(u => user.id === u.id) + ) || (reaction.emoji.name === STOP_EMOJI && user.id === startingUser.id) + ), + time: duration, + dispose: true + }); + + const updateInterval = setInterval(() => { + m.edit(formatMessage(participants, duration - (Date.now() - started), name)); + }, 3_000); + + collector.on('collect', (reaction, user) => { + if (reaction.emoji.name === STOP_EMOJI) { + collector.stop(); + } else { + participants.push(user); + m.edit(formatMessage(participants, duration - (Date.now() - started), name)); + } + }); + + collector.on('remove', (_, user) => { + participants = participants.filter(u => u.id !== user.id); + m.edit(formatMessage(participants, duration - (Date.now() - started), name)); + }); + + collector.on('end', async () => { + clearInterval(updateInterval); + m.edit(formatMessage(participants, 0, name)); + + await callback(participants, m.channel); + + m.edit(formatMessage(participants, 0, name, true)); + }); +} + +export async function getTextResponse(user: User, prompt: string, filter: (content: string) => boolean = () => true): Promise { + const msg = await user.send(prompt); + try { + const collected = await msg.channel.awaitMessages({ + max: 1, + time: 45_000, + errors: ['time'], + filter: (msg) => { + const valid = msg.content !== '' && msg.content.length <= 2000 && filter(msg.content); + if (!valid) msg.react(BAD_EMOJI); + return valid; + } + }); + const message = collected.first() as Message; + await message.react(DONE_EMOJI); + return message.content; + } catch (err) { + return null; + } +} +export async function getTextResponsePrettyPlease(user: User, prompt: string, filter: (content: string) => boolean = () => true): Promise { + const resp = await getTextResponse(user, prompt, filter); + if (resp) return resp; + user.send('Took too long... Surprise... Added...... :)'); + return randomWord(); +} + +function* chunks(arr: unknown[], n: number) { + for (let i = 0; i < arr.length; i += n) { + yield arr.slice(i, i + n); + } +} + +export async function sendSegments(segments: string[], channel: TextBasedChannel) { + const content = []; + let contentBuffer = ''; + + while (segments.length > 0) { + const segment = segments.splice(0, 1)[0]; + const newMsg = contentBuffer + '\n' + segment; + if (newMsg.length > 2000) { + content.push(contentBuffer); + contentBuffer = ''; + + if (segment.length > 2000) { + content.push(...([...(chunks(segment.split(''), 2000))].map(s => s.join('')))); + } else { + contentBuffer = segment; + } + } else { + contentBuffer = newMsg; + } + } + if (contentBuffer !== '') content.push(contentBuffer); + + content.forEach(async content => { + await channel.send({ + content: content, + allowedMentions: { + parse: ['users'] + } + }); + }); +} \ No newline at end of file