diff --git a/.env.example b/.env.example index 3bd073b..7c5a77a 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ DISCORD_BOT_TOKEN= DISCORD_SUPPORT_ROLE_ID=, CONTEXT_ID= -ASKAI_CHANNEL= +ASKAI_CHANNELS= REDIS_SERVER_URL= \ No newline at end of file diff --git a/package.json b/package.json index 70c7569..57f4853 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "thirdweb-support-discord-bot", - "version": "3.2.0", + "version": "3.3.0", "description": "A self-hosted dedicated forum-based support Discord bot for the thirdweb community.", "main": "src/bot.ts", "author": "Waren Gonzaga", diff --git a/src/bot.js b/src/bot.js index 975e27a..e5a22e4 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,14 +1,15 @@ const fs = require("fs"); const path = require("node:path"); const { Client, GatewayIntentBits, Partials } = require("discord.js"); -const { serverTime } = require("./utils/core"); -const redis = require("./events/database"); - -// dot env -require("dotenv").config(); - -// discord bot environment vars -const { DISCORD_BOT_TOKEN } = process.env; +const { serverTime, localMode, debugMode } = require("./utils/core"); +const redis = require("./utils/database"); +const { + discord_bot_token, + discord_support_role, + context_id, + askai_channels +} = require("./utils/env"); +const config = require("./config.json"); // discord bot instents and partials const client = new Client({ @@ -41,10 +42,25 @@ for (const file of eventFiles) { } } -// check if the Redis is ready then log in the bot to Discord -redis.on("ready", () => { - console.log(`[${serverTime()}][log]: Redis is ready!`); - +// check if the bot is in local development mode ignoring redis connection +if (localMode()) { + // inform the admin about the mode + console.log(`[${serverTime()}][WARNING]: This bot is in local mode, this means it will not connect to Redis server.`); + if(debugMode()) { + console.log(`[${serverTime()}][INFO]: Debug mode enabled!\n======\nDetails Loaded...\n\nDISCORD SUPPORT ROLES: ${discord_support_role}\nDISCORD ASK CHANNELS: ${askai_channels}\nCONTEXT ID: ${context_id}\n\nLOCAL: ${config.local.toString()}\nDEBUG: ${config.debug.toString()}\n======`); + } // log in to Discord with your client's token - client.login(DISCORD_BOT_TOKEN); -}); + client.login(discord_bot_token); +} else { + // check if the Redis is ready then log in the bot to Discord + redis.on("ready", () => { + console.log(`[${serverTime()}][LOG]: Redis is ready!`); + + if(debugMode()) { + console.log(`[${serverTime()}][INFO]: Debug mode enabled!\n======\nDetails Loaded...\n\nDISCORD SUPPORT ROLES: ${discord_support_role}\nDISCORD ASK CHANNELS: ${askai_channels}\nCONTEXT ID: ${context_id}\n\nLOCAL: ${config.local.toString()}\nDEBUG: ${config.debug.toString()}\n======`); + } + + // log in to Discord with your client's token + client.login(discord_bot_token); + }); +} diff --git a/src/config.json b/src/config.json index 2db506c..c4c7200 100644 --- a/src/config.json +++ b/src/config.json @@ -2,9 +2,13 @@ "command_prefix": "!", "utc_offset": -8, "ai_thinking_message": "**🤖 Beep Boop Boop Beep:** thinking...", - "getting_started_ai_message": "Hello, kindly use \\`!ask\\` or \\`!askai\\` followed by your question to get started.", + "getting_started_ai_message": "Hello, kindly use `!ask` or `!askai` followed by your question to get started.", "helpful_message": "Thank you so much for your feedback!", "not_helpful_messsage": "Thank you for your valuable feedback, this will help us improve the responses of our AI assistant.\n\nIn the meantime, would you like to contact a human customer success agent? Just click the link or the button below to submit a ticket.", - "reminder_mention": "Hey there! If you need assistance, please don't hesitate to reach out on our official support platform and create a ticket: https://thirdweb.com/support. Our dedicated team is always ready to help you out. Thank you for choosing to build with us!" + "reminder_outside_channel": "You can ask me all things thirdweb in the <#1214948528334311464> channel. Just type your question after the command \\`!askai\\` or \\`!ask\\` to get started.", + "reminder_mention": "Hey there! \n\nKindly use `!ask` or `!askai` followed by your question and I'll help you the best I can.\n\nIf you need assistance, please don't hesitate to reach out on our official support platform and create a ticket: https://thirdweb.com/support. Our dedicated team is always ready to help you out. Thank you for choosing to build with us!", + "response_team_mention": "We have moved to a community driven discord support model.\n\nYou can ask me all things thirdweb in the <#1214948528334311464> channel. Use the command \\`!askai\\` or \\`!ask\\` followed by your question to get started.", + "local": false, + "debug": false } \ No newline at end of file diff --git a/src/events/database.js b/src/events/database.js deleted file mode 100644 index b64843e..0000000 --- a/src/events/database.js +++ /dev/null @@ -1,21 +0,0 @@ -const Redis = require("ioredis"); -const { serverTime } = require('../utils/core'); - -require("dotenv").config(); - -const { REDIS_SERVER_URL } = process.env; - -const redis = new Redis(REDIS_SERVER_URL); - -// log connection -redis.on("connect", () => { - console.log(`[${serverTime()}][log]: Successfully connected to Redis!`); -}); -redis.on("reconnecting", () =>{ - console.log(`[${serverTime()}][log]: Reconnecting to Redis...`); -}); -redis.on("error", (err) => () => { - console.log(`[${serverTime()}][error]: Redis Error: ${err}`); -}); - -module.exports = redis; \ No newline at end of file diff --git a/src/events/error.js b/src/events/error.js index ab42b73..ed91443 100644 --- a/src/events/error.js +++ b/src/events/error.js @@ -4,6 +4,6 @@ module.exports = { name: Events.Error, once: false, execute(error) { - console.log(`[${serverTime()}][error]: ${error}`); + console.log(`[${serverTime()}][ERROR]: ${error}`); }, }; \ No newline at end of file diff --git a/src/events/interaction.js b/src/events/interaction.js index 7f49d7c..3ad5e81 100644 --- a/src/events/interaction.js +++ b/src/events/interaction.js @@ -3,10 +3,10 @@ const { sendEmbedMessage, serverTime, CloseButtonComponent } = require("../utils/core"); -const { ContextSDK } = require("@context-labs/sdk"); -const redis = require("./database"); +const redis = require("../utils/database"); const config = require("../config.json"); -const context = new ContextSDK({}); +const context = require("../utils/ai"); +const { setQueryFeedback } = require("../utils/ai"); module.exports = { name: Events.InteractionCreate, @@ -27,17 +27,14 @@ module.exports = { if (err) { console.error(err); } else { - await context.setQueryFeedback({ - queryId: result, - helpful: true, - }); + setQueryFeedback(result, true); } }); await interaction.message.edit({ components: [] }); await redis.del(messageId); // log the feedback - console.log(`[${serverTime()}][log]: User sent a "Helpful" feedback!`); + console.log(`[${serverTime()}][LOG]: User sent a "Helpful" feedback!`); } if (interaction.customId === "not-helpful") { @@ -51,17 +48,14 @@ module.exports = { if (err) { console.error(err); } else { - await context.setQueryFeedback({ - queryId: result, - helpful: false, - }); + setQueryFeedback(result, false); } }); await interaction.message.edit({ components: [] }); await redis.del(messageId); // log the feedback - console.log(`[${serverTime()}][log]: User sent a "Not Helpful" feedback!`); + console.log(`[${serverTime()}][LOG]: User sent a "Not Helpful" feedback!`); } } } diff --git a/src/events/message.js b/src/events/message.js index 9409a1e..94c5989 100644 --- a/src/events/message.js +++ b/src/events/message.js @@ -1,21 +1,18 @@ const { Events } = require("discord.js"); const { sendEmbedMessage, + FeedbackButtonComponent, serverTime, - FeedbackButtonComponent } = require("../utils/core"); + localMode, +} = require("../utils/core"); const { version } = require("../../package.json"); const config = require("../config.json"); -const redis = require("./database"); -const { ContextSDK } = require("@context-labs/sdk"); +const redis = require("../utils/database"); +const { context, contextID } = require("../utils/ai"); +const { discord_support_role, askai_channels } = require("../utils/env"); -// discord bot env -const { - DISCORD_SUPPORT_ROLE_ID, - ASKAI_CHANNEL, - CONTEXT_ID } = process.env; -const roleIDs = DISCORD_SUPPORT_ROLE_ID.split(","); - -const context = new ContextSDK({}); +const roleIDs = discord_support_role.split(","); +const askChannelIDs = askai_channels.split(","); module.exports = { name: Events.MessageCreate, @@ -38,7 +35,7 @@ module.exports = { message.reply({ embeds: [sendEmbedMessage(`Latency is ${Date.now() - message.createdTimestamp}ms.`)], }); - console.log(`[${serverTime()}][log]: responded to ping command`); + console.log(`[${serverTime()}][LOG]: responded to ping command`); } // check version @@ -46,7 +43,7 @@ module.exports = { message.reply({ embeds: [sendEmbedMessage(`Version: ${version}`)], }); - console.log(`[${serverTime()}][log]: responded to version command in version ${version}`); + console.log(`[${serverTime()}][LOG]: responded to version command in version ${version}`); } /** @@ -64,28 +61,41 @@ module.exports = { embeds: [sendEmbedMessage(config.getting_started_ai_message)], }); } else { - if (message.channel.id === ASKAI_CHANNEL) { + if (askChannelIDs.includes(message.channel.id)) { let aiMessageLoading = await message.channel.send({ embeds: [sendEmbedMessage(config.ai_thinking_message)], }); await context.query({ - botId: CONTEXT_ID, + botId: contextID, query: question, onComplete: async (query) => { - // respond to the user with the answer from the AI - await message.channel.messages.fetch(aiMessageLoading.id).then((msg) => { - msg.edit({ - content: `Hey <@${message.author.id}> 👇`, - embeds: [ - sendEmbedMessage(`**Response:**\n${query.output.toString()}`), - ], - components: [FeedbackButtonComponent()], - }) - redis.set(msg.id, query._id); + // check if local mode is active or not + if(!localMode()) { + // respond to the user with the answer from the AI + await message.channel.messages.fetch(aiMessageLoading.id).then((msg) => { + msg.edit({ + content: `Hey <@${message.author.id}> 👇`, + embeds: [ + sendEmbedMessage(`**Response:**\n${query.output.toString()}`), + ], + components: [FeedbackButtonComponent()], + + }) + redis.set(msg.id, query._id); + }); + } else { + // respond to the user with the answer from the AI + await message.channel.messages.fetch(aiMessageLoading.id).then((msg) => { + msg.edit({ + content: `Hey <@${message.author.id}> 👇`, + embeds: [ + sendEmbedMessage(`**Response:**\n${query.output.toString()}`), + ] + }) + }); } - ); }, onError: async (error) => { @@ -109,7 +119,7 @@ module.exports = { // if the command is not from the channel message.reply({ content: `Hey <@${message.author.id}> 👇`, - embeds: [sendEmbedMessage(`You can ask me all things thirdweb in the <#${ASKAI_CHANNEL}> channel. Just type your question after the command \`!askai\` or \`!ask\` to get started.`)], + embeds: [sendEmbedMessage(config.reminder_outside_channel)], }); } } @@ -131,7 +141,7 @@ module.exports = { let mentioned = message.guild.members.cache.get(mention.users.first().id) if (mentioned.roles.cache.hasAny(...roleIDs) && !member.roles.cache.hasAny(...roleIDs)) { message.reply({ - embeds: [sendEmbedMessage(`We have moved to a community driven discord support model.\n\nYou can ask me all things thirdweb in the <#${ASKAI_CHANNEL}> channel. Use the command \`!askai\` or \`!ask\` followed by your question to get started.`)], + embeds: [sendEmbedMessage(config.response_team_mention)], }).then(msg => { setTimeout(() => msg.delete(), 60000) }) diff --git a/src/events/ready.js b/src/events/ready.js index a0c8324..763b93b 100644 --- a/src/events/ready.js +++ b/src/events/ready.js @@ -14,6 +14,6 @@ module.exports = { }); - console.log(`[${serverTime()}][online]: logged in as ${bot.user.tag} @ v${packageJSON.version}`); + console.log(`[${serverTime()}][ONLINE]: logged in as ${bot.user.tag} @ v${packageJSON.version}`); }, }; \ No newline at end of file diff --git a/src/utils/ai.js b/src/utils/ai.js new file mode 100644 index 0000000..256b30b --- /dev/null +++ b/src/utils/ai.js @@ -0,0 +1,14 @@ +const { ContextSDK } = require("@context-labs/sdk"); +const { context_id } = require("./env"); + +const context = new ContextSDK({}); +const contextID = context_id; + +const setQueryFeedback = (queryId, helpful) => { + context.setQueryFeedback({ + queryId: queryId, + helpful: helpful, + }); +} + +module.exports = { context, contextID, setQueryFeedback }; diff --git a/src/utils/core.js b/src/utils/core.js index 2b89970..fc525a1 100644 --- a/src/utils/core.js +++ b/src/utils/core.js @@ -1,7 +1,18 @@ -const { EmbedBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder } = require('discord.js'); - +const { + EmbedBuilder, + ButtonBuilder, + ButtonStyle, + ActionRowBuilder } = require('discord.js'); const moment = require('moment'); const config = require('../config.json'); +const packageJSON = require('../../package.json'); + +/** + * This function determines the current mode of the bot. + * @returns {string} The mode of the bot. + */ + +const mode = () => localMode() ? (debugMode() ? "LOCAL-DEBUG" : "LOCAL") : (debugMode() ? "BUILD-DEBUG" : "BUILD"); /** * send embed message @@ -16,7 +27,7 @@ const sendEmbedMessage = (message) => { { name: 'Need Help?', value: 'Submit a ticket here: https://thirdweb.com/support', inline: false} ) .setTimestamp() - .setFooter({ text: 'thirdweb', iconURL: 'https://ipfs.io/ipfs/QmTWMy6Dw1PDyMxHxNcmDmPE8zqFCQMfD6m2feHVY89zgu/Icon/Favicon-01.png' }); + .setFooter({ text: `thirdweb @ ${packageJSON.version} ${mode()}`, iconURL: 'https://ipfs.io/ipfs/QmTWMy6Dw1PDyMxHxNcmDmPE8zqFCQMfD6m2feHVY89zgu/Icon/Favicon-01.png' }); } /** @@ -75,10 +86,28 @@ const serverTime = () => { return moment.utc().utcOffset(config.utc_offset).format('M/DD/YYYY HH:mm:ss'); } +/** + * check if the bot is in local mode or not + * @returns {boolean} + */ +const localMode = () => { + return config.local ? true : false; +} + +/** + * check if the bot is in debug mode or not + * @returns {boolean} + */ +const debugMode = () => { + return config.debug ? true : false; +} + module.exports = { sendEmbedMessage, CloseButtonComponent, FeedbackButtonComponent, formatTime, - serverTime + serverTime, + localMode, + debugMode } \ No newline at end of file diff --git a/src/utils/database.js b/src/utils/database.js new file mode 100644 index 0000000..a97dc16 --- /dev/null +++ b/src/utils/database.js @@ -0,0 +1,22 @@ +const Redis = require("ioredis"); +const { serverTime, localMode } = require("./core"); +const { redis_server_url } = require("./env"); + +let redis; + +if (!localMode()) { + redis = new Redis(redis_server_url); + + // log connection + redis.on("connect", () => { + console.log(`[${serverTime()}][LOG]: Successfully connected to Redis!`); + }); + redis.on("reconnecting", () =>{ + console.log(`[${serverTime()}][LOG]: Reconnecting to Redis...`); + }); + redis.on("error", (err) => () => { + console.log(`[${serverTime()}][ERROR]: Redis Error: ${err}`); + }); +} + +module.exports = redis; diff --git a/src/utils/env.js b/src/utils/env.js new file mode 100644 index 0000000..ef49fa1 --- /dev/null +++ b/src/utils/env.js @@ -0,0 +1,27 @@ +/** + * This is to handle the environment variables + */ + +require("dotenv").config(); + +const { + DISCORD_BOT_TOKEN, + DISCORD_SUPPORT_ROLE_ID, + CONTEXT_ID, + ASKAI_CHANNELS, + REDIS_SERVER_URL +} = process.env; + +const discord_bot_token = DISCORD_BOT_TOKEN; +const discord_support_role = DISCORD_SUPPORT_ROLE_ID; +const context_id = CONTEXT_ID; +const askai_channels = ASKAI_CHANNELS; +const redis_server_url = REDIS_SERVER_URL; + +module.exports = { + discord_bot_token, + discord_support_role, + context_id, + askai_channels, + redis_server_url +};