Initial Emmelia merge
This commit is contained in:
6
Server/.gitignore
vendored
Normal file
6
Server/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
.env
|
||||
package-lock.json
|
||||
*.bak
|
||||
*.log
|
||||
*._.*
|
||||
15
Server/LICENSE.md
Normal file
15
Server/LICENSE.md
Normal file
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2021
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
10
Server/TODO.md
Normal file
10
Server/TODO.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## TODOs
|
||||
- ~~Create balance command for user to view their balance~~
|
||||
- ~~Create a command that explains the pricing~~
|
||||
- ~~Update welcome and insufficient replies ~~
|
||||
- ~~add a section for the help menu to show items that need tokens~~
|
||||
- add a limiter to the rss feeds to slow down the sends when multiple updates are occurring
|
||||
- add a blank .env file to the git
|
||||
- clean up logging
|
||||
- ensure documentation for functions
|
||||
- merge with Discord CnC
|
||||
@@ -1,48 +0,0 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var nodesRouter = require('./routes/nodes');
|
||||
var adminRouter = require('./routes/admin');
|
||||
|
||||
var app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
app.use(logger('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Web Interface
|
||||
app.use('/', indexRouter);
|
||||
|
||||
// Nodes API
|
||||
app.use('/nodes', nodesRouter);
|
||||
|
||||
// Admin API
|
||||
app.use('/admin', adminRouter);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use((req, res, next) => {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use((err, req, res, next) => {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var app = require('../app');
|
||||
// Debug
|
||||
const debug = require('debug')('server');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("server", "www");
|
||||
var http = require('http');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
var server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
log.DEBUG('Listening on ' + bind);
|
||||
debug("testing");
|
||||
}
|
||||
47
Server/commands/add.js
Normal file
47
Server/commands/add.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const libCore = require("../libCore.js");
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "add");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('add')
|
||||
.setDescription('Add RSS Source')
|
||||
.addStringOption(option =>
|
||||
option.setName('title')
|
||||
.setDescription('The title of the RSS feed')
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option.setName('link')
|
||||
.setDescription('The link to the RSS feed')
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option.setName('category')
|
||||
.setDescription('The category for the RSS feed *("ALL" by default")*')
|
||||
.setRequired(false)),
|
||||
example: "add [title] [https://domain.com/feed.xml] [category]",
|
||||
isPrivileged: false,
|
||||
async execute(interaction, args) {
|
||||
try {
|
||||
var title = interaction.options.getString('title');
|
||||
var link = interaction.options.getString('link');
|
||||
var category = interaction.options.getString('category');
|
||||
|
||||
if (!category) category = "ALL";
|
||||
|
||||
await libCore.addSource(title, link, category, interaction.guildId, interaction.channelId, (err, result) => {
|
||||
log.DEBUG("Result from adding entry", result);
|
||||
|
||||
if (result) {
|
||||
interaction.reply(`Adding ${title} to the list of RSS sources`);
|
||||
} else {
|
||||
interaction.reply(`${title} already exists in the list of RSS sources`);
|
||||
}
|
||||
});
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
28
Server/commands/balance.js
Normal file
28
Server/commands/balance.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "balance");
|
||||
|
||||
const { checkBalance } = require("../controllers/accountController");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('balance')
|
||||
.setDescription('Check your balance of AI tokens'),
|
||||
example: "balance",
|
||||
isPrivileged: false,
|
||||
requiresTokens: false,
|
||||
defaultTokenUsage: 0,
|
||||
deferInitialReply: false,
|
||||
async execute(interaction) {
|
||||
try{
|
||||
checkBalance(interaction.member.id, async (err, balance) => {
|
||||
if (err) throw err;
|
||||
|
||||
await interaction.reply({ content: `${interaction.member.user}, you have ${balance} tokens remaining`, ephemeral: true })
|
||||
})
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
await interaction.reply(`Sorry ${interaction.member.user}, something went wrong`);
|
||||
}
|
||||
}
|
||||
};
|
||||
27
Server/commands/category.js
Normal file
27
Server/commands/category.js
Normal file
@@ -0,0 +1,27 @@
|
||||
var libCore = require("../libCore.js");
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "categories");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('categories')
|
||||
.setDescription('Return all categories'),
|
||||
example: "categories",
|
||||
isPrivileged: false,
|
||||
async execute(interaction) {
|
||||
await libCore.getCategories(async (err, categoryResults) => {
|
||||
if (err) throw err;
|
||||
|
||||
log.DEBUG("Returned Categories: ", categoryResults);
|
||||
var categories = [];
|
||||
for (const record of categoryResults) {
|
||||
categories.push(record.category);
|
||||
}
|
||||
|
||||
await interaction.reply(
|
||||
`Categories: [${categories}]`
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
61
Server/commands/chat.js
Normal file
61
Server/commands/chat.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const { submitTextPromptTransaction } = require("../controllers/openAiController");
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "chat");
|
||||
const { EmmeliaEmbedBuilder } = require('../libUtils');
|
||||
|
||||
const COST_OF_COMMAND = 100
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('chat')
|
||||
.setDescription(`Send a text prompt to ChatGPT`)
|
||||
.addStringOption(option =>
|
||||
option.setName('prompt')
|
||||
.setDescription('The prompt to be sent to ChatGPT')
|
||||
.setRequired(true))
|
||||
.addBooleanOption(option =>
|
||||
option.setName('public')
|
||||
.setDescription("Set this to false if you would like the message to only be visible to you. *defaults to public*")
|
||||
.setRequired(false))
|
||||
.addNumberOption(option =>
|
||||
option.setName('temperature')
|
||||
.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, defaults to ${COST_OF_COMMAND}`)
|
||||
.setRequired(false)),
|
||||
example: "chat [tell me a story] [0.07] [400]", // Need to figure out the tokens
|
||||
isPrivileged: false,
|
||||
requiresTokens: true,
|
||||
defaultTokenUsage: 100,
|
||||
deferInitialReply: true,
|
||||
async execute(interaction) {
|
||||
const promptText = interaction.options.getString('prompt');
|
||||
const temperature = interaction.options.getNumber('temperature') ?? undefined;
|
||||
const maxTokens = interaction.options.getNumber('tokens') ?? undefined;
|
||||
const discordAccountId = interaction.member.id;
|
||||
try {
|
||||
submitTextPromptTransaction(promptText, temperature, maxTokens, discordAccountId, interaction, this, async (err, result) => {
|
||||
if (err) throw err;
|
||||
|
||||
const gptEmbed = new EmmeliaEmbedBuilder()
|
||||
.setColor(0x0099FF)
|
||||
.setTitle(`New GPT response`)
|
||||
.setDescription(`${interaction.member.user} sent: '${promptText}'`)
|
||||
.addFields(
|
||||
{ name: 'Generated Text', value: result.promptResult },
|
||||
)
|
||||
.addFields({ name: 'Tokens Used', value: `${result.totalTokens}`, inline: true })
|
||||
|
||||
await interaction.editReply({ embeds: [gptEmbed], ephemeral: false });
|
||||
});
|
||||
|
||||
// Needs reply code to reply to the generation
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
//await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
18
Server/commands/exit.js
Normal file
18
Server/commands/exit.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const libUtils = require("../libUtils.js");
|
||||
const discordAuth = require("../middleware/discordAuthorization");
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('exit')
|
||||
.setDescription('Exit the current application.'),
|
||||
example: "exit",
|
||||
isPrivileged: true,
|
||||
async execute(interaction) {
|
||||
// TODO - Need to add middleware for admins
|
||||
await interaction.reply(
|
||||
`Goodbye world - Disconnection imminent.`
|
||||
);
|
||||
libUtils.runAfter(process.exit, 5000);
|
||||
}
|
||||
};
|
||||
63
Server/commands/help.js
Normal file
63
Server/commands/help.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const fs = require('fs');
|
||||
const path = require('node:path');
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "help");
|
||||
|
||||
const { EmmeliaEmbedBuilder } = require("../libUtils");
|
||||
|
||||
const commandsPath = path.resolve(__dirname, '../commands'); // Resolves from either working dir or __dirname
|
||||
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('help')
|
||||
.setDescription('Display this help message'),
|
||||
example: "help",
|
||||
isPrivileged: false,
|
||||
async execute(interaction) {
|
||||
try{
|
||||
generalCommandText = "";
|
||||
paidCommandText = "";
|
||||
|
||||
for (const file of commandFiles) {
|
||||
const filePath = path.join(commandsPath, file);
|
||||
const command = require(filePath);
|
||||
|
||||
if (!command.isPrivileged){ // TODO - Need to add middleware for admins
|
||||
if (!command.requiresTokens){
|
||||
if (generalCommandText.length > 1 && generalCommandText.slice(-2) != `\n`){
|
||||
generalCommandText += `\n\n`;
|
||||
}
|
||||
|
||||
generalCommandText += `**/${command.data.name}** - *${command.data.description}*`;
|
||||
|
||||
if (command.example) generalCommandText += `\n\t\t***Usage:*** \`/${command.example}\``
|
||||
}
|
||||
else{
|
||||
if (paidCommandText.length > 1 && paidCommandText.slice(-2) != `\n`){
|
||||
paidCommandText += `\n\n`;
|
||||
}
|
||||
|
||||
paidCommandText += `**/${command.data.name}** - *${command.data.description}*`;
|
||||
|
||||
if (command.example) paidCommandText += `\n\t\t***Usage:*** \`/${command.example}\``
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const helpEmbed = new EmmeliaEmbedBuilder()
|
||||
.setColor(0x0099FF)
|
||||
.setTitle(`Help`)
|
||||
.addFields(
|
||||
{ name: 'General Commands', value: `${generalCommandText}` },
|
||||
{ name: 'Paid Commands', value: `${paidCommandText}` }
|
||||
)
|
||||
await interaction.reply({ embeds: [helpEmbed], ephemeral: true });
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
//await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
81
Server/commands/imagine.js
Normal file
81
Server/commands/imagine.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const { submitImagePromptTransaction, DALLE_COLOR } = require("../controllers/openAiController");
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "imagine");
|
||||
const { EmmeliaEmbedBuilder } = require('../libUtils');
|
||||
|
||||
const COST_OF_COMMAND = 800;
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('imagine')
|
||||
.setDescription(`Submit an image generation prompt to DALL-E`)
|
||||
.addStringOption(option =>
|
||||
option.setName('prompt')
|
||||
.setDescription('The prompt to be sent to DALL-E')
|
||||
.setRequired(true))
|
||||
.addBooleanOption(option =>
|
||||
option.setName('public')
|
||||
.setDescription("Set this to false if you would like the message to only be visible to you. *defaults to public*")
|
||||
.setRequired(false))
|
||||
.addNumberOption(option =>
|
||||
option.setName('images')
|
||||
.setDescription('The number of images you wish to generate [1 - 10] *(defaults to 1)*')
|
||||
.setRequired(false))
|
||||
.addStringOption(option =>
|
||||
option.setName('size')
|
||||
.setDescription('The size of the images to be generated *defaults to 256px*')
|
||||
.addChoices(
|
||||
{ name: '1024px - 1000 tokens', value: '1024x1024' },
|
||||
{ name: '512px - 900 tokens', value: '512x512' },
|
||||
{ name: '256px - 800 tokens', value: '256x256' },
|
||||
)
|
||||
.setRequired(false)),
|
||||
example: "imagine [the sinking of the titanic on acid] [4] [", // Need to figure out the tokens
|
||||
isPrivileged: false,
|
||||
requiresTokens: true,
|
||||
defaultTokenUsage: COST_OF_COMMAND,
|
||||
deferInitialReply: true,
|
||||
async execute(interaction) {
|
||||
const promptText = interaction.options.getString('prompt');
|
||||
const images = interaction.options.getNumber('images') ?? undefined;
|
||||
const size = interaction.options.getString('size') ?? undefined;
|
||||
const discordAccountId = interaction.member.id;
|
||||
try {
|
||||
submitImagePromptTransaction(promptText, discordAccountId, images, size, interaction, this, async (err, imageResults) => {
|
||||
if (err) throw err;
|
||||
|
||||
var dalleEmbeds = [];
|
||||
log.DEBUG("Image Results: ", imageResults)
|
||||
// Add the information post
|
||||
dalleEmbeds.push(new EmmeliaEmbedBuilder()
|
||||
.setColor(DALLE_COLOR)
|
||||
.setTitle(`New Image Result`)
|
||||
.setDescription(`${interaction.member.user} sent the prompt: '${promptText}'`)
|
||||
);
|
||||
// Add the images to the result
|
||||
const imagesInResult = Array(imageResults.results).length
|
||||
log.DEBUG("Images in the result: ", imagesInResult);
|
||||
if (imagesInResult >= 1) {
|
||||
for (const imageData of imageResults.results.data){
|
||||
const imageUrl = imageData.url;
|
||||
dalleEmbeds.push(new EmmeliaEmbedBuilder().setURL(imageUrl).setImage(imageUrl).setColor(DALLE_COLOR));
|
||||
}
|
||||
}
|
||||
// Add the information post
|
||||
dalleEmbeds.push(new EmmeliaEmbedBuilder()
|
||||
.setColor(DALLE_COLOR)
|
||||
.addFields({ name: 'Tokens Used', value: `${imageResults.totalTokens}`, inline: true })
|
||||
.addFields({ name: 'Images Generated', value: `${imagesInResult}`, inline: true })
|
||||
.addFields({ name: 'Image Size Requested', value: `${imagesInResult}`, inline: true })
|
||||
);
|
||||
await interaction.editReply({ embeds: dalleEmbeds, ephemeral: false });
|
||||
});
|
||||
|
||||
// Needs reply code to reply to the generation
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
//await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
29
Server/commands/ping.js
Normal file
29
Server/commands/ping.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "ping");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ping')
|
||||
.setDescription('Replies with your input!'),
|
||||
/*
|
||||
.addStringOption(option =>
|
||||
option.setName('input')
|
||||
.setDescription('The input to echo back')
|
||||
.setRequired(false)
|
||||
.addChoices()),
|
||||
*/
|
||||
example: "ping",
|
||||
isPrivileged: false,
|
||||
requiresTokens: false,
|
||||
defaultTokenUsage: 0,
|
||||
deferInitialReply: false,
|
||||
async execute(interaction) {
|
||||
try{
|
||||
await interaction.channel.send('**Pong.**'); // TODO - Add insults as the response to this command
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
//await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
34
Server/commands/pricing.js
Normal file
34
Server/commands/pricing.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "pricing");
|
||||
|
||||
const { EmmeliaEmbedBuilder } = require("../libUtils");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('pricing')
|
||||
.setDescription('Replies with the pricing for tokens'),
|
||||
example: "pricing",
|
||||
isPrivileged: false,
|
||||
requiresTokens: false,
|
||||
defaultTokenUsage: 0,
|
||||
deferInitialReply: false,
|
||||
async execute(interaction) {
|
||||
try{
|
||||
const pricingEmbed = new EmmeliaEmbedBuilder()
|
||||
.setColor(0x0099FF)
|
||||
.setTitle(`Emmelia's Pricing`)
|
||||
.addFields(
|
||||
{ name: 'Tokens', value: `Tokens are a shared currency that is used between all AI models. Each model is charges tokens differently however, so do keep this in mind. $1 = 45,000 tokens` },
|
||||
{ name: 'Text (ChatGPT)', value: `Tokens are used in the prompt and in the response of a generation. The max tokens will not be breached by the combined prompt and response. Keep this is mind when using text generations. Each syllable is one token. This section is 50 tokens.` },
|
||||
{ name: 'Images (DALL-E)', value: `Tokens are used for each generation, variation, and upscale. The image size also affects the amount of tokens used: 256px = 800 tokens, 512px = 900 tokens, 1024px = 1000 tokens` }
|
||||
)
|
||||
|
||||
await interaction.reply({ embeds: [pricingEmbed] });
|
||||
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
//await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
36
Server/commands/remove.js
Normal file
36
Server/commands/remove.js
Normal file
@@ -0,0 +1,36 @@
|
||||
var libCore = require("../libCore.js");
|
||||
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "remove");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('remove')
|
||||
.setDescription('Remove an RSS source by it\' title')
|
||||
.addStringOption(option =>
|
||||
option.setName('title')
|
||||
.setDescription('The title of the source to remove')
|
||||
.setRequired(true)),
|
||||
example: "remove ['Leafly']",
|
||||
isPrivileged: false,
|
||||
requiresTokens: false,
|
||||
async execute(interaction) {
|
||||
try{
|
||||
var title = interaction.options.getString("title");
|
||||
|
||||
libCore.deleteSource(title, (err, result) => {
|
||||
log.DEBUG("Result from removing entry", result);
|
||||
|
||||
if (result) {
|
||||
interaction.reply(`Removing ${title} from the list of RSS sources`);
|
||||
} else {
|
||||
interaction.reply(`${title} does not exist in the list of RSS sources`);
|
||||
}
|
||||
});
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
29
Server/commands/sources.js
Normal file
29
Server/commands/sources.js
Normal file
@@ -0,0 +1,29 @@
|
||||
var libCore = require("../libCore.js");
|
||||
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "sources");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('sources')
|
||||
.setDescription('Reply with all of the available sources'),
|
||||
example: "sources",
|
||||
isPrivileged: false,
|
||||
requiresTokens: false,
|
||||
async execute(interaction) {
|
||||
try{
|
||||
var sourceArray = libCore.getSources();
|
||||
var sourceString = "";
|
||||
|
||||
sourceArray.forEach(source => {
|
||||
sourceString +=`[${source.title}](${source.link}) \n`;
|
||||
});
|
||||
|
||||
await interaction.reply(sourceString);
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
//await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
37
Server/commands/weather.js
Normal file
37
Server/commands/weather.js
Normal file
@@ -0,0 +1,37 @@
|
||||
var libCore = require("../libCore.js");
|
||||
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "alert");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('weather')
|
||||
.setDescription('Get any current weather alerts in the state specified.')
|
||||
.addStringOption(option =>
|
||||
option.setName('state')
|
||||
.setDescription('The state to get any current weather alerts from')
|
||||
.setRequired(false)
|
||||
.addChoices()),
|
||||
example: "alert [state]",
|
||||
isPrivileged: false,
|
||||
requiresTokens: false,
|
||||
async execute(interaction) {
|
||||
try{
|
||||
var question = encodeURIComponent(interaction.options.getString("state").join(" "));
|
||||
|
||||
var answerData = await libCore.weatherAlert(question);
|
||||
answerData.forEach(feature => {
|
||||
interaction.reply(`
|
||||
${feature.properties.areaDesc}
|
||||
${feature.properties.headline}
|
||||
${feature.properties.description}
|
||||
`);
|
||||
});
|
||||
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
//await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
85
Server/controllers/accountController.js
Normal file
85
Server/controllers/accountController.js
Normal file
@@ -0,0 +1,85 @@
|
||||
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "accountController");
|
||||
|
||||
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|*
|
||||
*/
|
||||
exports.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
|
||||
*/
|
||||
exports.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);
|
||||
})
|
||||
}
|
||||
|
||||
exports.withdrawBalance = async (_withdrawAmount, _discordAccountId, callback) => {
|
||||
userStorage.updateBalance('withdraw', _withdrawAmount, _discordAccountId, async (err, result) => {
|
||||
if (err) return callback(err, undefined);
|
||||
|
||||
if(result) return callback(undefined, result);
|
||||
|
||||
return callback(undefined, undefined);
|
||||
})
|
||||
}
|
||||
|
||||
exports.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);
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Check the given account for the token balance
|
||||
*
|
||||
* @param {*} _discordAccountId
|
||||
* @param {*} callback
|
||||
*/
|
||||
exports.checkBalance = (_discordAccountId, callback) => {
|
||||
if (!_discordAccountId) return callback(new Error("No discord account given to check balance of"), undefined);
|
||||
userStorage.getRecordBy("discord_account_id", _discordAccountId, (err, accountRecord) => {
|
||||
if (err) return callback(err, undefined);
|
||||
|
||||
if (!accountRecord) return callback(new Error("No account record given"), undefined);
|
||||
|
||||
return callback(undefined, accountRecord.balance);
|
||||
})
|
||||
}
|
||||
|
||||
exports.insufficientTokensResponse = (interaction) => {
|
||||
log.DEBUG("INSUFFICIENT TOKENS RESPONSE")
|
||||
|
||||
return interaction.reply({ content: `Sorry ${interaction.member.user}, you don't have enough tokens for this purchase. Please contact your bot rep to increase your balance to have more fun playing around!`, ephemeral: true });
|
||||
}
|
||||
|
||||
exports.welcomeResponse = (interaction) => {
|
||||
log.DEBUG("WELCOME RESPONSE")
|
||||
|
||||
return interaction.reply({ content: `Hey there ${interaction.member.user}! You haven't ran any commands that require tokens before. I've gone ahead and created an account for you. When you get a chance reach out to your nearest bot rep to fill your balance to start playing around! In the meantime however, you can run \`/pricing\` to view the current pricing.`, ephemeral: true });
|
||||
}
|
||||
@@ -20,7 +20,7 @@ async function getPresetsOfOnlineNodes(callback) {
|
||||
systems[onlineNode.id] = utils.BufferToJson(onlineNode.nearbySystems);
|
||||
});
|
||||
|
||||
callback(systems);
|
||||
return callback(systems);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ async function requestNodeListenToPreset(preset, nodeId, callback) {
|
||||
"channelID": discordConfig.channelID,
|
||||
"presetName": preset
|
||||
}), (responseObject) => {
|
||||
callback(responseObject)
|
||||
return callback(responseObject)
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -46,7 +46,7 @@ async function getNodeBotStatus(nodeId, callback) {
|
||||
else {
|
||||
// Bot is free
|
||||
}
|
||||
callback(responseObject);
|
||||
return callback(responseObject);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -58,13 +58,13 @@ async function requestNodeLeaveServer(nodeId, callback) {
|
||||
mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{
|
||||
reqOptions = new requests.requestOptions("/bot/leave", "POST", nodeObject.ip, nodeObject.port);
|
||||
requests.sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => {
|
||||
callback(responseObject);
|
||||
return callback(responseObject);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Bot is free
|
||||
callback(false);
|
||||
return callback(false);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
179
Server/controllers/openAiController.js
Normal file
179
Server/controllers/openAiController.js
Normal file
@@ -0,0 +1,179 @@
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "openAiController");
|
||||
const crypto = require('crypto')
|
||||
|
||||
const { createTransaction } = require("./transactionController");
|
||||
const { authorizeTokenUsage } = require("../middleware/balanceAuthorization");
|
||||
|
||||
const { encode } = require("gpt-3-encoder")
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const configuration = new Configuration({
|
||||
organization: process.env.OPENAI_ORG,
|
||||
apiKey: process.env.OPENAI_KEY
|
||||
});
|
||||
|
||||
const openai = new OpenAIApi(configuration);
|
||||
|
||||
// Global Vars for Other functions
|
||||
exports.DALLE_COLOR = 0x34c6eb;
|
||||
exports.CHATGPT_COLOR = 0x34eb9b;
|
||||
|
||||
async function getImageGeneration(_prompt, { _images_to_generate = 1, _image_size = "256x256" }, callback){
|
||||
const validImageSizes = ["256x256", "512x512", "1024x1024"];
|
||||
|
||||
if (!_prompt) callback(new Error("No prompt given before generating image"), undefined);
|
||||
if (!validImageSizes.includes(_image_size)) callback(new Error("Image size given is not valid, valid size: ", validImageSizes));
|
||||
if (!_images_to_generate || _images_to_generate === 0 || _images_to_generate > 10) callback(new Error("Invalid image count given"));
|
||||
|
||||
// Calculate token usage?
|
||||
|
||||
log.DEBUG("Getting image generation with these properties: ", _prompt, _images_to_generate, _image_size)
|
||||
try{
|
||||
const response = await openai.createImage({
|
||||
prompt: _prompt,
|
||||
n: _images_to_generate,
|
||||
size: _image_size
|
||||
})
|
||||
|
||||
|
||||
if(!response?.data) return callback(new Error("Error in response data: ", response));
|
||||
return callback(undefined, response.data);
|
||||
} catch (err){
|
||||
log.ERROR(err);
|
||||
log.ERROR("Error when handing image model request");
|
||||
return callback(err, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response from GPT with the specified parameters
|
||||
*
|
||||
* @param {*} _prompt The text prompt to send to the model
|
||||
* @param {*} callback The callback to call with errors or results
|
||||
* @param {*} param2 Any parameters the user has changed for this request
|
||||
* @returns
|
||||
*/
|
||||
async function getTextGeneration(_prompt, callback, { _model = "text-davinci-003", _temperature = 0, _max_tokens = 100}) {
|
||||
// If the temperature is set to null
|
||||
_temperature = _temperature ?? 0;
|
||||
// If the tokens are set to null
|
||||
_max_tokens = _max_tokens ?? 100;
|
||||
|
||||
const encodedPrompt = encode(_prompt);
|
||||
const promptTokens = encodedPrompt.length;
|
||||
log.DEBUG("Tokens in prompt: ", promptTokens);
|
||||
if (promptTokens >= _max_tokens) return callback(new Error("Tokens of request are greater than the set max tokens", promptTokens, _max_tokens));
|
||||
|
||||
_max_tokens = _max_tokens - promptTokens;
|
||||
log.DEBUG("Updated max tokens: ", _max_tokens);
|
||||
|
||||
log.DEBUG("Getting chat with these properties: ", _prompt, _model, _temperature, _max_tokens)
|
||||
try{
|
||||
const response = await openai.createCompletion({
|
||||
model: _model,
|
||||
prompt: _prompt,
|
||||
temperature: _temperature,
|
||||
max_tokens: _max_tokens
|
||||
});
|
||||
if(!response?.data) return callback(new Error("Error in response data: ", response));
|
||||
return callback(undefined, response.data);
|
||||
} catch (err){
|
||||
log.ERROR("Error when handing text model request: ", err);
|
||||
return callback(err, undefined);
|
||||
}
|
||||
//var responseData = response.data.choices[0].text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use ChatGPT to generate a response
|
||||
*
|
||||
* @param {*} _prompt The use submitted text prompt
|
||||
* @param {*} param1 Default parameters can be modified
|
||||
* @returns
|
||||
*/
|
||||
exports.submitTextPromptTransaction = async (prompt, temperature, max_tokens, discord_account_id, interaction, command, callback) => {
|
||||
|
||||
getTextGeneration(prompt, (err, gptResult) => {
|
||||
if (err) callback(err, undefined);
|
||||
|
||||
// TODO - Use the pricing table to calculate discord tokens
|
||||
log.DEBUG("GPT Response", gptResult);
|
||||
const discordTokensUsed = gptResult.usage.total_tokens;
|
||||
|
||||
if (gptResult){
|
||||
createTransaction(gptResult.id, discord_account_id, discordTokensUsed, gptResult.usage.total_tokens, 1, async (err, transactionResult) => {
|
||||
if (err) callback(err, undefined);
|
||||
|
||||
if (transactionResult){
|
||||
log.DEBUG("Transaction Created: ", transactionResult);
|
||||
callback(undefined, ({ promptResult: gptResult.choices[0].text, totalTokens: discordTokensUsed}));
|
||||
}
|
||||
});
|
||||
}
|
||||
}, { _temperature: temperature, _max_tokens: max_tokens });
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to generate an image from a prompt and params and store this information in a transaction
|
||||
*
|
||||
* @param {*} prompt The prompt of the image
|
||||
* @param {*} images_to_generate The number of images to generate
|
||||
* @param {*} image_size The size of the image ["256x256" | "512x512" | "1024x1024"]
|
||||
* @param {*} callback
|
||||
*/
|
||||
exports.submitImagePromptTransaction = async (prompt, discord_account_id, images_to_generate, image_size, interaction, command, callback) => {
|
||||
let pricePerImage = 800;
|
||||
log.DEBUG(image_size)
|
||||
switch(image_size){
|
||||
case "1024x1024":
|
||||
log.DEBUG("1024 selected");
|
||||
pricePerImage = 1000;
|
||||
break;
|
||||
case "512x512":
|
||||
pricePerImage = 900;
|
||||
log.DEBUG("512 selected");
|
||||
break;
|
||||
case "256x256":
|
||||
log.DEBUG("256 selected");
|
||||
pricePerImage = 800;
|
||||
break;
|
||||
default:
|
||||
log.DEBUG("256px defaulted");
|
||||
pricePerImage = 800;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!images_to_generate) images_to_generate = 1;
|
||||
if (!image_size) image_size = "256x256";
|
||||
|
||||
totalTokensToBeUsed = pricePerImage * images_to_generate;
|
||||
|
||||
log.DEBUG("Total tokens to be used", totalTokensToBeUsed, pricePerImage, images_to_generate);
|
||||
|
||||
authorizeTokenUsage(interaction, command, totalTokensToBeUsed, (isAuthorized) => {
|
||||
if (isAuthorized) {
|
||||
getImageGeneration(prompt, {
|
||||
_image_size: image_size,
|
||||
_images_to_generate: images_to_generate
|
||||
}, (err, dalleResult) => {
|
||||
if (err) callback(err, undefined);
|
||||
|
||||
// TODO - Use the pricing table to calculate discord tokens
|
||||
log.DEBUG("DALL-E Result", dalleResult);
|
||||
|
||||
const dalleResultHash = crypto.createHash('sha1').update(JSON.stringify({ discord_account_id : prompt, images_to_generate: image_size })).digest('hex')
|
||||
|
||||
if (dalleResult){
|
||||
createTransaction(dalleResultHash, discord_account_id, totalTokensToBeUsed, totalTokensToBeUsed, 2, async (err, transactionResult) => {
|
||||
if (err) callback(err, undefined);
|
||||
|
||||
if (transactionResult){
|
||||
log.DEBUG("Transaction Created: ", transactionResult);
|
||||
callback(undefined, ({ results: dalleResult, totalTokens: totalTokensToBeUsed}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
38
Server/controllers/rssController.js
Normal file
38
Server/controllers/rssController.js
Normal file
@@ -0,0 +1,38 @@
|
||||
//Will handle updating feeds in all channels
|
||||
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "rssController");
|
||||
|
||||
const libCore = require("../libCore");
|
||||
const libUtils = require("../libUtils");
|
||||
|
||||
const refreshInterval = process.env.RSS_REFRESH_INTERVAL ?? 300000;
|
||||
|
||||
exports.RSSController = class RSSController {
|
||||
constructor(_client) {
|
||||
this.client = _client;
|
||||
}
|
||||
|
||||
async start(){
|
||||
// Wait for the refresh period before starting rss feeds, so the rest of the bot can start
|
||||
await new Promise(resolve => setTimeout(resolve, refreshInterval));
|
||||
|
||||
log.INFO("Starting RSS Controller");
|
||||
// Get initial feeds before the starting the infinite loop
|
||||
await libCore.updateFeeds(this.client);
|
||||
|
||||
while(true){
|
||||
// Wait for the refresh interval, then wait for the posts to return, then wait a quarter of the refresh interval to make sure everything is cleared up
|
||||
await new Promise(resolve => setTimeout(resolve, refreshInterval));
|
||||
await this.collectLatestPosts();
|
||||
await new Promise(resolve => setTimeout(resolve, refreshInterval / 4));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
async collectLatestPosts(){
|
||||
log.INFO("Updating sources");
|
||||
await libCore.updateFeeds(this.client)
|
||||
return;
|
||||
}
|
||||
}
|
||||
35
Server/controllers/transactionController.js
Normal file
35
Server/controllers/transactionController.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// Controller for managing transactions
|
||||
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "transactionController");
|
||||
|
||||
const { TransactionStorage } = require("../libStorage");
|
||||
const transactionStorage = new TransactionStorage();
|
||||
|
||||
const { BaseTransaction } = require("../utilities/recordHelper");
|
||||
const { withdrawBalance } = require("./accountController");
|
||||
|
||||
exports.createTransaction = async (_provider_transaction_id, _account_id, _discord_tokens_used, _provider_tokens_used, _provider_id, callback) => {
|
||||
if (!_provider_transaction_id && !_account_id && !_discord_tokens_used && !_provider_id) return callback(new Error("Invalid vars when creating transaction", {vars: [_provider_transaction_id, _account_id, _discord_tokens_used, _provider_id, callback]}))
|
||||
|
||||
const newTransaction = new BaseTransaction(_provider_transaction_id, _account_id, _discord_tokens_used, _provider_tokens_used, _provider_id, callback);
|
||||
log.DEBUG("New Transaction Object: ", newTransaction);
|
||||
withdrawBalance(newTransaction.discord_tokens_used, newTransaction.account_id, (err, withdrawResult) => {
|
||||
if (err) return callback(err, undefined);
|
||||
|
||||
if (withdrawResult){
|
||||
log.DEBUG("New withdraw result: ", withdrawResult);
|
||||
transactionStorage.createTransaction(newTransaction, async (err, transactionResult) =>{
|
||||
if (err) return callback(err, undefined);
|
||||
|
||||
if(transactionResult){
|
||||
log.DEBUG("New transaction result: ", transactionResult);
|
||||
return callback(undefined, transactionResult);
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
return callback(undefined, undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
//Config
|
||||
import { getTOKEN, getGuildID, getApplicationID } from './utilities/configHandler.js.js';
|
||||
// Commands
|
||||
import ping from './commands/ping.js';
|
||||
import join from './commands/join.js.js';
|
||||
import leave from './commands/leave.js.js';
|
||||
import status from './commands/status.js.js';
|
||||
// Debug
|
||||
import ModuleDebugBuilder from "./utilities/moduleDebugBuilder.js.js";
|
||||
const log = new ModuleDebugBuilder("bot", "app");
|
||||
// Modules
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
// Utilities
|
||||
import registerCommands from './utilities/registerCommands.js.js';
|
||||
|
||||
/**
|
||||
* Host Process Object Builder
|
||||
*
|
||||
* This constructor is used to easily construct responses to the host process
|
||||
*/
|
||||
class HPOB {
|
||||
/**
|
||||
* Build an object to be passed to the host process
|
||||
* @param command The command to that was run ("Status", "Join", "Leave", "ChgPreSet")
|
||||
* @param response The response from the command that was run
|
||||
*/
|
||||
constructor(command = "Status"||"Join"||"Leave"||"ChgPreSet", response) {
|
||||
this.cmd = command;
|
||||
this.msg = response;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the Discord client
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.GuildVoiceStates
|
||||
]
|
||||
});
|
||||
|
||||
/**
|
||||
* When the parent process sends a message, this will interpret the message and act accordingly
|
||||
*
|
||||
* DRB IPC Message Structure:
|
||||
* msg.cmd = The command keyword; Commands covered on the server side
|
||||
* msg.params = An array containing the parameters for the command
|
||||
*
|
||||
*/
|
||||
process.on('message', (msg) => {
|
||||
log.DEBUG('IPC Message: ', msg);
|
||||
const guildID = getGuilds()[0];
|
||||
|
||||
log.DEBUG("Guild Name: ", getGuildNameFromID(guildID));
|
||||
switch (msg.cmd) {
|
||||
// Check the status of the bot
|
||||
case "Status":
|
||||
log.INFO("Status command run from IPC");
|
||||
|
||||
status({guildID: guildID, callback: (statusObj) => {
|
||||
log.DEBUG("Status Object string: ", statusObj);
|
||||
if (!statusObj.voiceConnection) return process.send(new HPOB("Status", "VDISCONN"));
|
||||
}});
|
||||
break;
|
||||
|
||||
// Check the params for a server ID and if so join the server
|
||||
case "Join":
|
||||
log.INFO("Join command run from IPC");
|
||||
|
||||
join({guildID: guildID, guildObj: client.guilds.cache.get(guildID), channelID: msg.params.channelID, callback: () => {
|
||||
process.send(new HPOB("Join", "AIDS"));
|
||||
}})
|
||||
break;
|
||||
|
||||
// Check to see if the bot is in a server and if so leave
|
||||
case "Leave":
|
||||
log.INFO("Leave command run from IPC");
|
||||
|
||||
leave({guildID: guildID, callback: (response) => {
|
||||
process.send(new HPOB("Leave", response));
|
||||
}});
|
||||
break;
|
||||
|
||||
default:
|
||||
// Command doesn't exist
|
||||
log.INFO("Unknown command run from IPC");
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
// When the client is connected and ready
|
||||
client.on('ready', () =>{
|
||||
log.INFO(`${client.user.tag} is ready`)
|
||||
process.send({'msg': "INIT READY"});
|
||||
});
|
||||
|
||||
/*
|
||||
* Saved For later
|
||||
client.on('messageCreate', (message) => {
|
||||
log.DEBUG(`Message Sent by: ${message.author.tag}\n\t'${message.content}'`);
|
||||
});
|
||||
*/
|
||||
|
||||
// When a command is sent
|
||||
client.on('interactionCreate', (interaction) => {
|
||||
if (interaction.isChatInputCommand()){
|
||||
switch (interaction.commandName) {
|
||||
case "ping":
|
||||
ping(interaction);
|
||||
break;
|
||||
case "join":
|
||||
join({ interaction: interaction });
|
||||
break;
|
||||
case "leave":
|
||||
leave({ interaction: interaction });
|
||||
break;
|
||||
case "status":
|
||||
status({ interaction: interaction });
|
||||
break;
|
||||
default:
|
||||
interaction.reply({ content: 'Command not found, try one that exists', fetchReply: true })
|
||||
.then((message) => log.DEBUG(`Reply sent with content ${message.content}`))
|
||||
.catch((err) => log.ERROR(err));
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function loginBot(){
|
||||
client.login(getTOKEN());
|
||||
}
|
||||
|
||||
function getGuilds() {
|
||||
return client.guilds.cache.map(guild => guild.id)
|
||||
}
|
||||
|
||||
function getGuildNameFromID(guildID) {
|
||||
return client.guilds.cache.map((guild) => {
|
||||
if (guild.id === guildID) return guild.name;
|
||||
})[0]
|
||||
}
|
||||
|
||||
function main(){
|
||||
registerCommands(() => {
|
||||
loginBot();
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
//module.exports = client;
|
||||
@@ -1,6 +0,0 @@
|
||||
// Utilities
|
||||
import { replyToInteraction } from '../utilities/messageHandler.js.js';
|
||||
|
||||
export default function ping(interaction) {
|
||||
return replyToInteraction(interaction, "Pong! I have Aids and now you do too!");
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"TOKEN": "OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY",
|
||||
"ApplicationID": "943742040255115304",
|
||||
"GuildID": "367396189529833472",
|
||||
"DeviceID": "5",
|
||||
"DeviceName": "VoiceMeeter Aux Output (VB-Audi"
|
||||
}
|
||||
52
Server/events/interactionCreate.js
Normal file
52
Server/events/interactionCreate.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const { Events } = require('discord.js');
|
||||
const { authorizeCommand } = require('../middleware/discordAuthorization');
|
||||
const { authorizeTokenUsage } = require('../middleware/balanceAuthorization');
|
||||
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "interactionCreate");
|
||||
|
||||
module.exports = {
|
||||
name: Events.InteractionCreate,
|
||||
async execute(interaction) {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const command = interaction.client.commands.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
log.ERROR(`No command matching ${interaction.commandName} was found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
log.DEBUG(`${interaction.member.user} is running '${interaction.commandName}'`);
|
||||
|
||||
await authorizeCommand(interaction, command, async () => {
|
||||
await authorizeTokenUsage(interaction, command, undefined, async () => {
|
||||
try {
|
||||
if (command.deferInitialReply) {
|
||||
try {
|
||||
if (interaction.options.getBool('public') && interaction.options.getBool('public') == false) await interaction.deferReply({ ephemeral: true });
|
||||
else await interaction.deferReply({ ephemeral: false });
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof TypeError) {
|
||||
// The public option doesn't exist in this command
|
||||
await interaction.deferReply({ ephemeral: false });
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
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 });
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
};
|
||||
14
Server/events/messageCreate.js
Normal file
14
Server/events/messageCreate.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const { Events } = require('discord.js');
|
||||
const { authorizeCommand } = require('../middleware/discordAuthorization');
|
||||
const { authorizeTokenUsage } = require('../middleware/balanceAuthorization');
|
||||
const { linkCop } = require("../modules/linkCop");
|
||||
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "messageCreate");
|
||||
|
||||
module.exports = {
|
||||
name: Events.MessageCreate,
|
||||
async execute(interaction) {
|
||||
await linkCop(interaction);
|
||||
},
|
||||
};
|
||||
7
Server/extras/linkCopInsults.js
Normal file
7
Server/extras/linkCopInsults.js
Normal file
@@ -0,0 +1,7 @@
|
||||
exports.linkCopInsults = [
|
||||
"{%mtn_user%}, tsk tsk. Links belong here:\n '{%ref_og_msg%}'",
|
||||
"{%mtn_user%}, the channel is quite literally called 'links':\n '{%ref_og_msg%}'",
|
||||
"{%mtn_user%}. Well, well, well, if it isn't the man who's been posting links in the wrong channel.\n'{%ref_og_msg%}'",
|
||||
"{%mtn_user%}, isn't this convenient. A whole channel for links and you put links in, and you put {%ref_links%} in {%ref_og_channel%}.\n'{%ref_og_msg%}'",
|
||||
"{%mtn_user%}, that's odd. I don't recall {%ref_og_channel%} being called 'links. Maybe I misread?\n'{%ref_og_msg%}'",
|
||||
]
|
||||
149
Server/index.js
Normal file
149
Server/index.js
Normal file
@@ -0,0 +1,149 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
var http = require('http');
|
||||
|
||||
const fs = require('fs');
|
||||
require('dotenv').config();
|
||||
const { RSSController } = require("./controllers/rssController");
|
||||
const libUtils = require("./libUtils");
|
||||
const deployCommands = require("./utilities/deployCommands");
|
||||
|
||||
const { DebugBuilder } = require("./utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "index");
|
||||
|
||||
//const Discord = require('discord.js');Client, Collection, Intents
|
||||
const {
|
||||
Client,
|
||||
Events,
|
||||
Collection,
|
||||
GatewayIntentBits,
|
||||
MessageActionRow,
|
||||
MessageButton
|
||||
} = require('discord.js');
|
||||
//const client = new Discord.Client();
|
||||
|
||||
const client = new Client({
|
||||
intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds]
|
||||
});
|
||||
|
||||
prefix = process.env.PREFIX
|
||||
discordToken = process.env.TOKEN;
|
||||
rssTimeoutValue = process.env.RSS_TIMEOUT_VALUE ?? 300000;
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var nodesRouter = require('./routes/nodes');
|
||||
var adminRouter = require('./routes/admin');
|
||||
|
||||
// HTTP Server Config
|
||||
var app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
app.use(logger('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Web Interface
|
||||
app.use('/', indexRouter);
|
||||
|
||||
// Nodes API
|
||||
app.use('/nodes', nodesRouter);
|
||||
|
||||
// Admin API
|
||||
app.use('/admin', adminRouter);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use((req, res, next) => {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
var port = libUtils.normalizePort(process.env.HTTP_PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
// error handler
|
||||
app.use((err, req, res, next) => {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
/**
|
||||
* Start the HTTP background server
|
||||
*/
|
||||
async function runHTTPServer() {
|
||||
var server = http.createServer(app);
|
||||
server.listen(port);
|
||||
|
||||
server.on('error', libUtils.onError);
|
||||
|
||||
server.on('listening', () => {
|
||||
log.INFO("HTTP server started!");
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the RSS background process
|
||||
*/
|
||||
async function runRssService() {
|
||||
const rssController = new RSSController(client);
|
||||
rssController.start();
|
||||
}
|
||||
|
||||
// Discord bot config
|
||||
|
||||
// Setup commands for the Discord bot
|
||||
client.commands = new Collection();
|
||||
const commandsPath = path.join(__dirname, 'commands');
|
||||
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||
//const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
|
||||
for (const file of commandFiles) {
|
||||
const filePath = path.join(commandsPath, file);
|
||||
const command = require(filePath);
|
||||
log.DEBUG("Importing command: ", command.data.name);
|
||||
// Set a new item in the Collection
|
||||
// With the key as the command name and the value as the exported module
|
||||
client.commands.set(command.data.name, command);
|
||||
}
|
||||
|
||||
// Run when the bot is ready
|
||||
client.on('ready', () => {
|
||||
log.DEBUG(`Discord server up and running with client: ${client.user.tag}`);
|
||||
log.INFO(`Logged in as ${client.user.tag}!`);
|
||||
|
||||
// Deploy slash commands
|
||||
log.DEBUG("Deploying slash commands");
|
||||
deployCommands.deploy(client.user.id, client.guilds.cache.map(guild => guild.id));
|
||||
|
||||
log.DEBUG(`Starting HTTP Server`);
|
||||
runHTTPServer();
|
||||
|
||||
log.DEBUG("Starting RSS watcher");
|
||||
runRssService();
|
||||
});
|
||||
|
||||
// Setup any additional event handlers
|
||||
const eventsPath = path.join(__dirname, 'events');
|
||||
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
|
||||
|
||||
for (const file of eventFiles) {
|
||||
const filePath = path.join(eventsPath, file);
|
||||
const event = require(filePath);
|
||||
if (event.once) {
|
||||
client.once(event.name, (...args) => event.execute(...args));
|
||||
} else {
|
||||
client.on(event.name, (...args) => event.execute(...args));
|
||||
}
|
||||
}
|
||||
|
||||
client.login(discordToken); //Load Client Discord Token
|
||||
382
Server/libCore.js
Normal file
382
Server/libCore.js
Normal file
@@ -0,0 +1,382 @@
|
||||
const { all } = require('axios');
|
||||
const axios = require('axios');
|
||||
|
||||
const { FeedStorage, PostStorage } = require("./libStorage");
|
||||
const libUtils = require("./libUtils");
|
||||
const { DebugBuilder } = require("./utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "libCore");
|
||||
const mysql = require("mysql");
|
||||
|
||||
const UserAgent = require("user-agents");
|
||||
process.env.USER_AGENT_STRING = new UserAgent({ platform: 'Win32' }).toString();
|
||||
|
||||
log.DEBUG("Generated User Agent string:", process.env.USER_AGENT_STRING);
|
||||
|
||||
// Initiate the parser
|
||||
let Parser = require('rss-parser');
|
||||
let parser = new Parser({
|
||||
headers: {
|
||||
'User-Agent': process.env.USER_AGENT_STRING,
|
||||
"Accept": "application/rss+xml,application/xhtml+xml,application/xml"
|
||||
}
|
||||
});
|
||||
|
||||
// Setup Storage handlers
|
||||
var feedStorage = new FeedStorage();
|
||||
var postStorage = new PostStorage();
|
||||
|
||||
// Initiate a running array of objects to keep track of sources that have no feeds/posts
|
||||
/*
|
||||
var runningPostsToRemove = [{
|
||||
"{SOURCE URL}": {NUMBER OF TIMES IT'S BEEN REMOVED}
|
||||
}]
|
||||
*/
|
||||
var runningPostsToRemove = {};
|
||||
const sourceFailureLimit = process.env.SOURCE_FAILURE_LIMIT ?? 3;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} sourceURL
|
||||
*/
|
||||
exports.removeSource = function removeSource(sourceURL) {
|
||||
log.INFO("Removing source URL: ", sourceURL);
|
||||
if (!sourceURL in runningPostsToRemove) {runningPostsToRemove[sourceURL] = 1; return;}
|
||||
|
||||
if (runningPostsToRemove[sourceURL] < sourceFailureLimit) {runningPostsToRemove[sourceURL] += 1; return;}
|
||||
|
||||
feedStorage.getRecordBy('link', sourceURL, (err, record) => {
|
||||
if (err) log.ERROR("Error getting record from feedStorage", err);
|
||||
|
||||
if (!record) log.ERROR("No source returned from feedStorage");
|
||||
feedStorage.destroy(record.id, (err, results) => {
|
||||
if (err) log.ERROR("Error removing ID from results", err);
|
||||
|
||||
if (!results) log.WARN("No results from remove entry");
|
||||
|
||||
log.DEBUG("Source exceeded the limit of retries and has been removed", sourceURL);
|
||||
return;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset a source URL from deletion if the source has not already been deleted
|
||||
* @param {*} sourceURL The source URL to be unset from deletion
|
||||
* @returns {*}
|
||||
*/
|
||||
exports.unsetRemoveSource = function unsetRemoveSource(sourceURL) {
|
||||
log.INFO("Unsetting source URL from deletion (if not already deleted): ", sourceURL);
|
||||
if (!sourceURL in runningPostsToRemove) return;
|
||||
|
||||
if (runningPostsToRemove[sourceURL] > sourceFailureLimit) return delete runningPostsToRemove[sourceURL];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or updates new source url to configured storage
|
||||
* @constructor
|
||||
* @param {string} title - Title/Name of the RSS feed.
|
||||
* @param {string} link - URL of RSS feed.
|
||||
* @param {string} category - Category of RSS feed.
|
||||
*/
|
||||
exports.addSource = async (title, link, category, guildId, channelId, callback) => {
|
||||
feedStorage.create([{
|
||||
"fields": {
|
||||
"title": title,
|
||||
"link": link,
|
||||
"category": category,
|
||||
'guild_id': guildId,
|
||||
"channel_id": channelId
|
||||
}
|
||||
}], function (err, record) {
|
||||
if (err) {
|
||||
log.ERROR("Error in create:", err);
|
||||
return callback(err, undefined);
|
||||
}
|
||||
|
||||
if (!record) return callback(undefined, false);
|
||||
|
||||
log.DEBUG("Record ID:", record.getId());
|
||||
|
||||
return callback(undefined, record);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a new source url by title
|
||||
* @constructor
|
||||
* @param {string} title - Title/Name of the RSS feed.
|
||||
*/
|
||||
exports.deleteSource = function (title, callback) {
|
||||
feedStorage.getRecordBy('title', title, (err, results) => {
|
||||
if (err) return callback(err, undefined);
|
||||
|
||||
if (!results?.id) {
|
||||
log.DEBUG("No record found for title: ", title)
|
||||
return callback(undefined, undefined);
|
||||
}
|
||||
|
||||
feedStorage.destroy(results.id, function (err, deletedRecord) {
|
||||
if (err) {
|
||||
log.ERROR(err);
|
||||
return callback(err, undefined);
|
||||
}
|
||||
log.DEBUG("Deleted Record: ", deletedRecord);
|
||||
return callback(undefined, deletedRecord ?? true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update channels with new posts from sources
|
||||
*/
|
||||
exports.updateFeeds = (client) => {
|
||||
if (!client) throw new Error("Client object not passed");
|
||||
// Create a temp pool to use for all connections while updating the feed
|
||||
var tempConnection = mysql.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME,
|
||||
connectionLimit: 10
|
||||
});
|
||||
|
||||
const tempFeedStorage = new FeedStorage(tempConnection);
|
||||
const tempPostStorage = new PostStorage(tempConnection);
|
||||
|
||||
// Array of promises to wait on before closing the connection
|
||||
var recordPromiseArray = [];
|
||||
var sourcePromiseArray = [];
|
||||
|
||||
tempFeedStorage.getAllRecords(async (err, records) => {
|
||||
// Load the posts from each RSS source
|
||||
for (const source of records) {
|
||||
sourcePromiseArray.push(new Promise((resolve, reject) => {
|
||||
log.DEBUG('Record title: ', source.title);
|
||||
log.DEBUG('Record link: ', source.link);
|
||||
log.DEBUG('Record category: ', source.category);
|
||||
log.DEBUG('Record guild ID: ', source.guild_id);
|
||||
log.DEBUG('Record channel ID: ', source.channel_id);
|
||||
// Parse the RSS feed
|
||||
parser.parseURL(source.link, async (err, parsedFeed) => {
|
||||
if (err) {
|
||||
log.ERROR("Parser Error: ", runningPostsToRemove, source, err);
|
||||
// Call the wrapper to make sure the site isn't just down at the time it checks and is back up the next time
|
||||
this.removeSource(source.link);
|
||||
reject;
|
||||
}
|
||||
try {
|
||||
if (parsedFeed?.items){
|
||||
this.unsetRemoveSource(source.link);
|
||||
for (const post of parsedFeed.items.reverse()){
|
||||
recordPromiseArray.push(new Promise((recordResolve, recordReject) => {
|
||||
log.DEBUG("Parsed Source Keys", Object.keys(post), post?.title);
|
||||
log.VERBOSE("Post from feed: ", post);
|
||||
if (!post.title || !post.link) return recordReject("Missing information from the post");
|
||||
if (!post.content || !post['content:encoded']) log.WARN("There is no content for post: ", post.title);
|
||||
|
||||
post.postId = post.postId ?? post.guid ?? post.id ?? libUtils.returnHash(post.title, post.link, post.pubDate ?? Date.now());
|
||||
tempPostStorage.getRecordBy('post_guid', post.postId, (err, existingRecord) => {
|
||||
if (err) throw err;
|
||||
|
||||
log.DEBUG("Existing post record: ", existingRecord);
|
||||
if (existingRecord) return recordResolve("Existing record found for this post");
|
||||
|
||||
const channel = client.channels.cache.get(source.channel_id);
|
||||
libUtils.sendPost(post, source, channel, (err, sendResults) =>{
|
||||
if (err) throw err;
|
||||
|
||||
if (!sendResults) {
|
||||
log.ERROR("No sending results from sending a post: ", sendResults, existingRecord, post);
|
||||
return recordReject("No sending results from sending a post");
|
||||
}
|
||||
|
||||
log.DEBUG("Saving post to database: ", sendResults, post.title, source.channel_id);
|
||||
|
||||
tempPostStorage.savePost(post, (err, saveResults) => {
|
||||
if(err) throw err;
|
||||
|
||||
if (saveResults) {
|
||||
log.DEBUG("Saved results: ", saveResults);
|
||||
return recordResolve("Saved results", saveResults);
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.removeSource(source.link);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
log.ERROR("Error Parsing Feed: ", source.link, err);
|
||||
this.removeSource(source.link);
|
||||
throw err;
|
||||
}
|
||||
Promise.all(recordPromiseArray).then((values) => {
|
||||
log.DEBUG("All posts finished for: ", source.title, values);
|
||||
return resolve(source.title);
|
||||
});
|
||||
});
|
||||
}))
|
||||
}
|
||||
|
||||
// Wait for all connections to finish then close the temp connections
|
||||
|
||||
Promise.all(sourcePromiseArray).then((values) => {
|
||||
log.DEBUG("All sources finished, closing temp connections: ", values);
|
||||
tempConnection.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a state for any weather alerts
|
||||
*
|
||||
* @param {*} state The state to search for any weather alerts in
|
||||
* @returns
|
||||
*/
|
||||
exports.weatherAlert = async function (state) {
|
||||
|
||||
var answerURL = `https://api.weather.gov/alerts/active?area=${state}`;
|
||||
log.DEBUG(answerURL);
|
||||
answerData = [];
|
||||
|
||||
await axios.get(answerURL)
|
||||
.then(response => {
|
||||
response.data.features.forEach(feature => {
|
||||
answerData.push(feature);
|
||||
})
|
||||
|
||||
return answerData;
|
||||
})
|
||||
.catch(error => {
|
||||
log.DEBUG(error);
|
||||
});
|
||||
return answerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a random food recipe
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
exports.getFood = async function () {
|
||||
|
||||
var answerURL = `https://www.themealdb.com/api/json/v1/1/random.php`;
|
||||
log.DEBUG(answerURL);
|
||||
answerData = {
|
||||
text: `No answer found try using a simpler search term`,
|
||||
source: ``
|
||||
}
|
||||
await axios.get(answerURL)
|
||||
.then(response => {
|
||||
//log.DEBUG(response.data.RelatedTopics[0].Text);
|
||||
//log.DEBUG(response.data.RelatedTopics[0].FirstURL);
|
||||
|
||||
// if (response.data.meals.length != 0) {
|
||||
|
||||
answerData = {
|
||||
strMeal: `No Data`,
|
||||
strSource: `-`,
|
||||
strInstructions: `-`,
|
||||
strMealThumb: `-`,
|
||||
strCategory: `-`
|
||||
}
|
||||
|
||||
answerData = {
|
||||
strMeal: `${unescape(response.data.meals[0].strMeal)}`,
|
||||
strSource: `${unescape(response.data.meals[0].strSource)}`,
|
||||
strInstructions: `${unescape(response.data.meals[0].strInstructions)}`,
|
||||
strMealThumb: `${unescape(response.data.meals[0].strMealThumb)}`,
|
||||
strCategory: `${unescape(response.data.meals[0].strCategory)}`
|
||||
}
|
||||
// } else {
|
||||
|
||||
//}
|
||||
return answerData;
|
||||
})
|
||||
.catch(error => {
|
||||
log.DEBUG(error);
|
||||
});
|
||||
return answerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Urban dictionary for a phrase
|
||||
*
|
||||
* @param {*} question The phrase to search urban dictionary for
|
||||
* @returns
|
||||
*/
|
||||
exports.getSlang = async function (question) {
|
||||
|
||||
var answerURL = `https://api.urbandictionary.com/v0/define?term=${question}`;
|
||||
log.DEBUG(answerURL);
|
||||
slangData = {
|
||||
definition: `No answer found try using a simpler search term`,
|
||||
example: ``
|
||||
}
|
||||
await axios.get(answerURL)
|
||||
.then(response => {
|
||||
log.DEBUG(response.data.list[0]);
|
||||
|
||||
slangData = {
|
||||
definition: `${unescape(response.data.list[0].definition) ? unescape(response.data.list[0].definition) : ''}`,
|
||||
example: `${unescape(response.data.list[0].example) ? unescape(response.data.list[0].example) : ''}`,
|
||||
thumbs_down: `${unescape(response.data.list[0].thumbs_down)}`,
|
||||
thumbs_up: `${unescape(response.data.list[0].thumbs_up)}`
|
||||
}
|
||||
|
||||
return slangData;
|
||||
})
|
||||
.catch(error => {
|
||||
log.DEBUG(error);
|
||||
});
|
||||
return slangData;
|
||||
}
|
||||
|
||||
/**
|
||||
* getSources - Get the RSS sources currently in use
|
||||
* @constructor
|
||||
*/
|
||||
exports.getSources = function () {
|
||||
feedStorage.getAllRecords((err, records) => {
|
||||
if (err) throw err;
|
||||
return records;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* getQuotes - Get a random quote from the local list
|
||||
* @constructor
|
||||
*/
|
||||
exports.getQuotes = async function (quote_url) {
|
||||
|
||||
var data = [];
|
||||
await axios.get(quote_url)
|
||||
.then(response => {
|
||||
log.DEBUG(response.data[0].q);
|
||||
log.DEBUG(response.data[0].a);
|
||||
data = response.data;
|
||||
|
||||
return data;
|
||||
})
|
||||
.catch(error => {
|
||||
log.DEBUG(error);
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* getCategories - Returns feed categories
|
||||
* @constructor
|
||||
*/
|
||||
exports.getCategories = async (callback) => {
|
||||
feedStorage.getUniqueByKey("category", (err, results) => {
|
||||
if (err) return callback(err, undefined);
|
||||
|
||||
return callback(undefined, results);
|
||||
});
|
||||
}
|
||||
500
Server/libStorage.js
Normal file
500
Server/libStorage.js
Normal file
@@ -0,0 +1,500 @@
|
||||
// Customizable storage module for any mode of storage
|
||||
// Update the functions here to change the storage medium
|
||||
|
||||
// Import modules
|
||||
const { DebugBuilder } = require("./utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "libStorage");
|
||||
const { RSSSourceRecord, RSSPostRecord } = require("./utilities/recordHelper");
|
||||
|
||||
// Storage Specific Modules
|
||||
// MySQL
|
||||
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;
|
||||
|
||||
var Connection = mysql.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME,
|
||||
connectionLimit: 10
|
||||
});
|
||||
|
||||
// Helper Functions
|
||||
/**
|
||||
* Function to run and handle SQL errors
|
||||
* @param {string} sqlQuery The SQL query string
|
||||
* @param {*} connection The SQL connection to be used to query
|
||||
* @param {function} callback The callback function to be called with an error or the results
|
||||
* @param {number} _retry Set by error retry, increments the number a query has been retried to increase wait time and track a specific query
|
||||
*/
|
||||
function runSQL(sqlQuery, connection, callback = (err, rows) => {
|
||||
log.ERROR(err);
|
||||
throw err;
|
||||
}, _retry = 0) {
|
||||
// Start the MySQL Connection
|
||||
if (!connection) connection = Connection;
|
||||
connection.query(sqlQuery, (err, rows) => {
|
||||
if (err) {
|
||||
if (err.code === "EHOSTUNREACH") {
|
||||
// DB Connection is unavailable
|
||||
let retryTimeout;
|
||||
switch(_retry){
|
||||
case 0:
|
||||
retryTimeout = 30000;
|
||||
break;
|
||||
case retry < 15:
|
||||
retryTimeout = 30000 + retry * 15000;
|
||||
break;
|
||||
default:
|
||||
log.ERROR("Retried Database 15 times over, please check connection status and restart the app", sqlQuery, err);
|
||||
return callback(err, undefined);
|
||||
}
|
||||
log.WARN(`Database connection is unavailable, waiting ${ retryTimeout / 1000 } seconds...`);
|
||||
_retry += 1
|
||||
// Wait for the retry timeout before trying the query again
|
||||
setTimeout(runSQL(sqlQuery, connection, callback, _retry));
|
||||
}
|
||||
else return callback(err, undefined);
|
||||
}
|
||||
log.VERBOSE(`SQL result for query '${sqlQuery}':`, rows);
|
||||
return callback(undefined, 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, _connection) {
|
||||
this.dbTable = _dbTable;
|
||||
this.connection = _connection;
|
||||
this.validKeys = [];
|
||||
|
||||
var sqlQuery = `SHOW COLUMNS FROM ${this.dbTable};`;
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return log.ERROR("Error getting column names: ", err);
|
||||
if (rows){
|
||||
for (const validKey of rows){
|
||||
this.validKeys.push(validKey.Field);
|
||||
}
|
||||
log.VERBOSE(`Database rows for '${this.dbTable}': `, rows);
|
||||
log.DEBUG(`Keys for '${this.dbTable}': `, this.validKeys);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to delete an entry using the storage method configured
|
||||
* @param {} entryID The ID of the entry to be deleted
|
||||
* @param {function} callback The callback function to be called with the record when deleted
|
||||
*/
|
||||
destroy(entryID, callback) {
|
||||
if (!entryID) return callback(Error("No entry ID given"), undefined);
|
||||
|
||||
this.getRecordBy('id', entryID, (err, entryRecord) => {
|
||||
this.removeEntry(entryRecord.id, (err, results) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, results);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a record by a specified key
|
||||
* @param {*} key The key to search for
|
||||
* @param {*} keyValue The value of the key to search for
|
||||
* @param {*} callback The callback function
|
||||
*/
|
||||
getRecordBy(key, keyValue, callback) {
|
||||
if (!this.validKeys.includes(key)) return callback(new Error("Given key not valid", key), undefined);
|
||||
|
||||
const sqlQuery = `SELECT * FROM ${this.dbTable} WHERE ${key} = "${keyValue}"`;
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
if (rows[0]?.[key]) return callback(undefined, rows[0]);
|
||||
else return callback(undefined, false);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all records stored
|
||||
* @param {function} callback
|
||||
*/
|
||||
getAllRecords(callback) {
|
||||
log.INFO("Getting all records from: ", this.dbTable);
|
||||
const sqlQuery = `SELECT * FROM ${this.dbTable}`
|
||||
|
||||
let records = [];
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
for (const row of rows) {
|
||||
if (this.dbTable == rssFeedsTable){
|
||||
records.push(new RSSSourceRecord(row.id, row.title, row.link, row.category, row.guild_id, row.channel_id));
|
||||
}
|
||||
else {
|
||||
records.push(rows);
|
||||
}
|
||||
}
|
||||
log.VERBOSE("All records:", records);
|
||||
return callback(undefined, records);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all unique rows in the given key
|
||||
* @param {*} key
|
||||
* @param {*} callback
|
||||
*/
|
||||
getUniqueByKey(key, callback){
|
||||
log.INFO("Getting all unique values in column: ", key);
|
||||
const sqlQuery = `SELECT DISTINCT ${key} FROM ${this.dbTable}`
|
||||
|
||||
let records = [];
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
for (const row of rows) {
|
||||
if (this.dbTable == rssFeedsTable){
|
||||
records.push(new RSSSourceRecord(row.id, row.title, row.link, row.category, row.guild_id, row.channel_id));
|
||||
}
|
||||
else {
|
||||
records.push(rows);
|
||||
}
|
||||
}
|
||||
log.VERBOSE("All records:", records);
|
||||
return callback(undefined, records);
|
||||
});
|
||||
}
|
||||
|
||||
closeConnection() {
|
||||
try {
|
||||
this.connection.end();
|
||||
}
|
||||
catch (err) {
|
||||
log.ERROR("Error closing connection :", this.connection, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.UserStorage = class UserStorage extends Storage {
|
||||
constructor(connection = undefined) {
|
||||
super(accountsTable, connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check or return the balance of a given account ID
|
||||
*
|
||||
* @param {*} _tokensToBeUsed The amount of tokens to be used, set to 0 to return the balance
|
||||
* @param {*} _account_id The account ID to check or return the balance of
|
||||
* @param {*} callback
|
||||
*/
|
||||
checkBalance(_tokensToBeUsed, _account_id, callback) {
|
||||
if (!_account_id) return callback(new Error("Account not specified when checking account balance"), undefined);
|
||||
log.DEBUG("Tokens to verify against balance", _tokensToBeUsed, _account_id);
|
||||
if (!_tokensToBeUsed && !_tokensToBeUsed >= 0) return callback(new Error("Specified tokens are invalid when checking account balance"), undefined);
|
||||
this.getRecordBy('account_id', _account_id, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
|
||||
// Check to see if the account has a balance greater than what was given
|
||||
if(_tokensToBeUsed > 0){
|
||||
if (record?.balance && record.balance > _tokensToBeUsed) return callback(undefined, true);
|
||||
else return callback(undefined, false);
|
||||
}
|
||||
|
||||
return callback(undefined, record.balance)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user's account Balance
|
||||
*
|
||||
* @param {string} _updateType The type of update to make to the account [ withdraw | deposit ]
|
||||
* @param {number} _updateAmount The amount to update the account
|
||||
* @param {number} _account_id The ID of the discord account to update
|
||||
* @param {function} callback The callback function to call with the results
|
||||
* @returns Result from the SQL query or false
|
||||
*/
|
||||
updateBalance(_updateType, _updateAmount, _discord_account_id, callback){
|
||||
var sqlQuery = "";
|
||||
switch(_updateType){
|
||||
case "withdraw":
|
||||
// Code here to withdraw funds
|
||||
sqlQuery = `UPDATE ${this.dbTable} SET balance=balance-${_updateAmount} WHERE discord_account_id = ${_discord_account_id};`;
|
||||
break;
|
||||
case "deposit":
|
||||
// Code here to withdraw funds
|
||||
sqlQuery = `UPDATE ${this.dbTable} SET balance=balance+${_updateAmount} WHERE discord_account_id = ${_discord_account_id};`;
|
||||
break;
|
||||
default:
|
||||
log.ERROR('Update type not valid: ', _updateType);
|
||||
return callback(new Error("Update type not valid"));
|
||||
}
|
||||
|
||||
if(!sqlQuery) return callback(new Error("SQL Query empty"), undefined);
|
||||
|
||||
log.DEBUG("Updating Balance with SQL Query: ", sqlQuery);
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
if (!rows?.affectedRows > 0) return callback(new Error("Error updating Balance", rows), undefined);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.TransactionStorage = class TransactionStorage extends Storage {
|
||||
constructor(connection = undefined) {
|
||||
super(transactionsTable, connection);
|
||||
}
|
||||
|
||||
createTransaction(transaction, callback){
|
||||
var sqlQuery = `INSERT INTO ${this.dbTable} (transaction_id, account_id, discord_tokens_used, provider_tokens_used, provider_id, order_date) VALUES ("${transaction.transaction_id}", "${transaction.account_id}", "${transaction.discord_tokens_used}", "${transaction.provider_tokens_used}", "${transaction.provider_id}", "${returnMysqlTime()}");`;
|
||||
|
||||
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);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.FeedStorage = class FeedStorage extends Storage {
|
||||
constructor(connection = undefined) {
|
||||
super(rssFeedsTable, connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to save a new entry using the storage method configured
|
||||
* @param {Array} toBeSaved Entry or Entries to be added
|
||||
* @param {function} callback The callback function to be called with the record when saved
|
||||
*/
|
||||
create(toBeSaved, callback) {
|
||||
log.DEBUG("To be saved:", toBeSaved);
|
||||
log.DEBUG("to be saved length:", toBeSaved.length);
|
||||
// If the request was for the Feeds Table
|
||||
if (!toBeSaved[0].fields?.title) return callback(Error("No title given"), undefined);
|
||||
let newRecords = []
|
||||
for (var entry of toBeSaved) {
|
||||
entry = entry.fields;
|
||||
log.DEBUG("Entry:", entry);
|
||||
this.returnRecord(undefined, entry.title, entry.link, entry.category, entry.guild_id, entry.channel_id, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
newRecords.push(record);
|
||||
if (toBeSaved.length === 1) {
|
||||
log.DEBUG("One record to callback with:", record);
|
||||
return callback(undefined, record);
|
||||
}
|
||||
}, false) // Do not update the if it exists
|
||||
}
|
||||
if (!toBeSaved.length === 1) {
|
||||
return callback(undefined, newRecords);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if an entry exists in the storage method configured
|
||||
* @param {*} title The title of the entry to check if it exists
|
||||
* @returns {true|false|*}
|
||||
*/
|
||||
checkForTitle(title, callback) {
|
||||
if (!title) return callback(new Error("No title given when checking for title"), undefined)
|
||||
|
||||
this.getRecordBy("title", title, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the given entry to the storage medium
|
||||
* @param {Object} entryObject The entry object to be saved
|
||||
* @param {function} callback The callback to be called with either an error or undefined if successful
|
||||
*/
|
||||
saveEntry(entryObject, callback) {
|
||||
log.DEBUG("Saving entry:", entryObject);
|
||||
if (!entryObject?.title || !entryObject?.link || !entryObject?.category) {
|
||||
return callback(new Error("Entry object malformed, check the object before saving it"), undefined)
|
||||
}
|
||||
|
||||
const sqlQuery = `INSERT INTO ${this.dbTable} (title, link, category, guild_id, channel_id) VALUES ("${entryObject.title}", "${entryObject.link}", "${entryObject.category}", "${entryObject.guild_id}", "${entryObject.channel_id}");`;
|
||||
|
||||
log.DEBUG(`Adding new entry with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the given entry to the storage medium
|
||||
* @param {Object} entryObject The entry object to be saved
|
||||
* @param {function} callback The callback to be called with either an error or undefined if successful
|
||||
*/
|
||||
updateEntry(entryObject, callback) {
|
||||
let queryParams = [];
|
||||
if (!entryObject.title) return callback(new Error("No title given before updating"), undefined);
|
||||
queryParams.push(`title = "${entryObject.title}"`);
|
||||
if (!entryObject.link) return callback(new Error("No link given before updating"), undefined);
|
||||
queryParams.push(`link = "${entryObject.link}"`);
|
||||
if (entryObject.category) queryParams.push(`category = "${entryObject.category}"`);
|
||||
if (entryObject.guild_id) queryParams.push(`guild_id = "${entryObject.guild_id}"`);
|
||||
if (entryObject.channel_id) queryParams.push(`channel_id = "${entryObject.channel_id}"`);
|
||||
|
||||
let sqlQuery = `UPDATE ${this.dbTable} SET`;
|
||||
|
||||
let i = 0;
|
||||
for (const param of queryParams) {
|
||||
if (i === queryParams.length-1) {
|
||||
sqlQuery = `${sqlQuery} ${param}`
|
||||
i += 1;
|
||||
}
|
||||
else {
|
||||
sqlQuery = `${sqlQuery} ${param},`
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
sqlQuery = `${sqlQuery} WHERE title = "${entryObject.title}";`
|
||||
|
||||
log.DEBUG(`Updating entry with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given entry from the storage medium
|
||||
* @param {*} id The title of the entry to be deleted
|
||||
* @param {function} callback The callback to be called with either an error or undefined if successful
|
||||
*/
|
||||
removeEntry(id, callback) {
|
||||
if (!id) {
|
||||
return callback(new Error("No entry id given before deleting"), undefined)
|
||||
}
|
||||
|
||||
const sqlQuery = `DELETE FROM ${this.dbTable} WHERE id = "${id}";`;
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, rows[0]);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a record class for the given information, if there's no ID, it will create it
|
||||
* @param {*} _id The ID / line number of the record in the storage medium (OPT)
|
||||
* @param {*} _title The title of the record
|
||||
* @param {*} _link The link to the RSS feed
|
||||
* @param {*} _category The category of the record
|
||||
* @param {*} callback Callback function to return an error or the record
|
||||
*/
|
||||
returnRecord(_id, _title, _link, _category, _guild_id, _channel_id, callback, updateEnabled = true) {
|
||||
log.DEBUG(`Return record for these values: ID: '${_id}', Title: '${_title}', Category: '${_category}', Link: '${_link}', Guild: '${_guild_id}', Channel:'${_channel_id}', Update Enabled: `, updateEnabled)
|
||||
if (!_link && !_title && !_guild_id && !_channel_id) return callback(new Error("No link or title given when creating a record"), undefined);
|
||||
let entryObject = {
|
||||
"title": _title,
|
||||
"link": _link,
|
||||
"guild_id": _guild_id,
|
||||
"channel_id": _channel_id
|
||||
}
|
||||
if (_category) entryObject.category = _category;
|
||||
|
||||
if (_id) {
|
||||
entryObject.id = _id;
|
||||
if (!updateEnabled) return callback(undefined, undefined);
|
||||
|
||||
this.updateEntry(entryObject, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
this.getRecordBy('id', entryObject.id, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, new RSSSourceRecord(record.id, record.title, record.link, record.category, record.guild_id, record.channel_id));
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.checkForTitle(_title, (err, titleExists) => {
|
||||
if (!titleExists) {
|
||||
log.DEBUG("Entry doesn't exist, making one now", entryObject);
|
||||
this.saveEntry(entryObject, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
this.getRecordBy("title", entryObject.title, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, new RSSSourceRecord(record.id, record.title, record.link, record.category, record.guild_id, record.channel_id));
|
||||
})
|
||||
});
|
||||
}
|
||||
else{
|
||||
if (!updateEnabled) return callback(undefined, undefined);
|
||||
|
||||
this.updateEntry(entryObject, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
this.getRecordBy('title', entryObject.title, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, new RSSSourceRecord(record.id, record.title, record.link, record.category, record.guild_id, record.channel_id));
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.PostStorage = class PostStorage extends Storage {
|
||||
constructor(connection = undefined) {
|
||||
super(rssPostsTable, connection);
|
||||
}
|
||||
|
||||
savePost(_postObject, callback){
|
||||
const tempCreationDate = returnMysqlTime();
|
||||
log.DEBUG("Saving Post Object:", _postObject);
|
||||
if (!_postObject?.postId || !_postObject?.link) {
|
||||
return callback(new Error("Post object malformed, check the object before saving it", _postObject), undefined)
|
||||
}
|
||||
|
||||
if (_postObject.link.length > 250) _postObject.link = _postObject.link.substring(0, 250);
|
||||
|
||||
const sqlQuery = `INSERT INTO ${this.dbTable} (post_guid, post_link, post_sent_date) VALUES ("${_postObject.postId}","${_postObject.link}","${tempCreationDate}");`;
|
||||
|
||||
log.DEBUG(`Adding new post with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
}
|
||||
175
Server/libUtils.js
Normal file
175
Server/libUtils.js
Normal file
@@ -0,0 +1,175 @@
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("./utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "libUtils");
|
||||
const { NodeHtmlMarkdown } = require('node-html-markdown');
|
||||
const { parse } = require("node-html-parser");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const imageRegex = /(http(s?):)([/|.|\w|\s|-])*((\.(?:jpg|gif|png|webm))|(\/gallery\/(?:[/|.|\w|\s|-])*))/g;
|
||||
const youtubeVideoRegex = /((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)/g
|
||||
|
||||
exports.EmmeliaEmbedBuilder = class PostEmbedBuilder extends EmbedBuilder {
|
||||
constructor() {
|
||||
super()
|
||||
this.setTimestamp();
|
||||
this.setFooter({ text: 'Brought to you by Emmelia.' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sleep - sleep/wait
|
||||
* @constructor
|
||||
*/
|
||||
exports.runAfter = async (toRun, timeout = 10000) => {
|
||||
log.DEBUG(`Running '${toRun}' after ${timeout / 1000} seconds`);
|
||||
setTimeout(toRun, timeout)
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*
|
||||
* @param {*} val Value to be normalized
|
||||
* @returns Normalized value
|
||||
*/
|
||||
exports.normalizePort = (val) => {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
exports.onError = (error) => {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
log.ERROR(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
log.ERROR(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
exports.sendPost = (post, source, channel, callback) => {
|
||||
log.DEBUG("Sending post from source: ", post, source);
|
||||
const postTitle = String(post.title).substring(0, 150);
|
||||
const postLink = post.link;
|
||||
let postContent;
|
||||
|
||||
if (post.content) {
|
||||
// Reset the content parameter with the encoded parameter
|
||||
post.content = parse(post['content:encoded'] ?? post.content);
|
||||
// Get the post content and trim it to length or add a placeholder if necessary
|
||||
var postText = String(post.content.text);
|
||||
if (postText.length >= 3800) postText = `${postText.slice(0, 3800).substring(0, Math.min(String(post.content.text).length, String(post.content.text).lastIndexOf(" ")))} [...](${post.link})`;
|
||||
else if (postText.length === 0) postText = `*This post has no content* [Direct Link](${post.link})`;
|
||||
postContent = postText;
|
||||
}
|
||||
else postContent = `*This post has no content* [Direct Link](${post.link})`;
|
||||
|
||||
// Check for embedded youtube videos and add the first four as links
|
||||
const ytVideos = String(post.content).match(youtubeVideoRegex);
|
||||
if (ytVideos) {
|
||||
for (var ytVideo of ytVideos.slice(0,4)){
|
||||
// If the video is an embed, replace the embed to make it watchable
|
||||
if (ytVideo.includes("embed")) ytVideo = ytVideo.replace("embed/", "watch?v=");
|
||||
postContent += `\nEmbeded Video from Post: [YouTube](${ytVideo})`
|
||||
}
|
||||
}
|
||||
log.DEBUG("Post content: ", postContent);
|
||||
|
||||
const postId = post.postId;
|
||||
if (!post.pubDate) post.pubDate = Date.now();
|
||||
const postPubDate = new Date(post.pubDate).toISOString();
|
||||
|
||||
var postSourceLink = source.title;
|
||||
var postImage = post.image ?? undefined;
|
||||
|
||||
if (!postImage){
|
||||
if (post.content){
|
||||
const linksInPost = post.content.querySelectorAll("a");
|
||||
if (linksInPost) {
|
||||
log.DEBUG("Found links in post:", linksInPost);
|
||||
for (const link of linksInPost) {
|
||||
// Check to see if this link is a youtube video that was already found, if so skip it
|
||||
if (ytVideos?.includes(link)) continue;
|
||||
const images = String(link.getAttribute("href")).match(imageRegex);
|
||||
log.DEBUG("Images found in post:", images);
|
||||
if (images) {
|
||||
postImage = images[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.DEBUG("Sending an RSS post to discord", postTitle, postId, postContent)
|
||||
try{
|
||||
const rssMessage = new this.EmmeliaEmbedBuilder()
|
||||
.setColor(0x0099FF)
|
||||
.setTitle(postTitle)
|
||||
.setURL(postLink)
|
||||
.addFields({ name: 'Source', value: postSourceLink, inline: true })
|
||||
.addFields({ name: 'Published', value: postPubDate, inline: true });
|
||||
|
||||
// TODO - If there is more than one image, create a canvas and post the created canvas
|
||||
if (postImage) {
|
||||
log.DEBUG("Image from post:", postImage);
|
||||
rssMessage.setImage(postImage);
|
||||
}
|
||||
|
||||
//Add the main content if it's present
|
||||
postContent = postContent.slice(0, 4090).trim();
|
||||
if (postContent) rssMessage.setDescription( postContent );
|
||||
|
||||
channel.send({ embeds: [rssMessage] });
|
||||
|
||||
//throw new Error("YOU SHALL NOT PASS");
|
||||
|
||||
return callback(undefined, true);
|
||||
}
|
||||
catch (err){
|
||||
log.ERROR("Error sending message: ", postTitle, postId, postContent, postPubDate, err);
|
||||
return callback(err, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
exports.returnHash = (...stringsIncluded) => {
|
||||
return crypto.createHash('sha1').update(`${stringsIncluded.join("-<<??//\\\\??>>-")}`).digest("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key exists in an array of objects
|
||||
* @param {*} key The key to search for
|
||||
* @param {*} array The object to search for the key
|
||||
* @returns {boolean} If the key exists in the object
|
||||
*/
|
||||
exports.checkForKeyInArrayOfObjects = (key, array) => {
|
||||
return array.filter(function (o) {
|
||||
return o.hasOwnProperty(key);
|
||||
}).length > 0;
|
||||
}
|
||||
65
Server/middleware/balanceAuthorization.js
Normal file
65
Server/middleware/balanceAuthorization.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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 {
|
||||
checkForAccount,
|
||||
createAccount,
|
||||
verifyBalance,
|
||||
insufficientTokensResponse,
|
||||
welcomeResponse
|
||||
} = require("../controllers/accountController");
|
||||
|
||||
/**
|
||||
* Authorize a transaction amount for an account
|
||||
*
|
||||
* @param {*} interaction
|
||||
* @param {*} command
|
||||
* @param {undefined|number} _tokens The amount of tokens to authorize, set to undefined if the value is in the interaction
|
||||
* @param {*} next
|
||||
* @returns
|
||||
*/
|
||||
exports.authorizeTokenUsage = async (interaction, command, _tokens = undefined, next) => {
|
||||
log.DEBUG("Command requires tokens? ", command.isPrivileged)
|
||||
if(!command.requiresTokens) return next(true);
|
||||
|
||||
if(!interaction.member && (!_tokens || !interaction.options.getNumber("tokens") || !command.defaultTokenUsage)) throw new Error("No member or tokens specified before attempting to authorize");
|
||||
const memberId = interaction.member.id;
|
||||
var tokensToBeUsed;
|
||||
|
||||
if (!_tokens || _tokens && isNaN(_tokens)){
|
||||
if (interaction.options.getNumber("tokens")) tokensToBeUsed = interaction.options.getNumber("tokens");
|
||||
else tokensToBeUsed = command.defaultTokenUsage;
|
||||
}
|
||||
else tokensToBeUsed = _tokens;
|
||||
|
||||
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);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
23
Server/middleware/discordAuthorization.js
Normal file
23
Server/middleware/discordAuthorization.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// To authorize a message sender for admin level commands
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "discordAuthorization");
|
||||
|
||||
const botAdmins = process.env.BOT_ADMINS;
|
||||
|
||||
exports.authorizeCommand = async (interaction, command, next) => {
|
||||
log.DEBUG("Command is privileged? ", command.isPrivileged)
|
||||
|
||||
// If the command is not privileged, run the command
|
||||
if (!command.isPrivileged) return next(true);
|
||||
|
||||
log.DEBUG(`${interaction.member.user} is attempting to run the privileged command '${command}'`);
|
||||
|
||||
// Check to see if the user has the role specified in the config
|
||||
if (!interaction.member.roles.cache.has(`${botAdmins}`)) {
|
||||
log.DEBUG(`Unauthorized - ${interaction.member.user} does not have the privilege to run '${command}'`);
|
||||
return await interaction.reply({ content: `Sorry ${interaction.member.user}, you are not permitted to run that command`, ephemeral: true });
|
||||
} else {
|
||||
log.DEBUG(`Authorized - ${interaction.member.user} can run '${command}'`);
|
||||
return next(true);
|
||||
}
|
||||
}
|
||||
115
Server/modules/linkCop.js
Normal file
115
Server/modules/linkCop.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "linkCop");
|
||||
|
||||
const { linkCopInsults } = require('../extras/linkCopInsults');
|
||||
|
||||
const linkRegExp = /(?:http[s]?:\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/g
|
||||
// Langvars that can be used in the insults
|
||||
// Mention the posting user
|
||||
const mtnUsrRegExp = /\{\%mtn_user\%\}/g
|
||||
// Reference the original message
|
||||
const refOgMsgRegExp = /\{\%ref_og_msg\%\}/g
|
||||
// Reference the channel the original message was posted in
|
||||
const refOgChannelRegExp = /\{\%ref_og_channel\%\}/g
|
||||
// Reference the links from the original message
|
||||
const refLinksRegExp = /\{\%ref_links\%\}/g
|
||||
|
||||
const restrictedChannelIds = [
|
||||
'757379843792044102',
|
||||
'367396189529833474',
|
||||
"918029426397184000"
|
||||
]
|
||||
|
||||
const approvedLinksChannel = "767303243285790721";
|
||||
|
||||
async function replaceLangVars(messageContent, selectedInsult) {
|
||||
selectedInsult = String(selectedInsult)
|
||||
selectedInsult = selectedInsult.replace(mtnUsrRegExp, messageContent.author);
|
||||
selectedInsult = selectedInsult.replace(refOgMsgRegExp, messageContent.content);
|
||||
selectedInsult = selectedInsult.replace(refOgChannelRegExp, `<#${messageContent.channelId}>`);
|
||||
selectedInsult = selectedInsult.replace(refLinksRegExp, Array(messageContent.links).join(", "));
|
||||
|
||||
return selectedInsult;
|
||||
}
|
||||
|
||||
exports.linkCop = async (message) => {
|
||||
if (!restrictedChannelIds.includes(message.channelId)) return;
|
||||
const client = message.client;
|
||||
log.DEBUG("Message Content: ", message.content);
|
||||
|
||||
log.VERBOSE("Interaction: ", message);
|
||||
const urls = String(message.content).matchAll(linkRegExp);
|
||||
if (!urls || urls.length === 0) return;
|
||||
log.DEBUG("Found URLs: ", urls);
|
||||
|
||||
const messageContent = {
|
||||
'author': message.author,
|
||||
'content': String(message.content),
|
||||
'channelId': message.channelId,
|
||||
'links': urls
|
||||
}
|
||||
|
||||
await message.delete();
|
||||
|
||||
var selectedInsult = linkCopInsults[Math.floor(Math.random()*linkCopInsults.length)];
|
||||
const finalMessage = await replaceLangVars(messageContent, selectedInsult);
|
||||
log.DEBUG("Final Message: ", finalMessage);
|
||||
|
||||
log.VERBOSE("Client: ", client.guilds);
|
||||
|
||||
client.channels.get(approvedLinksChannel).send(finalMessage);
|
||||
}
|
||||
|
||||
/* TODO
|
||||
- Get the message to send, getting an error because client.guilds and .channels doesn't have functions
|
||||
- Make sure the links are added to the final message
|
||||
*/
|
||||
|
||||
/*
|
||||
Message {
|
||||
channelId: '918029426397184000',
|
||||
guildId: '367396189529833472',
|
||||
id: '1101346007087845437',
|
||||
createdTimestamp: 1682651750109,
|
||||
type: 0,
|
||||
system: false,
|
||||
content: '<@943742040255115304> asd',
|
||||
author: User {
|
||||
id: '301507932678520833',
|
||||
bot: false,
|
||||
system: false,
|
||||
flags: UserFlagsBitField { bitfield: 0 },
|
||||
username: 'Logan',
|
||||
discriminator: '3331',
|
||||
avatar: null,
|
||||
banner: undefined,
|
||||
accentColor: undefined
|
||||
},
|
||||
pinned: false,
|
||||
tts: false,
|
||||
nonce: '1101346006672343040',
|
||||
embeds: [],
|
||||
components: [],
|
||||
attachments: Collection(0) [Map] {},
|
||||
stickers: Collection(0) [Map] {},
|
||||
position: null,
|
||||
editedTimestamp: null,
|
||||
reactions: ReactionManager { message: [Circular *1] },
|
||||
mentions: MessageMentions {
|
||||
everyone: false,
|
||||
users: Collection(1) [Map] { '943742040255115304' => [ClientUser] },
|
||||
roles: Collection(0) [Map] {},
|
||||
_members: null,
|
||||
_channels: null,
|
||||
_parsedUsers: null,
|
||||
crosspostedChannels: Collection(0) [Map] {},
|
||||
repliedUser: null
|
||||
},
|
||||
webhookId: null,
|
||||
groupActivityApplication: null,
|
||||
applicationId: null,
|
||||
activity: null,
|
||||
flags: MessageFlagsBitField { bitfield: 0 },
|
||||
reference: null,
|
||||
interaction: null
|
||||
} */
|
||||
3664
Server/package-lock.json
generated
3664
Server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,38 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
},
|
||||
"name": "Emmelia",
|
||||
"version": "1.0.0",
|
||||
"description": "Discord RSS News Bot",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "~1.4.0",
|
||||
"@discordjs/rest": "~1.5.0",
|
||||
"axios": "~1.3.4",
|
||||
"chatgpt": "~4.7.2",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"debug": "~2.6.9",
|
||||
"discord-api-types": "~0.37.35",
|
||||
"discord.js": "~14.7.1",
|
||||
"dotenv": "~16.0.3",
|
||||
"ejs": "~2.6.1",
|
||||
"express": "~4.16.1",
|
||||
"express": "~4.18.2",
|
||||
"fs": "~0.0.1-security",
|
||||
"gpt-3-encoder": "~1.1.4",
|
||||
"http-errors": "~1.6.3",
|
||||
"jsdoc": "~3.6.7",
|
||||
"jsonfile": "~6.1.0",
|
||||
"morgan": "~1.9.1",
|
||||
"mysql": "^2.18.1"
|
||||
}
|
||||
"mysql": "~2.18.1",
|
||||
"node-html-markdown": "~1.3.0",
|
||||
"node-html-parser": "~6.1.5",
|
||||
"openai": "~3.1.0",
|
||||
"parse-files": "~0.1.1",
|
||||
"rss-parser": "~3.12.0",
|
||||
"user-agents": "~1.0.1303"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node index.js"
|
||||
},
|
||||
"author": "Logan Cusano",
|
||||
"license": "ISC"
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
body {
|
||||
padding: 50px;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B7FF;
|
||||
}
|
||||
@@ -1,9 +1,31 @@
|
||||
const libCore = require("../libCore");
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', (req, res, next) => {
|
||||
res.render('index', { title: 'Express' });
|
||||
router.get('/', (req, res) => {
|
||||
var sources = libCore.getSources();
|
||||
//res.render('index', { "sources": sources });
|
||||
|
||||
var htmlOutput = "";
|
||||
|
||||
sources.forEach(source => {
|
||||
htmlOutput += `
|
||||
<div style='margin-bottom:15px;'>
|
||||
|
||||
<div> Title: ${source.title} </div>
|
||||
<div> Link: ${source.link} </div>
|
||||
<div> category: ${source.category} </div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
`
|
||||
});
|
||||
res.send(htmlOutput);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
15
Server/update.sh
Normal file
15
Server/update.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# Stating Message
|
||||
echo "<!-- UPDATING ---!>"
|
||||
|
||||
# TODO - Add an updater for Stable Diffusion API
|
||||
|
||||
# Update the git Repo
|
||||
git fetch -a -p
|
||||
git pull
|
||||
|
||||
# Install any new libraries
|
||||
npm i
|
||||
|
||||
# Update complete message
|
||||
echo "<!--- UPDATE COMPLETE! ---!>"
|
||||
@@ -1,17 +0,0 @@
|
||||
// Debug
|
||||
const debug = require('debug');
|
||||
|
||||
/**
|
||||
* Create the different logging methods for a function
|
||||
* Namespace template = ("[app]:[fileName]:['INFO', 'WARNING', 'DEBUG', 'ERROR']")
|
||||
* @param {string} appName The name of the app to be used in the 'app' portion of the namespace
|
||||
* @param {string} fileName The name of the file calling the builder to be used in the 'fileName' portion of the namespace
|
||||
*/
|
||||
exports.DebugBuilder = class DebugBuilder {
|
||||
constructor(appName, fileName) {
|
||||
this.INFO = debug(`${appName}:${fileName}:INFO`);
|
||||
this.DEBUG = debug(`${appName}:${fileName}:DEBUG`);
|
||||
this.WARN = debug(`${appName}:${fileName}:WARNING`);
|
||||
this.ERROR = debug(`${appName}:${fileName}:ERROR`);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("server", "httpRequests");
|
||||
// Modules
|
||||
const http = require("http");
|
||||
|
||||
exports.requestOptions = class requestOptions {
|
||||
/**
|
||||
* Construct an HTTP request
|
||||
* @param {*} method Method of request to use [GET, POST]
|
||||
* @param {*} headers Headers of the request, not required but will get filled with default values if not set
|
||||
* @param {*} hostname The destination of the request
|
||||
* @param {*} port The port for the destination, will use 3001 by default
|
||||
*/
|
||||
constructor(path, method, hostname, port = 3001, headers = undefined, timeout = undefined) {
|
||||
this.hostname = hostname;
|
||||
this.path = path;
|
||||
this.port = port;
|
||||
this.method = method;
|
||||
this.timeout = timeout;
|
||||
if (method === "POST"){
|
||||
this.headers = headers ?? {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the HTTP request to the server
|
||||
* @param requestOptions
|
||||
* @param data
|
||||
* @param callback
|
||||
*/
|
||||
exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callback){
|
||||
log.DEBUG("Sending a request to: ", requestOptions.hostname, requestOptions.port)
|
||||
// Create the request
|
||||
const req = http.request(requestOptions, res => {
|
||||
res.on('data', (data) => {
|
||||
const responseObject = {
|
||||
"statusCode": res.statusCode,
|
||||
"body": data
|
||||
};
|
||||
|
||||
try {
|
||||
responseObject.body = JSON.parse(responseObject.body)
|
||||
}
|
||||
catch (err) {
|
||||
}
|
||||
|
||||
log.DEBUG("Response Object: ", responseObject);
|
||||
callback(responseObject);
|
||||
})
|
||||
}).on('error', err => {
|
||||
log.ERROR('Error: ', err.message)
|
||||
// TODO need to handle if the server is down
|
||||
})
|
||||
|
||||
if (requestOptions.timeout) {
|
||||
req.setTimeout(requestOptions.timeout, () => {
|
||||
callback(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Write the data to the request and send it
|
||||
req.write(data)
|
||||
req.end()
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
const mysql = require('mysql');
|
||||
const databaseConfig = require('../config/databaseConfig');
|
||||
const utils = require('./utils');
|
||||
|
||||
const connection = mysql.createConnection({
|
||||
host: databaseConfig.database_host,
|
||||
user: databaseConfig.database_user,
|
||||
password: databaseConfig.database_password,
|
||||
database: databaseConfig.database_database
|
||||
});
|
||||
|
||||
const nodesTable = `${databaseConfig.database_database}.nodes`;
|
||||
|
||||
connection.connect()
|
||||
|
||||
/** Get all nodes the server knows about regardless of status
|
||||
* @param {*} callback Callback function
|
||||
*/
|
||||
exports.getAllNodes = (callback) => {
|
||||
const sqlQuery = `SELECT * FROM ${nodesTable}`
|
||||
runSQL(sqlQuery, (rows) => {
|
||||
callback(rows);
|
||||
})
|
||||
}
|
||||
|
||||
/** Get all nodes that have the online status set true (are online)
|
||||
* @param callback Callback function
|
||||
*/
|
||||
exports.getOnlineNodes = (callback) => {
|
||||
const sqlQuery = `SELECT * FROM ${nodesTable} WHERE online = 1;`
|
||||
runSQL(sqlQuery, (rows) => {
|
||||
callback(rows);
|
||||
})
|
||||
}
|
||||
|
||||
/** Get info on a node based on ID
|
||||
* @param nodeId The ID of the node
|
||||
* @param callback Callback function
|
||||
*/
|
||||
exports.getNodeInfoFromId = (nodeId, callback) => {
|
||||
const sqlQuery = `SELECT * FROM ${nodesTable} WHERE id = ${nodeId}`
|
||||
runSQL(sqlQuery, (rows) => {
|
||||
// Call back the first (and theoretically only) row
|
||||
// Specify 0 so downstream functions don't have to worry about it
|
||||
callback(rows[0]);
|
||||
})
|
||||
}
|
||||
|
||||
/** Add a new node to the DB
|
||||
* @param nodeObject Node information object
|
||||
* @param callback Callback function
|
||||
*/
|
||||
exports.addNewNode = (nodeObject, callback) => {
|
||||
if (!nodeObject.name) throw new Error("No name provided");
|
||||
const name = nodeObject.name,
|
||||
ip = nodeObject.ip,
|
||||
port = nodeObject.port,
|
||||
location = nodeObject.location,
|
||||
nearbySystems = utils.JsonToBuffer(nodeObject.nearbySystems),
|
||||
online = nodeObject.online;
|
||||
const sqlQuery = `INSERT INTO ${nodesTable} (name, ip, port, location, nearbySystems, online) VALUES ('${name}', '${ip}', ${port}, '${location}', '${nearbySystems}', ${online})`;
|
||||
|
||||
runSQL(sqlQuery, (rows) => {
|
||||
callback(rows);
|
||||
})
|
||||
}
|
||||
|
||||
/** Update the known info on a node
|
||||
* @param nodeObject Node information object
|
||||
* @param callback Callback function
|
||||
*/
|
||||
exports.updateNodeInfo = (nodeObject, callback) => {
|
||||
const name = nodeObject.name,
|
||||
ip = nodeObject.ip,
|
||||
port = nodeObject.port,
|
||||
location = nodeObject.location,
|
||||
online = nodeObject.online;
|
||||
let queryParams = [],
|
||||
nearbySystems = nodeObject.nearbySystems;
|
||||
|
||||
if (name) queryParams.push(`name = '${name}'`);
|
||||
if (ip) queryParams.push(`ip = '${ip}'`);
|
||||
if (port) queryParams.push(`port = ${port}`);
|
||||
if (location) queryParams.push(`location = '${location}'`);
|
||||
if (nearbySystems) {
|
||||
nearbySystems = utils.JsonToBuffer(nearbySystems)
|
||||
queryParams.push(`nearbySystems = '${nearbySystems}'`);
|
||||
}
|
||||
if (typeof online === "boolean") {
|
||||
if (online) queryParams.push(`online = 1`);
|
||||
else queryParams.push(`online = 0`);
|
||||
}
|
||||
|
||||
let sqlQuery = `UPDATE ${nodesTable} SET`
|
||||
if (!queryParams || queryParams.length === 0) return callback(undefined);
|
||||
if (queryParams.length === 1) {
|
||||
sqlQuery = `${sqlQuery} ${queryParams[0]}`
|
||||
} else {
|
||||
let i = 0;
|
||||
for (const param of queryParams) {
|
||||
if (i === queryParams.length-1) {
|
||||
sqlQuery = `${sqlQuery} ${param}`
|
||||
i += 1;
|
||||
}
|
||||
else {
|
||||
sqlQuery = `${sqlQuery} ${param},`
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sqlQuery = `${sqlQuery} WHERE id = ${nodeObject.id};`
|
||||
|
||||
runSQL(sqlQuery, (rows) => {
|
||||
if (rows.affectedRows === 1) callback(true);
|
||||
else callback(rows);
|
||||
})
|
||||
}
|
||||
|
||||
// Function to run and handle SQL errors
|
||||
function runSQL(sqlQuery, callback, error = (err) => {
|
||||
console.log(err);
|
||||
throw err;
|
||||
}) {
|
||||
connection.query(sqlQuery, (err, rows) => {
|
||||
if (err) return error(err);
|
||||
//console.log('The rows are:', rows);
|
||||
return callback(rows);
|
||||
})
|
||||
}
|
||||
|
||||
exports.closeConnection = () => {
|
||||
connection.end()
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Convert a JSON object to a buffer for the DB
|
||||
exports.JsonToBuffer = (jsonObject) => {
|
||||
return Buffer.from(JSON.stringify(jsonObject))
|
||||
}
|
||||
|
||||
// Convert a buffer from the DB to JSON object
|
||||
exports.BufferToJson = (buffer) => {
|
||||
return JSON.parse(buffer.toString());
|
||||
}
|
||||
|
||||
/** Find a key in an object by its value
|
||||
*
|
||||
* @param {*} object The object to search
|
||||
* @param {*} value The value to search the arrays in the object for
|
||||
* @returns The key of the object that contains the value
|
||||
*/
|
||||
exports.getKeyByArrayValue = (object, value) => {
|
||||
return Object.keys(object).find(key => object[key].includes(value));
|
||||
}
|
||||
Reference in New Issue
Block a user