diff --git a/commands/chat.js b/commands/chat.js index be2cbb9..c5cebf4 100644 --- a/commands/chat.js +++ b/commands/chat.js @@ -14,34 +14,34 @@ module.exports = { .setRequired(true)) .addNumberOption(option => option.setName('temperature') - .setDescription('Set the temperature, 0 = repetitive, 1 = random') + .setDescription('Set the temperature, 0 = repetitive, 1 = random; Defaults to 0') .setRequired(false)) .addNumberOption(option => option.setName('tokens') .setDescription('The max amount of tokens to be spent') .setRequired(false)), example: "chat [tell me a story] [0.07] []", // Need to figure out the tokens - isPrivileged: true, + isPrivileged: false, requiresTokens: true, + defaultTokenUsage: 100, + deferInitialReply: true, async execute(interaction) { - // Needs middleware for payment + try { + var params = {}; + var promptText = interaction.options.getString('prompt'); + var temperature = interaction.options.getNumber('temperature'); + var maxTokens = interaction.options.getNumber('tokens') ?? this.defaultTokenUsage; - try { - var params = {}; - var prompt = encodeURIComponent(interaction.options.getString('prompt').join(" ")); - var temperature = interaction.options.getNumber('temperature'); - var maxTokens = interaction.options.getNumber('tokens'); + if (temperature) params._temperature = temperature; + if (maxTokens) params._max_tokens = maxTokens; + + var gptResponse = await libCore.getChat(promptText, params); + await interaction.reply(`${interaction.member.user} ${gptResponse}`); - if (temperature) params._temperature = temperature; - if (maxTokens) params._max_tokens = maxTokens; - - var gptResponse = await prompt.libCore.getChat(prompt, params); - await interaction.reply(`${interaction.author.username} ${gptResponse}`); - - // Needs reply code to reply to the generation - }catch(err){ - log.ERROR(err) - //await interaction.reply(err.toString()); - } + // Needs reply code to reply to the generation + }catch(err){ + log.ERROR(err) + //await interaction.reply(err.toString()); + } } }; \ No newline at end of file diff --git a/events/interactionCreate.js b/events/interactionCreate.js index 3162603..47efcdc 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -1,5 +1,6 @@ const { Events } = require('discord.js'); -const discordAuth = require('../middleware/discordAuthorization'); +const { authorizeCommand } = require('../middleware/discordAuthorization'); +const { authorizeTokenUsage } = require('../middleware/balanceAuthorization'); const { DebugBuilder } = require("../utilities/debugBuilder"); const log = new DebugBuilder("server", "interactionCreate"); @@ -18,17 +19,19 @@ module.exports = { log.DEBUG(`${interaction.member.user} is running '${interaction.commandName}'`); - await discordAuth.authorizeCommand(interaction, command, () => { - try { - command.execute(interaction); - } catch (error) { - log.ERROR(error); - if (interaction.replied || interaction.deferred) { - interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true }); - } else { - interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); + await authorizeCommand(interaction, command, async () => { + await authorizeTokenUsage(interaction, command, async () => { + try { + command.execute(interaction); + } catch (error) { + log.ERROR(error); + if (interaction.replied || interaction.deferred) { + interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true }); + } else { + interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); + } } - } + }) }); }, }; \ No newline at end of file diff --git a/libCore.js b/libCore.js index 8840c4d..ccbed74 100644 --- a/libCore.js +++ b/libCore.js @@ -5,9 +5,9 @@ let parser = new Parser(); const { FeedStorage, PostStorage } = require("./libStorage"); const libUtils = require("./libUtils"); const { createHash } = require("crypto"); +const { all } = require('axios'); const { DebugBuilder } = require("./utilities/debugBuilder"); -const { all } = require('axios'); const log = new DebugBuilder("server", "libCore"); /* OpenAI config @@ -267,7 +267,10 @@ exports.getSlang = async function (question) { * @param {*} param1 Default parameters can be modified * @returns */ -exports.getChat = async function (_prompt, { _model = "text-davinci-003", _temperature = 0, _max_tokens = 100 }) { +exports.getChat = async (_prompt, { _model = "text-davinci-003", _temperature = 0, _max_tokens = 100 }) => { + log.DEBUG("Getting chat with these properties: ", _prompt, _model, _temperature, _max_tokens) + return + /* const response = await openai.createCompletion({ model: _model, prompt: _prompt, @@ -275,9 +278,9 @@ exports.getChat = async function (_prompt, { _model = "text-davinci-003", _tempe max_tokens: _max_tokens }); - var responseData = response.data.choices[0].text; return responseData; + */ } /** diff --git a/libStorage.js b/libStorage.js index e8f447e..dc3c223 100644 --- a/libStorage.js +++ b/libStorage.js @@ -12,6 +12,9 @@ const mysql = require("mysql"); const rssFeedsTable = process.env.DB_RSS_FEEDS_TABLE; const rssPostsTable = process.env.DB_RSS_POSTS_TABLE; +const accountsTable = process.env.DB_ACCOUNTS_TABLE; +const transactionsTable = process.env.DB_TRANSACTIONS_TABLE; +const pricingTable = process.env.DB_PRICING_TABLE; // Helper Functions // Function to run and handle SQL errors @@ -30,6 +33,15 @@ function runSQL(sqlQuery, connection, callback = (err, rows) => { }) } +/** + * Return a formatted date time string from now for MySQL + * + * @returns Date string for now formatted for MySQL + */ +function returnMysqlTime(){ + return new Date().toISOString().slice(0, 19).replace('T', ' '); +} + class Storage { constructor(_dbTable) { this.connection = mysql.createPool({ @@ -142,6 +154,63 @@ class Storage { } } +exports.UserStorage = class UserStorage extends Storage { + constructor() { + super(accountsTable); + } + + /** + * Save a new account to the database + * @param {*} _discordAccountId The Discord ID the the user + * @param {*} callback The callback to be sent + * @callback Error|Array|* + */ + saveAccount(_discordAccountId, callback){ + const sqlQuery = `INSERT INTO ${this.dbTable} (discord_account_id, balance) VALUES ('${_discordAccountId}', ${0});`; + + log.DEBUG(`Adding new entry with SQL query: '${sqlQuery}'`) + + runSQL(sqlQuery, this.connection, (err, rows) => { + if (err) return callback(err, undefined); + if (rows?.affectedRows > 0) return callback(undefined, rows); + return callback(undefined, undefined); + }) + } + + checkBalance(_tokensToBeUsed, _account_id, callback) { + this.getRecordBy('account_id', _account_id, (err, record) => { + if (err) return callback(err, undefined); + + if (record?.balance && record.balance > _tokensToBeUsed) return callback(undefined, true); + else{ + return callback(undefined, false); + } + }) + } + + updateBalance(){ + + } +} +/* +exports.TransactionStorage = class TransactionStorage extends Storage { + constructor() { + super(transactionsTable); + } + + createTransaction(transaction, callback){ + const sqlQuery = `INSERT INTO ${this.dbTable} () VALUES ('${}');`; + + log.DEBUG(`Adding new entry with SQL query: '${sqlQuery}'`) + + runSQL(sqlQuery, this.connection, (err, rows) => { + if (err) return callback(err, undefined); + if (rows[0]?.id) return callback(undefined, rows); + return callback(undefined, undefined); + }) + } +} +*/ exports.FeedStorage = class FeedStorage extends Storage { constructor() { super(rssFeedsTable); @@ -329,7 +398,7 @@ exports.PostStorage = class PostStorage extends Storage { } savePost(_postObject, callback){ - const tempCreationDate = new Date().toISOString().slice(0, 19).replace('T', ' '); + const tempCreationDate = returnMysqlTime(); log.DEBUG("Saving Post Object:", _postObject); if (!_postObject?.guid || !_postObject?.link) { return callback(new Error("Post object malformed, check the object before saving it"), undefined) diff --git a/middleware/balanceAuthorization.js b/middleware/balanceAuthorization.js new file mode 100644 index 0000000..da07404 --- /dev/null +++ b/middleware/balanceAuthorization.js @@ -0,0 +1,97 @@ +// To ensure the command caller has, 1. an account and 2. has enough balance for the request +// The pricing table needs to be in the DB so it can be updated via API + +const { DebugBuilder } = require("../utilities/debugBuilder"); +const log = new DebugBuilder("server", "balanceAuthorizations"); + +const { UserStorage } = require("../libStorage"); +const userStorage = new UserStorage(); + +/** + * Check to see if the discord ID has an account associated with it + * @param {*} _discordAccountId The discord account to look for + * @param {*} callback The callback function to be called + * @callback false|* + */ +function checkForAccount(_discordAccountId, callback){ + userStorage.getRecordBy("discord_account_id", _discordAccountId, (err, results) => { + if (err) return callback(err, undefined); + + if (!results) return callback(undefined, false); + return callback(undefined, results); + }) +} + +/** + * Create an account from a discord ID + * @param {*} _discordAccountId + * @param {*} callback + */ +function createAccount(_discordAccountId, callback){ + if (!_discordAccountId) return callback(new Error("No discord account specified before creation")); + userStorage.saveAccount(_discordAccountId, (err, results) => { + if (err) return callback(err, undefined); + + if (!results) return callback(new Error("No results from creating account"), undefined); + + return callback(undefined, results); + }) +} + +function verifyBalance(_tokensToBeUsed, _accountId, callback){ + userStorage.checkBalance(_tokensToBeUsed, _accountId, (err, results) => { + if (err) return callback(err, undefined); + + if(!results) return callback(undefined, false); + + return callback(undefined, true); + }) +} + +function insufficientTokensResponse(interaction){ + log.DEBUG("INSUFFICIENT TOKENS RESPONSE") + + return interaction.reply("Sorry, not enough tokens for this request."); +} + +function welcomeResponse(interaction){ + log.DEBUG("WELCOME RESPONSE") + + return interaction.reply("Hey there, you have an account now."); +} + +exports.authorizeTokenUsage = async (interaction, command, next) => { + log.DEBUG("Command requires tokens? ", command.isPrivileged) + if(!command.requiresTokens) return next(true); + + if(!interaction.member && (!interaction.options.getNumber("tokens") || !command.defaultTokenUsage)) throw new Error("No member or tokens specified before attempting to authorize"); + const memberId = interaction.member.id; + const tokensToBeUsed = interaction.options.getNumber("tokens") ?? command.defaultTokenUsage + log.DEBUG(`Authorizing ${memberId} for a purchase worth ${tokensToBeUsed} tokens`) + log.DEBUG("Checking for account associated with discord ID: ", memberId); + await checkForAccount(memberId, async (err, results) => { + if (err) throw err; + + log.DEBUG("Results from checking for account: ", results); + + // First time user is attempting transaction + if(!results){ + log.DEBUG("No account for discord ID: ", memberId); + await createAccount(memberId, (err, results) => { + if (err) throw err; + + if (results) return welcomeResponse(interaction); + }) + } else{ + // User has an account + log.DEBUG(`Account ID: ${results.account_id} found for discord ID: ${memberId}`); + await verifyBalance(tokensToBeUsed, results.account_id, async (err, isVerified) => { + if (err) throw err; + + if(!isVerified) return insufficientTokensResponse(interaction); + + return next(isVerified); + }) + } + }) +} \ No newline at end of file