diff --git a/bun.lockb b/bun.lockb index c2cfc73..317c889 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/index.js b/index.js deleted file mode 100644 index 210f18b..0000000 --- a/index.js +++ /dev/null @@ -1,353 +0,0 @@ -import { Client, GatewayIntentBits } from "discord.js"; -import { - joinVoiceChannel, - createAudioPlayer, - createAudioResource, -} from "@discordjs/voice"; - -import fs from "fs"; -import path from "path"; -import { DateTime } from "luxon"; -import fetch from "node-fetch"; - -import { MsEdgeTTS } from "msedge-tts"; - -import * as dotenv from "dotenv"; -import { createStore } from "zustand/vanilla"; - -dotenv.config(); - -const channels = process.env.CHANNELIDS.split(","); - -const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildVoiceStates, - ], - allowedMentions: { parse: [], repliedUser: false }, -}); - -if (!fs.existsSync("./temp")) fs.mkdirSync("./temp"); - -// Map to store the last message timestamp per person -const cooldowns = new Map(); - -client.on("ready", async () => { - console.log(`Ready! Logged in as ${client.user.tag}`); - client.user.setActivity(`uwu`); -}); - -function shouldIReply(message) { - if (!message.content && !message.attachments) return false; - if ( - Math.random() < process.env.REPLY_CHANCE && - !channels.includes(message.channel.id) && - !message.mentions.has(client.user.id) - ) - return false; - if (message.content.startsWith("!!")) return false; - - // Check cooldown for the person who sent the message - const lastMessageTime = cooldowns.get(message.author.id); - if (lastMessageTime && Date.now() - lastMessageTime < 1000) return false; // Ignore the message if the cooldown hasn't expired - - // Update the last message timestamp for the person - cooldowns.set(message.author.id, Date.now()); - - return true; -} - -async function getPronouns(userid) { - // this is spagetti i'm sorry - try { - let response = await fetch( - `https://pronoundb.org/api/v2/lookup?platform=discord&ids=${userid}` - ); - - response = await response.json(); - - for (let userId in response) { - if (response[userId].sets.hasOwnProperty("en")) { - response[userId] = response[userId].sets["en"].join("/"); - } else { - response[userId] = "they/them"; - } - } - if (!response.hasOwnProperty(userid)) { - response[userid] = "they/them"; - } - - return response[userid]; - } catch (error) { - console.error(error); - } -} - -const character = fs.readFileSync("./character.txt", "utf8").replace("\n", " "); - -const initialHistory = [ - { role: "system", content: character }, - { - role: "user", - content: "lily (she/her) on May 14, 2024 at 12:55 AM UTC: hi sponge", - }, - { role: "assistant", content: "hi lily! how are you today :3" }, - { - role: "user", - content: - "zynsterr (they/them) on May 14, 2024 at 3:02 PM UTC: generate me an image of cheese puffs", - }, - { - role: "assistant", - content: "sure thing!\n!gen picture of a bowl of cheese puffs on a table", - }, -]; - -global.initialHistory = initialHistory; -let lastMessage = ""; -// let history = initialHistory; - -const historyStore = createStore((set) => ({ - history: [...initialHistory], - addMessage: ({ role, content }) => - set((s) => ({ - history: [ - ...s?.history, - { - role, - content, - }, - ], - })), - reset: () => - set((s) => ({ - history: [...initialHistory], - })), -})); - -client.on("messageCreate", async (message) => { - if (message.author.bot) return; - if (!shouldIReply(message)) return; - - const store = () => historyStore.getState(); - - try { - // Conversation reset - if (message.content.startsWith("%reset")) { - const totalAmountToReset = - store()?.history?.length - initialHistory?.length; - const initialResetReply = await message.reply( - `♻️ Conversation history reset (clearing ${totalAmountToReset} entries).` - ); - store()?.reset(); - initialResetReply.edit({ - content: `♻️ Conversation history reset (cleared ${totalAmountToReset} entries).`, - }); - return; - } - - if (message.content.startsWith("%messages")) { - message.reply( - `ℹ️ ${store()?.history?.length - initialHistory?.length} entries exist.` - ); - return; - } - - if (message.content.startsWith("%readback")) { - message.reply(`\`${lastMessage}\``); - return; - } - - message.channel.sendTyping(); - const imageDetails = await imageRecognition(message); - - // Send message to CharacterAI - let formattedUserMessage = `${message.author.username} (${await getPronouns( - message.author.id - )}) on ${DateTime.now() - .setZone("utc") - .toLocaleString(DateTime.DATETIME_FULL)}: ${ - message.content - }\n${imageDetails}`; - if (message.reference) { - await message.fetchReference().then(async (reply) => { - if (reply.author.id == client.user.id) { - formattedUserMessage = `> ${reply}\n${formattedUserMessage}`; - } else { - formattedUserMessage = `> ${reply.author.username}: ${reply}\n${formattedUserMessage}`; - } - }); - } - lastMessage = formattedUserMessage; - - message.channel.sendTyping(); - - store().addMessage({ role: "user", content: formattedUserMessage }); - const input = { - messages: store().history, - max_tokens: 512, - }; - let response = await fetch( - `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT}/ai/run/@cf/meta/llama-3-8b-instruct`, - { - method: "POST", - headers: { - Authorization: `Bearer ${process.env.CF_TOKEN}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(input), - } - ); - response = await response.json(); - response = response.result.response; - store().addMessage({ role: "assistant", content: response }); - if (store().history.length % 20 == 0) { - store().addMessage({ role: "system", content: `reminder: ${character}` }); - } - - if (response == "") - return message.reply( - `❌ AI returned an empty response! Yell at someone idk.` - ); - - let parts = response.split("!gen"); - - let trimmedResponse = parts[0].trim(); - - // Handle long responses - if (trimmedResponse.length >= 2000) { - fs.writeFileSync(path.resolve("./temp/how.txt"), trimmedResponse); - message.reply({ - content: "", - files: ["./temp/how.txt"], - failIfNotExists: false, - }); - return; - } - - // Send AI response - let sentMessage; - try { - sentMessage = await message.reply({ - content: `${trimmedResponse}`, - failIfNotExists: true, - }); - } catch (e) { - console.log(e); - sentMessage = await message.channel.send({ - content: `\`\`\`\n${message.author.username}: ${message.content}\n\`\`\`\n\n${trimmedResponse}`, - }); - } - if (response.includes("!gen")) { - selfImageGen(message, response, sentMessage); - } - - // tts! - if (message.member.voice.channel) { - tts(message, trimmedResponse); - } - } catch (error) { - console.error(error); - return message.reply(`❌ Error! Yell at arti.`); - } -}); - -async function imageRecognition(message) { - if (message.attachments.size > 0) { - let imageDetails = ""; - - const res = await fetch(message.attachments.first().url); - const blob = await res.arrayBuffer(); - const input = { - image: [...new Uint8Array(blob)], - prompt: "Generate a caption for this image", - max_tokens: 256, - }; - - try { - let response = await fetch( - `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT}/ai/run/@cf/llava-hf/llava-1.5-7b-hf`, - { - method: "POST", - headers: { - Authorization: `Bearer ${process.env.CF_TOKEN}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(input), - } - ); - response = await response.json(); - - imageDetails += `Attached: image of${String( - response.result.description - ).toLowerCase()}\n`; - } catch (error) { - console.error(error); - return message.reply(`❌ Error in image recognition! Try again later.`); - } - - return imageDetails; - } else { - return ""; - } -} - -async function selfImageGen(message, response, sentMessage) { - let parts = response.split("!gen"); - - let partBeforeGen = parts[0].trim(); - let partAfterGen = parts[1].trim().replace("[", "").replace("]", ""); - - try { - let response = await fetch( - `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ACCOUNT}/ai/run/@cf/lykon/dreamshaper-8-lcm`, - { - method: "POST", - headers: { - Authorization: `Bearer ${process.env.CF_TOKEN}`, - }, - body: JSON.stringify({ - prompt: partAfterGen, - }), - } - ); - response = await response.arrayBuffer(); - const imageBuffer = Buffer.from(response); - sentMessage.edit({ content: partBeforeGen, files: [imageBuffer] }); - } catch (error) { - console.error(error); - return message.reply(`❌ Error in image generation! Try again later.`); - } -} - -async function tts(message, text) { - const tts = new MsEdgeTTS(); - await tts.setMetadata( - "en-US-AnaNeural", - MsEdgeTTS.OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3 - ); - const filePath = await tts.toFile("./temp/audio.mp3", text); - - const channel = message.member.voice.channel; - - const connection = joinVoiceChannel({ - channelId: channel.id, - guildId: channel.guild.id, - adapterCreator: channel.guild.voiceAdapterCreator, - }); - - const player = createAudioPlayer(); - const resource = createAudioResource(fs.createReadStream(filePath)); - - connection.subscribe(player); - player.play(resource); - - player.on("error", (error) => { - console.error(`Audio Error: ${error.message}`); - }); -} - -client.login(process.env.DISCORD); diff --git a/package.json b/package.json index 8b89842..2cc5687 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,33 @@ { -<<<<<<< HEAD - "$schema": "https://json.schemastore.org/package.json", - "name": "SpongeChat", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "lint": "prettier --check . && eslint --ext .js,.mjs,.cjs --format=pretty src", - "format": "prettier --write . && eslint --ext .js,.mjs,.cjs --fix --format=pretty src", - "start": "node --require dotenv/config src/index.js", - "deploy": "node --require dotenv/config src/util/deploy.js" - }, - "dependencies": { - "@discordjs/core": "^1.2.0", - "discord.js": "^14.15.0", - "dotenv": "^16.3.1" - }, - "devDependencies": { - "eslint": "^8.53.0", - "eslint-config-neon": "^0.1.57", - "eslint-formatter-pretty": "^5.0.0", - "prettier": "^3.1.0", - "zod": "^3.22.4" - } -} -======= - "name": "spongegpt", - "version": "1.32.1", - "description": "custom AI chatbot for discord", - "main": "index.js", + "$schema": "https://json.schemastore.org/package.json", + "name": "spongechat", + "version": "2.0.0", + "private": true, "type": "module", "scripts": { - "start": "node index.js", - "dev": "nodemon index.js" + "lint": "prettier --check . && eslint --ext .js,.mjs,.cjs --format=pretty src", + "format": "prettier --write . && eslint --ext .js,.mjs,.cjs --fix --format=pretty src", + "start": "node --require dotenv/config src/index.js", + "deploy": "node --require dotenv/config src/util/deploy.js" + }, + "_moduleAliases": { + "@": "./src", + "@util": "./src/util", + "@events": "./src/events", + "@commands": "./src/commands" }, - "author": "artifish", - "license": "AGPL-3.0", "dependencies": { - "@discordjs/voice": "^0.17.0", - "discord.js": "^14.15.2", - "dotenv": "^16.4.5", - "ffmpeg-static": "^5.2.0", - "libsodium-wrappers": "^0.7.13", - "luxon": "^3.4.4", - "msedge-tts": "^1.3.4", - "node-fetch": "^3.3.2", - "zustand": "^4.5.2" + "@discordjs/core": "^1.1.0", + "@redis/json": "^1.0.6", + "discord.js": "^14.14.0", + "dotenv": "^16.3.1", + "module-alias": "^2.2.3", + "redis": "^4.6.13" }, "devDependencies": { - "nodemon": "^3.1.0" + "eslint": "^8.53.0", + "eslint-config-neon": "^0.1.57", + "eslint-formatter-pretty": "^5.0.0", + "prettier": "^3.0.3" } } ->>>>>>> efa92c0aa9ea26aceb1434c9101bfd9a0666be65 diff --git a/src/commands/correctperms.js b/src/commands/correctperms.js new file mode 100644 index 0000000..457f0e0 --- /dev/null +++ b/src/commands/correctperms.js @@ -0,0 +1,41 @@ +import { PermissionFlagsBits } from 'discord.js'; +import { whitelisted } from '../util/helpers.js'; + +/** @type {import('./index.js').Command} */ +export default { + data: { + name: 'correctperms', + description: 'Correct permissions required for this application (threads).', + }, + async execute(interaction) { + // if (!whitelisted.includes(interaction.user.id)) { + if (true) { + void interaction.reply({ + content: "You aren't allowed to use this command - only those on the whitelist are eligible to.", + ephemeral: true, + }); + return; + } + + await interaction.deferReply({ ephemeral: true }); + + const guild = interaction.guild; + + if (guild.roles.everyone.permissions.has('SendMessagesInThreads')) { + void interaction.editReply({ content: 'All required permissions have already been applied to @everyone.' }); + return; + } + + await guild.roles.everyone + .setPermissions([...guild.roles.everyone.permissions, PermissionFlagsBits.SendMessagesInThreads]) + .catch(async (error) => { + console.log('Error: ' + error); + await interaction.editReply({ content: "The permissions of the role couldn't be set." }); + }) + .then(async () => { + await interaction.editReply({ content: 'Yes!' }); + }); + + // + }, +}; diff --git a/src/commands/index.js b/src/commands/index.js index a0ba2e0..ea7dbf2 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -1,5 +1,3 @@ -import { z } from 'zod'; - /** * Defines the structure of a command. * @@ -8,18 +6,16 @@ import { z } from 'zod'; * @property {(interaction: import('discord.js').CommandInteraction) => Promise | void} execute The function to execute when the command is called */ -/** - * Defines the schema for a command - */ -export const schema = z.object({ - data: z.record(z.any()), - execute: z.function(), -}); - /** * Defines the predicate to check if an object is a valid Command type. * * @type {import('../util/loaders.js').StructurePredicate} * @returns {structure is Command} */ -export const predicate = (structure) => schema.safeParse(structure).success; +export const predicate = (structure) => + Boolean(structure) && + typeof structure === 'object' && + 'data' in structure && + 'execute' in structure && + typeof structure.data === 'object' && + typeof structure.execute === 'function'; diff --git a/src/commands/ping.js b/src/commands/ping.js index 8a30f80..cb5e2e9 100644 --- a/src/commands/ping.js +++ b/src/commands/ping.js @@ -5,6 +5,6 @@ export default { description: 'Ping!', }, async execute(interaction) { - await interaction.reply('Pong!'); + await interaction.reply("It's working!"); }, }; diff --git a/src/events/index.js b/src/events/index.js index 52365ee..57b2c6e 100644 --- a/src/events/index.js +++ b/src/events/index.js @@ -1,5 +1,3 @@ -import { z } from 'zod'; - /** * Defines the structure of an event. * @@ -10,20 +8,16 @@ import { z } from 'zod'; * @property {boolean} [once] Whether or not the event should only be listened to once */ -/** - * Defines the schema for an event. - * - */ -export const schema = z.object({ - name: z.string(), - once: z.boolean().optional().default(false), - execute: z.function(), -}); - /** * Defines the predicate to check if an object is a valid Event type. * * @type {import('../util/loaders').StructurePredicate} * @returns {structure is Event} */ -export const predicate = (structure) => schema.safeParse(structure).success; +export const predicate = (structure) => + Boolean(structure) && + typeof structure === 'object' && + 'name' in structure && + 'execute' in structure && + typeof structure.name === 'string' && + typeof structure.execute === 'function'; diff --git a/src/events/messageCreate.js b/src/events/messageCreate.js new file mode 100644 index 0000000..146666a --- /dev/null +++ b/src/events/messageCreate.js @@ -0,0 +1,41 @@ +import { ChannelType, Events, ThreadAutoArchiveDuration } from 'discord.js'; +// import { Environment } from '../util/helpers.js'; + +// const env = new Environment(); + +const callTextChannel = async ({ client, message }) => { + if (!message?.mentions.has(client.user?.id)) return; + + /*const thread = await message.startThread({ + name: `${new Date().toISOString()}`, + autoArchiveDuration: ThreadAutoArchiveDuration.OneHour, + reason: `Requested by ${message.author.id}`, + }); + + await thread.members.add(message.author.id); + + thread.send('It works?'); + */ +}; + +const callThreadChannel = async ({ client, message }) => { + const channel = message.channel; + message.reply('herro').catch(() => null); +}; + +/** @type {import('./index.js').Event} */ +export default { + name: Events.MessageCreate, + // once: false, + async execute(message) { + const client = message.client; + if (message.member.id === client.user.id) return; + if (message.channel.type === ChannelType.GuildText) { + void callTextChannel({ client, message }); + } + + if (message.channel.type === ChannelType.PublicThread) { + // void callThreadChannel({ client, message }); + } + }, +}; diff --git a/src/events/ready.js b/src/events/ready.js index 8b93cfd..0a8fad0 100644 --- a/src/events/ready.js +++ b/src/events/ready.js @@ -1,10 +1,24 @@ import { Events } from 'discord.js'; +import { Environment } from '../util/helpers.js'; +import { createClient } from 'redis'; + +const env = new Environment(); /** @type {import('./index.js').Event} */ export default { name: Events.ClientReady, once: true, async execute(client) { + if (env.getRuntimeScenario() === 'development') console.log('! Running in development'); + + const kv = await createClient({ + url: process.env.KV_URI, + }) + .on('error', (e) => console.log(`KV Client Error: `, e)) + .on('ready', () => console.log(`KV Client: connected`)) + .connect(); + client.kv = kv; + console.log(`Ready! Logged in as ${client.user.tag}`); }, }; diff --git a/src/index.js b/src/index.js index 71d61e6..b4c7f59 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,17 @@ import process from 'node:process'; import { URL } from 'node:url'; import { Client, GatewayIntentBits } from 'discord.js'; +import { cmdRollout } from './util/deploy.js'; import { loadCommands, loadEvents } from './util/loaders.js'; import { registerEvents } from './util/registerEvents.js'; // Initialize the client -const client = new Client({ intents: [GatewayIntentBits.Guilds] }); +const client = new Client({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages], +}); + +// Deploy commands +await cmdRollout(); // Load the events and commands const events = await loadEvents(new URL('events/', import.meta.url)); @@ -15,4 +21,4 @@ const commands = await loadCommands(new URL('commands/', import.meta.url)); registerEvents(commands, events, client); // Login to the client -void client.login(process.env.DISCORD); +void client.login(process.env.DISCORD_TOKEN); diff --git a/src/util/deploy.js b/src/util/deploy.js index 7120c4a..a672695 100644 --- a/src/util/deploy.js +++ b/src/util/deploy.js @@ -2,14 +2,26 @@ import process from 'node:process'; import { URL } from 'node:url'; import { API } from '@discordjs/core/http-only'; import { REST } from 'discord.js'; +import { Environment } from './helpers.js'; import { loadCommands } from './loaders.js'; -const commands = await loadCommands(new URL('../commands/', import.meta.url)); -const commandData = [...commands.values()].map((command) => command.data); +export const cmdRollout = async () => { + const env = new Environment(); -const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); -const api = new API(rest); + const commands = await loadCommands(new URL('../commands/', import.meta.url)); + const commandData = [...commands.values()].map((command) => command.data); -const result = await api.applicationCommands.bulkOverwriteGlobalCommands(process.env.APPLICATION_ID, commandData); + const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); + const api = new API(rest); -console.log(`Successfully registered ${result.length} commands.`); + const result = + env.getRuntimeScenario() === 'development' + ? await api.applicationCommands.bulkOverwriteGuildCommands( + process.env.APPLICATION_ID, + process.env.DEV_GUILD, + commandData, + ) + : await api.applicationCommands.bulkOverwriteGlobalCommands(process.env.APPLICATION_ID, commandData); + + console.log(`Successfully registered ${result.length} commands.`); +}; diff --git a/src/util/helpers.js b/src/util/helpers.js new file mode 100644 index 0000000..237edfb --- /dev/null +++ b/src/util/helpers.js @@ -0,0 +1,30 @@ +import process from 'node:process'; + +export const whitelisted = ['181944866987704320', '532053122017787924']; + +export class Environment { + constructor({ register } = {}) { + this.register = register || process.env; + } + + getRuntimeScenario() { + const useCase = (this.register.NODE_ENV || 'PRODUCTION').toLowerCase(); + + switch (useCase) { + case 'development': + return 'development'; + + case 'dev': + return 'development'; + + case 'staging': + return 'staging'; + + case 'stage': + return 'staging'; + + default: + return 'production'; + } + } +} diff --git a/src/util/loaders.js b/src/util/loaders.js index c3c4375..621134f 100644 --- a/src/util/loaders.js +++ b/src/util/loaders.js @@ -55,9 +55,7 @@ export async function loadStructures(dir, predicate, recursive = true) { const structure = (await import(`${dir}/${file}`)).default; // If the structure is a valid structure, add it - if (predicate(structure)) { - structures.push(structure); - } + if (predicate(structure)) structures.push(structure); } return structures; diff --git a/.eslintrc.json b/unused.eslintrc.json similarity index 100% rename from .eslintrc.json rename to unused.eslintrc.json