diff --git a/src/@types/discord.d.ts b/src/@types/discord.d.ts index ccd37b0..219461b 100644 --- a/src/@types/discord.d.ts +++ b/src/@types/discord.d.ts @@ -1,7 +1,7 @@ import type { Client } from 'discord.js' declare module 'discord.js' { - export interface Client extends Client { - commands: Collection - } + export interface Client extends Client { + commands: Collection + } } \ No newline at end of file diff --git a/src/commands/default/help.ts b/src/commands/default/help.ts new file mode 100644 index 0000000..de8058c --- /dev/null +++ b/src/commands/default/help.ts @@ -0,0 +1,11 @@ +import { SlashCommandBuilder, CommandInteraction } from "discord.js"; +import { helpEmbed } from "../../libs/discord.js"; + +export default { + data: new SlashCommandBuilder() + .setName("help") + .setDescription("Send the help message"), + async execute(interaction: CommandInteraction) { + await interaction.reply({ embeds: [ helpEmbed() ] }); + }, +}; diff --git a/src/commands/default/ping.ts b/src/commands/default/ping.ts index fa33fb9..560a0ad 100644 --- a/src/commands/default/ping.ts +++ b/src/commands/default/ping.ts @@ -1,10 +1,10 @@ import { SlashCommandBuilder, CommandInteraction } from "discord.js"; export default { - data: new SlashCommandBuilder() - .setName("ping") - .setDescription("Replies with Pong!"), - async execute(interaction: CommandInteraction) { - await interaction.reply("Pong!"); - }, + data: new SlashCommandBuilder() + .setName("ping") + .setDescription("Replies with Pong!"), + async execute(interaction: CommandInteraction) { + await interaction.reply("Pong!"); + }, }; diff --git a/src/commands/default/quota.ts b/src/commands/default/quota.ts new file mode 100644 index 0000000..9806748 --- /dev/null +++ b/src/commands/default/quota.ts @@ -0,0 +1,25 @@ +import { SlashCommandBuilder, CommandInteraction } from "discord.js"; +import { connectToDb, getUser } from "../../libs/mysql.js"; +import { errorEmbed, quotaEmbed } from "../../libs/discord.js"; + +export default { + data: new SlashCommandBuilder() + .setName("quota") + .setDescription("Send you quota") + .setDMPermission(false), + async execute(interaction: CommandInteraction) { + await interaction.deferReply(); + + const connection = await connectToDb(); + + const user: any[] = await getUser(connection, interaction.member?.user.id ? interaction.member?.user.id : ""); + + connection.end(); + + if (!user[0]) { + return interaction.editReply({ embeds: [ errorEmbed("Try asking something to the bot before requesting your quota.") ] }); + } + + interaction.editReply({ embeds: [ quotaEmbed(user[0].quota) ] }); + }, +}; diff --git a/src/commands/singleRequests/ask.ts b/src/commands/singleRequests/ask.ts index 0e4b34b..70365dc 100644 --- a/src/commands/singleRequests/ask.ts +++ b/src/commands/singleRequests/ask.ts @@ -3,53 +3,53 @@ import { getChatResponse, MistralMessage, Models, InputPrice, OutputPrice, Retur import { User, connectToDb, addUser, getUser, incrementQuota } from "../../libs/mysql.js"; export default { - data: new SlashCommandBuilder() - .setName("ask") - .setDescription("Make a single request to mistral API") - .setDMPermission(false) - .addStringOption(option => - option.setName("prompt").setDescription("The prompt to send to the API").setRequired(true) - ), - async execute(interaction: ChatInputCommandInteraction) { - if (interaction.member?.user.id == undefined) { - return; - } + data: new SlashCommandBuilder() + .setName("ask") + .setDescription("Make a single request to mistral API") + .setDMPermission(false) + .addStringOption(option => + option.setName("prompt").setDescription("The prompt to send to the API").setRequired(true) + ), + async execute(interaction: ChatInputCommandInteraction) { + if (interaction.member?.user.id == undefined) { + return; + } - await interaction.deferReply(); + await interaction.deferReply(); - const connection = await connectToDb(); + const connection = await connectToDb(); - var user: User[] = await getUser(connection, interaction.member?.user.id); + var user: User[] = await getUser(connection, interaction.member?.user.id); - if (!user[0]) { - await addUser(connection, interaction.member?.user.username, interaction.member?.user.id); - user = await getUser(connection, interaction.member?.user.id); - } + if (!user[0]) { + await addUser(connection, interaction.member?.user.username, interaction.member?.user.id); + user = await getUser(connection, interaction.member?.user.id); + } - if (user[0].quota > 0.4) { - interaction.editReply("You have exceed your quota.") - connection.end(); - return; - } + if (user[0].quota > 0.4) { + interaction.editReply("You have exceed your quota.") + connection.end(); + return; + } - const prompt: string | null = interaction.options.getString("prompt"); + const prompt: string | null = interaction.options.getString("prompt"); - const messages: MistralMessage[] = [ - { - role: "system", - content: Prompts.default - }, - { - role: "user", - content: prompt ? prompt : "" - }, - ] + const messages: MistralMessage[] = [ + { + role: "system", + content: Prompts.default + }, + { + role: "user", + content: prompt ? prompt : "" + }, + ] - const response: ReturnedValue = await getChatResponse(messages, Models.multi_tiny); + const response: ReturnedValue = await getChatResponse(messages, Models.multi_tiny); - await incrementQuota(connection, interaction.member?.user.id, InputPrice.multi_tiny * response.promptUsage + OutputPrice.multi_tiny * response.responseUsage); - connection.end(); + await incrementQuota(connection, interaction.member?.user.id, InputPrice.multi_tiny * response.promptUsage + OutputPrice.multi_tiny * response.responseUsage); + connection.end(); - interaction.editReply(response.message); - }, + interaction.editReply(response.message); + }, }; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 03c4322..f83f783 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,21 +1,21 @@ import { Events, Interaction } from "discord.js"; export default { - name: Events.InteractionCreate, - async execute(interaction: Interaction) { - if (interaction.isChatInputCommand()) { - const command = interaction.client.commands.get(interaction.commandName); + name: Events.InteractionCreate, + async execute(interaction: Interaction) { + if (interaction.isChatInputCommand()) { + const command = interaction.client.commands.get(interaction.commandName); - if (!command) { - return console.error(`No command matching ${interaction.commandName} was found.`); - } + if (!command) { + return console.error(`No command matching ${interaction.commandName} was found.`); + } - try { - await command.execute(interaction); - } - catch (error) { - console.error(error); - } - } - }, + try { + await command.execute(interaction); + } + catch (error) { + console.error(error); + } + } + }, }; diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 1d97570..d56e219 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -4,36 +4,36 @@ import { User, connectToDb, addUser, getUser, incrementQuota } from "../libs/mys import { getMessages } from "../libs/discord.js"; export default { - name: Events.MessageCreate, - async execute(message: Message) { - if (!message.guildId && message.author.id != process.env.BOT_ID) { - const prompt: string = message.content; + name: Events.MessageCreate, + async execute(message: Message) { + if (!message.guildId && message.author.id != process.env.BOT_ID) { + const prompt: string = message.content; - const connection = await connectToDb(); + const connection = await connectToDb(); - var user: User[] = await getUser(connection, message.author.id); + var user: User[] = await getUser(connection, message.author.id); - if (!user[0]) { - await addUser(connection, message.author.username, message.author.id); - user = await getUser(connection, message.author.id); - } + if (!user[0]) { + await addUser(connection, message.author.username, message.author.id); + user = await getUser(connection, message.author.id); + } - if (user[0].quota > 0.4) { - message.reply("You have exceed your quota.") - connection.end(); - return; - } + if (user[0].quota > 0.4) { + message.reply("You have exceed your quota.") + connection.end(); + return; + } - const messages: MistralMessage[] = await getMessages(message, message.channelId, message.author.id); + const messages: MistralMessage[] = await getMessages(message, message.channelId, message.author.id); - await message.channel.sendTyping(); + await message.channel.sendTyping(); - const response: ReturnedValue = await getChatResponse(messages, Models.multi_tiny); + const response: ReturnedValue = await getChatResponse(messages, Models.multi_tiny); - await incrementQuota(connection, message.author.id, InputPrice.multi_tiny * response.promptUsage + OutputPrice.multi_tiny * response.responseUsage); - connection.end(); + await incrementQuota(connection, message.author.id, InputPrice.multi_tiny * response.promptUsage + OutputPrice.multi_tiny * response.responseUsage); + connection.end(); - message.reply(response.message); - } - }, + message.reply(response.message); + } + }, }; diff --git a/src/events/ready.ts b/src/events/ready.ts index 0c9f202..b948477 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -2,19 +2,19 @@ import { Events, Client } from "discord.js"; import { checkReset } from "../libs/quotaReset.js"; export default { - name: Events.ClientReady, - once: true, - execute(client: Client) { - console.log(`Ready! Logged in as ${client.user?.tag}`); - - client.user?.setPresence({ activities: [{ name: '/ask | Version 3.0 !', type: 3 }] }); + name: Events.ClientReady, + once: true, + execute(client: Client) { + console.log(`Ready! Logged in as ${client.user?.tag}`); + + client.user?.setPresence({ activities: [{ name: '/ask | Version 3.0 !', type: 3 }] }); - setInterval(async () => { - await checkReset(); - }, 10 * 60 * 1000); + setInterval(async () => { + await checkReset(); + }, 1000); //10 * 60 * - setInterval(async () => { - client.user?.setPresence({ activities: [{ name: '/ask | Version 3.0 !', type: 3 }] }); - }, 10 * 60 * 1000); - }, + setInterval(async () => { + client.user?.setPresence({ activities: [{ name: '/ask | Version 3.0 !', type: 3 }] }); + }, 10 * 60 * 1000); + }, }; diff --git a/src/index.ts b/src/index.ts index e510c44..cca6c2b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,11 +5,11 @@ import { Client, Collection, REST, Routes, RESTPutAPIApplicationCommandsResult, const client: Client = new Client({ intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - GatewayIntentBits.DirectMessages, - ], + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.DirectMessages, + ], partials: [ Partials.Channel, Partials.Message, @@ -24,41 +24,41 @@ async function loadCommands() { const commandFolders = fs.readdirSync(foldersPath); for (const folder of commandFolders) { - const commandsPath = `./src/commands/${folder}`; - const commandFiles = fs - .readdirSync(commandsPath) - .filter((file) => file.endsWith(".ts") || file.endsWith(".js")); + const commandsPath = `./src/commands/${folder}`; + const commandFiles = fs + .readdirSync(commandsPath) + .filter((file) => file.endsWith(".ts") || file.endsWith(".js")); - for (const file of commandFiles) { - const filePath = `./commands/${folder}/${file}`; - const command = await import(filePath.replace(".ts", ".js")); - if ("data" in command.default && "execute" in command.default) { - client.commands.set(command.default.data.name, command.default); - commands.push(command.default.data.toJSON()); - } - else { - console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); - } - } + for (const file of commandFiles) { + const filePath = `./commands/${folder}/${file}`; + const command = await import(filePath.replace(".ts", ".js")); + if ("data" in command.default && "execute" in command.default) { + client.commands.set(command.default.data.name, command.default); + commands.push(command.default.data.toJSON()); + } + else { + console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); + } + } } } async function loadEvents() { const eventsPath = "./src/events"; const eventFiles = fs - .readdirSync(eventsPath) - .filter((file) => file.endsWith(".ts") || file.endsWith(".js")); + .readdirSync(eventsPath) + .filter((file) => file.endsWith(".ts") || file.endsWith(".js")); for (const file of eventFiles) { - const filePath = `./events/${file}`; - const event = await import(filePath.replace(".ts", ".js")); + const filePath = `./events/${file}`; + const event = await import(filePath.replace(".ts", ".js")); - if (event.default.once) { - client.once(event.default.name, (...args) => event.default.execute(...args)); - } - else { - client.on(event.default.name, (...args) => event.default.execute(...args)); - } + if (event.default.once) { + client.once(event.default.name, (...args) => event.default.execute(...args)); + } + else { + client.on(event.default.name, (...args) => event.default.execute(...args)); + } } } @@ -67,18 +67,18 @@ const rest = new REST().setToken(process.env.DISCORD_TOKEN ? process.env.DISCORD (async () => { await loadCommands(); await loadEvents(); - try { - console.log(`Started refreshing ${commands.length} application (/) commands.`); + try { + console.log(`Started refreshing ${commands.length} application (/) commands.`); - const data = await rest.put( - Routes.applicationCommands(process.env.BOT_ID ? process.env.BOT_ID : ""), - { body: commands } - ); + const data = await rest.put( + Routes.applicationCommands(process.env.BOT_ID ? process.env.BOT_ID : ""), + { body: commands } + ); - console.log(`Successfully reloaded ${commands.length} application (/) commands.`); - } catch (error) { - console.error(error); - } + console.log(`Successfully reloaded ${commands.length} application (/) commands.`); + } catch (error) { + console.error(error); + } })(); client.login(process.env.DISCORD_TOKEN); diff --git a/src/libs/discord.ts b/src/libs/discord.ts index 3141368..6106dbd 100644 --- a/src/libs/discord.ts +++ b/src/libs/discord.ts @@ -1,25 +1,66 @@ -import { Events, Message, Collection } from "discord.js"; +import { Events, Message, Collection, EmbedBuilder } from "discord.js"; import { getChatResponse, MistralMessage, Models, InputPrice, OutputPrice, ReturnedValue, Prompts } from "../libs/mistralai.js"; import { User, connectToDb, addUser, getUser, incrementQuota } from "../libs/mysql.js"; -export async function getMessages(message: Message, channelid: string, userid: string): Promise { - var discordMessages = await message.channel.messages.fetch({ limit: 7 }) - discordMessages.filter((m) => m.content && (m.author.id == message.author.id || m.author.id == process.env.BOT_ID)) - discordMessages.reverse(); +export function helpEmbed() { + return new EmbedBuilder() + .setTitle("Help :") + .setDescription( +` +**Commands** - var messages: MistralMessage[] = [ - { - role: "system", - content: Prompts.default, - } - ] +- \`/help\` : display this message +- \`/ask\` : make a single request to mistralAi API +- \`/quota\` : send how many credits you have left for the month - discordMessages.forEach(discordMessage => { - messages.push({ - role: discordMessage.author.id == process.env.BOT_ID ? "assistant" : "user", - content: discordMessage.content, - }) - }) +**Other way to use the bot** + +- You can DM the bot and it will answer you and remember the 6 previous messages + +**Quota** + +- You have 0.4$ of free credits +` +) + .setFooter({ text: "Bot by @ninja_jambon."}) + .setColor("#000000"); - return messages; +} + +export function errorEmbed(error: string) { + return new EmbedBuilder() + .setTitle("Error") + .setDescription(error) + .setFooter({ text: "Bot by @ninja_jambon."}) + .setColor("#000000"); +} + +export function quotaEmbed(quota: number) { + return new EmbedBuilder() + .setTitle("Quota left") + .setDescription(`You have ${0.4 - quota}$ left this month.`) + .setFooter({ text: "Bot by @ninja_jambon."}) + .setColor("#000000"); +} + +export async function getMessages(message: Message, channelid: string, userid: string): Promise { + var discordMessages = await message.channel.messages.fetch({ limit: 7 }) + discordMessages.filter((m) => m.content && (m.author.id == message.author.id || m.author.id == process.env.BOT_ID)) + discordMessages.reverse(); + + var messages: MistralMessage[] = [ + { + role: "system", + content: Prompts.default, + } + ] + + discordMessages.forEach(discordMessage => { + messages.push({ + role: discordMessage.author.id == process.env.BOT_ID ? "assistant" : "user", + content: discordMessage.content, + }) + }) + + return messages; } diff --git a/src/libs/mistralai.ts b/src/libs/mistralai.ts index 8d32273..616534a 100644 --- a/src/libs/mistralai.ts +++ b/src/libs/mistralai.ts @@ -2,42 +2,42 @@ import MistralClient from '@mistralai/mistralai'; import "dotenv/config"; export interface MistralMessage { - role: string, - content: string, + role: string, + content: string, } export enum Models { - tiny = "open-mistral-7b", - multi_tiny = "open-mixtral-8x7b", - small = "mistral-small-latest", - medium = "mistral-medium-latest", - large = "mistral-large-latest", + tiny = "open-mistral-7b", + multi_tiny = "open-mixtral-8x7b", + small = "mistral-small-latest", + medium = "mistral-medium-latest", + large = "mistral-large-latest", } export enum InputPrice { - tiny = 0.25 / 1000000, - multi_tiny = 0.7 / 1000000, - small = 2 / 1000000, - medium = 2.7 / 1000000, - large = 8 / 1000000, + tiny = 0.25 / 1000000, + multi_tiny = 0.7 / 1000000, + small = 2 / 1000000, + medium = 2.7 / 1000000, + large = 8 / 1000000, } export enum OutputPrice { - tiny = 0.25 / 1000000, - multi_tiny = 0.7 / 1000000, - small = 6 / 1000000, - medium = 8.1 / 1000000, - large = 24 / 1000000, + tiny = 0.25 / 1000000, + multi_tiny = 0.7 / 1000000, + small = 6 / 1000000, + medium = 8.1 / 1000000, + large = 24 / 1000000, } export interface ReturnedValue { - message: string, - promptUsage: number, - responseUsage: number, + message: string, + promptUsage: number, + responseUsage: number, } export enum Prompts { - default = "You are an helpful assistant, you always answer in the language of the user.", + default = "You are an helpful assistant, you always answer in the language of the user.", } const apiKey = process.env.MISTRAL_API_KEY; @@ -45,14 +45,14 @@ const apiKey = process.env.MISTRAL_API_KEY; const client = new MistralClient(apiKey); export async function getChatResponse(messages: MistralMessage[], model: Models): Promise { - const chatResponse = await client.chat({ - model: model, - messages: messages, - }); + const chatResponse = await client.chat({ + model: model, + messages: messages, + }); - return { - message: chatResponse.choices[0].message.content, - promptUsage: chatResponse.usage.prompt_tokens, - responseUsage: chatResponse.usage.completion_tokens, - }; + return { + message: chatResponse.choices[0].message.content, + promptUsage: chatResponse.usage.prompt_tokens, + responseUsage: chatResponse.usage.completion_tokens, + }; } \ No newline at end of file diff --git a/src/libs/quotaReset.ts b/src/libs/quotaReset.ts index 8b8ecb9..69f790e 100644 --- a/src/libs/quotaReset.ts +++ b/src/libs/quotaReset.ts @@ -2,7 +2,7 @@ import * as fs from "fs"; import { connectToDb, resetQuota } from "./mysql.js"; function getLastResetDate(): number { - const data: string = fs.readFileSync("../data/lastreset.txt", "utf8"); + const data: string = fs.readFileSync("./src/data/lastreset.txt", "utf8"); return parseInt(data); } @@ -11,16 +11,16 @@ export async function checkReset() { const now = Date.now() / 1000; if (now - lastResetDate > 1000 * 60 * 60 * 24 * 30) { - fs.writeFileSync("../data/lastreset.txt", now.toString()); + fs.writeFileSync("./src/data/lastreset.txt", now.toString()); - const connection = await connectToDb(); + const connection = await connectToDb(); - await resetQuota(connection); + await resetQuota(connection); - connection.end(); + connection.end(); - return; + return; } else { - return false; + return false; } } \ No newline at end of file