15 Commits

Author SHA1 Message Date
Logan Cusano
dd5b442377 Update client clientController to better handle no ID 2023-06-18 16:26:55 -04:00
Logan Cusano
c5a7131063 Improve validation when checking for nodeId 2023-06-18 15:31:15 -04:00
Logan Cusano
5d54f07af4 Enabled nodeMonitorService
- Updated logging for nodeMonitorService to use it's own debugBuilder
2023-06-18 14:56:54 -04:00
Logan Cusano
24faa5279d #29
- Update debugBuilder to inspect objects instead of stringify
- Moved tthe debug entry for saving post to after the post is validated
2023-06-18 14:49:15 -04:00
Logan Cusano
79d2ca1887 #6 Removed copy of env
- Should be done manually and edited before running setup script
2023-06-17 23:37:26 -04:00
Logan Cusano
c2b4b4bfa1 #16
- Allow bots to display the preset they are listening to
2023-06-17 22:02:09 -04:00
Logan Cusano
d8a697e583 #13
- Added command to allow users to give other users their group
- Shows all groups as options but will not let user add another user if the caller doesn't have the role
2023-06-17 21:49:46 -04:00
Logan Cusano
44caa11f7c Fixed typo in remove description 2023-06-17 21:28:33 -04:00
Logan Cusano
dc92b07426 Implemented function to remove commands from a given bot and guild 2023-06-17 19:37:13 -04:00
Logan Cusano
92f4caad0c Improve the join node verification system 2023-06-17 19:25:47 -04:00
Logan Cusano
b888a9233d Improve the response when a user requests a bot to join 2023-06-17 19:01:55 -04:00
Logan Cusano
b4e27162aa Update to mysql2 2023-06-17 18:40:09 -04:00
Logan Cusano
bfda15866e Streamlined joining and leaving autocomplete
- Removed custom command builder as it's no longer needed with autocomplete
2023-06-17 17:55:27 -04:00
Logan Cusano
f4475dc9d7 #19
- Update the wrapper called when a feed encounters an error
    - Will now use a more robus backoff system
    - Waits in increments of 30 seconds
    - Keeps track of ignored attempts and 'count'

- Updated wrapper to remove source from backoff list
    - Now removes the object after the first attempt irrespective of deletion station
2023-06-16 23:26:38 -04:00
Logan Cusano
c4650a9e99 Make the bot option in the leave command required 2023-06-16 22:02:54 -04:00
21 changed files with 250 additions and 330 deletions

View File

@@ -35,12 +35,12 @@ exports.joinServer = async (req, res) => {
log.INFO("Join requested to: ", deviceId, channelId, clientId, presetName, NGThreshold);
if (process.platform === "win32") {
log.DEBUG("Starting Windows Python");
pythonProcess = await spawn('python.exe', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId, '-n', NGThreshold], { cwd: resolve(__dirname, "../pdab/").toString() });
pythonProcess = await spawn('python.exe', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId, '-n', NGThreshold, '-p', presetName ], { cwd: resolve(__dirname, "../pdab/").toString() });
//pythonProcess = await spawn('C:\\Python310\\python.exe', [resolve(__dirname, "../PDAB/main.py"), deviceId, channelId, clientId, NGThreshold ]);
}
else {
log.DEBUG("Starting Linux Python");
pythonProcess = await spawn('python3', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId,'-n', NGThreshold ], { cwd: resolve(__dirname, "../pdab/") });
pythonProcess = await spawn('python3', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId,'-n', NGThreshold, '-p', presetName ], { cwd: resolve(__dirname, "../pdab/") });
}
log.VERBOSE("Python Process: ", pythonProcess);

View File

@@ -67,6 +67,11 @@ async function checkLocalIP() {
* Checks the config file for all required fields or gets and updates the required fields
*/
exports.checkConfig = async function checkConfig() {
if (!runningClientConfig.id || runningClientConfig.id == 0 || runningClientConfig.id == '0') {
updateConfig('id', "");
runningClientConfig.id = null;
}
if (!runningClientConfig.ip) {
const ipAddr = await checkLocalIP();
updateConfig('ip', ipAddr);
@@ -97,7 +102,7 @@ exports.checkIn = async () => {
await this.checkConfig();
// Check if there is an ID found, if not add the node to the server. If there was an ID, check in with the server to make sure it has the correct information
try {
if (runningClientConfig.id === 0) {
if (!runningClientConfig?.id || runningClientConfig.id == null) {
// ID was not found in the config, creating a new node
reqOptions = new requestOptions("/nodes/newNode", "POST");
sendHttpRequest(reqOptions, JSON.stringify(), (responseObject) => {
@@ -118,6 +123,7 @@ exports.checkIn = async () => {
// ID is in the config, checking in with the server
reqOptions = new requestOptions("/nodes/nodeCheckIn", "POST");
sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), (responseObject) => {
log.DEBUG("Check In Respose: ", responseObject);
if (responseObject.statusCode === 202) {
// Server accepted an update
}

View File

@@ -1,5 +1,5 @@
import argparse, platform, os
from discord import Intents, Client, Member, opus
from discord import Intents, Client, Member, opus, Activity, ActivityType
from discord.ext import commands
from NoiseGatev2 import NoiseGate
@@ -25,7 +25,7 @@ async def load_opus():
return "armv7l"
def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY', channelId=367396189529833476, NGThreshold=50, deviceId=1):
def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY', channelId=367396189529833476, NGThreshold=50, deviceId=1, presence="the radio"):
intents = Intents.default()
client = commands.Bot(command_prefix='!', intents=intents)
@@ -33,6 +33,8 @@ def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY',
@client.event
async def on_ready():
print(f'We have logged in as {client.user}')
# Set the presence of the bot (what it's listening to)
await client.change_presence(activity=Activity(type=ActivityType.listening, name=presence))
channelIdToJoin = client.get_channel(channelId)
print("Channel", channelIdToJoin)
@@ -55,11 +57,13 @@ def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY',
client.run(clientId)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("deviceId", type=int, help="The ID of the audio device to use")
parser.add_argument("channelId", type=int, help="The ID of the voice channel to use")
parser.add_argument("clientId", type=str, help="The discord client ID")
parser.add_argument("-n", "--NGThreshold", type=int, help="Change the noisegate threshold. This defaults to 50")
parser.add_argument("-p", "--presence", type=str, help="What the bot should be listening to")
args = parser.parse_args()
if (not args.NGThreshold):
@@ -71,5 +75,6 @@ main(
clientId=args.clientId,
channelId=args.channelId,
NGThreshold=args.NGThreshold,
deviceId=args.deviceId
deviceId=args.deviceId,
presence=args.presence
)

View File

@@ -13,9 +13,6 @@ ls -ld $SCRIPT_DIR | awk '{print $3}' >> ./config/installerName
useradd -M RadioNode
usermod -s -L RadioNode
# Create the .env file from the example
cp $SCRIPT_DIR/.env.example $SCRIPT_DIR/.env
# Change the ownership of the directory to the service user
chown RadioNode -R $SCRIPT_DIR

View File

@@ -4,6 +4,7 @@ const debug = require('debug');
require('dotenv').config();
// Modules
const { writeFile } = require('fs');
const { inspect } = require('util');
const logLocation = process.env.LOG_LOCATION;
@@ -34,31 +35,31 @@ exports.DebugBuilder = class DebugBuilder {
this.INFO = (...messageParts) => {
const _info = debug(`${appName}:${fileName}:INFO`);
_info(messageParts);
writeToLog(`${appName}:${fileName}:INFO\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:INFO\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
}
this.DEBUG = (...messageParts) => {
const _debug = debug(`${appName}:${fileName}:DEBUG`);
_debug(messageParts);
writeToLog(`${appName}:${fileName}:DEBUG\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:DEBUG\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
}
this.VERBOSE = (...messageParts) => {
const _verbose = debug(`${appName}:${fileName}:VERBOSE`);
_verbose(messageParts);
writeToLog(`${appName}:${fileName}:VERBOSE\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:VERBOSE\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
}
this.WARN = (...messageParts) => {
const _warn = debug(`${appName}:${fileName}:WARNING`);
_warn(messageParts);
writeToLog(`${appName}:${fileName}:WARNING\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:WARNING\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
}
this.ERROR = (...messageParts) => {
const _error = debug(`${appName}:${fileName}:ERROR`);
_error(messageParts);
writeToLog(`${appName}:${fileName}:ERROR\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:ERROR\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
if (process.env.EXIT_ON_ERROR && process.env.EXIT_ON_ERROR > 0) {
writeToLog("!--- EXITING ---!", appName);
setTimeout(process.exit, process.env.EXIT_ON_ERROR_DELAY ?? 0);

View File

@@ -0,0 +1,52 @@
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "give-role");
module.exports = {
data: new SlashCommandBuilder()
.setName('give-role')
.setDescription('Use this command to give a role you have to another member.')
.addUserOption(option =>
option.setName('user')
.setDescription('The user you wish to give the role to ')
.setRequired(true))
.addRoleOption(option =>
option.setName('role')
.setDescription('The role you wish to give the selected user')
.setRequired(true)),
example: "give-role",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: true,
/*async autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
},*/
async execute(interaction) {
try{
// The role to give to the user
const selectedRole = interaction.options.getRole('role');
// The user who should be given the role
var selectedUser = interaction.options.getUser("user");
selectedUser = interaction.guild.members.cache.get(selectedUser.id);
// The user who initiated the command
const initUser = interaction.member;
log.DEBUG("Give Role DEBUG: ", initUser, selectedRole, selectedUser);
// Check if the user has the role selected
if (!initUser.roles.cache.find(role => role.name === selectedRole.name)) return await interaction.editReply(`Sorry ${initUser}, you don't have the group ${selectedRole} and thus you cannot give it to ${selectedUser}`);
// Give the selected user the role and let both the user and the initiator know
await selectedUser.roles.add(selectedRole);
return await interaction.editReply(`Ok ${initUser}, ${selectedUser} has been given the ${selectedRole} role!`)
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

View File

@@ -1,9 +1,9 @@
// Modules
const { customSlashCommandBuilder } = require('../utilities/customSlashCommandBuilder');
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { getMembersInRole, getAllClientIds } = require("../utilities/utils");
const { getMembersInRole, getAllClientIds, filterAutocompleteValues } = require("../utilities/utils");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId } = require("../utilities/mysqlHandler");
const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId, getAllConnections } = require("../utilities/mysqlHandler");
// Global Vars
const log = new DebugBuilder("server", "join");
@@ -13,10 +13,10 @@ const log = new DebugBuilder("server", "join");
*
* @param {*} presetName The preset name to listen to on the client
* @param {*} channelId The channel ID to join the bot to
* @param {*} clientIdsUsed EITHER A collection of clients that are currently connected OR a single discord client ID (NOT dev portal ID) that should be used to join the server with
* @param {*} connections EITHER A collection of clients that are currently connected OR a single discord client ID (NOT dev portal ID) that should be used to join the server with
* @returns
*/
async function joinServerWrapper(presetName, channelId, clientIdsUsed) {
async function joinServerWrapper(presetName, channelId, connections) {
// Get nodes online
var onlineNodes = await new Promise((recordResolve, recordReject) => {
getOnlineNodes((nodeRows) => {
@@ -45,16 +45,16 @@ async function joinServerWrapper(presetName, channelId, clientIdsUsed) {
log.DEBUG("All clients: ", Object.keys(availableClientIds));
var selectedClientId;
if (typeof clientIdsUsed === 'string') {
if (typeof connections === 'string') {
for (const availableClientId of availableClientIds) {
if (availableClientId.discordId != clientIdsUsed ) selectedClientId = availableClientId;
if (availableClientId.discordId != connections ) selectedClientId = availableClientId;
}
}
else {
log.DEBUG("Client IDs Used: ", clientIdsUsed.keys());
for (const usedClientId of clientIdsUsed.keys()) {
log.DEBUG("Used Client ID: ", usedClientId);
availableClientIds = availableClientIds.filter(cid => cid.discordId != usedClientId);
log.DEBUG("Open connections: ", connections);
for (const connection of connections) {
log.DEBUG("Used Client ID: ", connection);
availableClientIds = availableClientIds.filter(cid => cid.discordId != connection.clientObject.discordId);
}
log.DEBUG("Available Client IDs: ", availableClientIds);
@@ -84,19 +84,48 @@ async function joinServerWrapper(presetName, channelId, clientIdsUsed) {
const nodeConnection = await addNodeConnection(selectedNode, selectedClientId);
log.DEBUG("Node Connection: ", nodeConnection);
});
return selectedClientId;
}
exports.joinServerWrapper = joinServerWrapper;
module.exports = {
data: new customSlashCommandBuilder()
data: new SlashCommandBuilder()
.setName('join')
.setDescription('Join the channel you are in with the preset you choose')
.addAllSystemPresetOptions(),
.addStringOption(option =>
option.setName("preset")
.setDescription("The preset you would like to listen to")
.setAutocomplete(true)
.setRequired(true)),
example: "join",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: true,
async autocomplete(interaction) {
const nodeObjects = await new Promise((recordResolve, recordReject) => {
getOnlineNodes((nodeRows) => {
recordResolve(nodeRows);
});
});
log.DEBUG("Node objects: ", nodeObjects);
var presetsAvailable = [];
for (const nodeObject of nodeObjects) {
log.DEBUG("Node object: ", nodeObject);
for (const presetName in nodeObject.nearbySystems) presetsAvailable.push(nodeObject.nearbySystems[presetName]);
}
log.DEBUG("All Presets available: ", presetsAvailable);
// Remove duplicates
options = [...new Set(presetsAvailable)];
log.DEBUG("DeDuped Presets available: ", options);
// Filter the results to what the user is entering
filterAutocompleteValues(interaction, options);
},
async execute(interaction) {
try{
const guildId = interaction.guild.id;
@@ -105,12 +134,13 @@ module.exports = {
const channelId = interaction.member.voice.channel.id;
log.DEBUG(`Join requested by: ${interaction.user.username}, to: '${presetName}', in channel: ${channelId} / ${guildId}`);
const onlineBots = await getMembersInRole(interaction);
const connections = await getAllConnections();
log.DEBUG("Online Bots: ", onlineBots);
log.DEBUG("Current Connections: ", connections);
await joinServerWrapper(presetName, channelId, onlineBots.online);
await interaction.editReply('**Pong.**');
const selectedClientId = await joinServerWrapper(presetName, channelId, connections);
await interaction.editReply(`Ok, ${interaction.member}. **${selectedClientId.name}** is joining your channel.`);
//await interaction.channel.send('**Pong.**'); // This will send a message to the channel of the interaction outside of the initial reply
}catch(err){
log.ERROR(err)

View File

@@ -1,13 +1,12 @@
// Modules
const { customSlashCommandBuilder } = require('../utilities/customSlashCommandBuilder');
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { getAllClientIds, getKeyByArrayValue } = require("../utilities/utils");
const { getAllClientIds, getKeyByArrayValue, filterAutocompleteValues } = require("../utilities/utils");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
const { checkNodeConnectionByClientId, removeNodeConnectionByNodeId, updateNodeInfo, getConnectedNodes, getAllConnections } = require('../utilities/mysqlHandler');
const { checkNodeConnectionByClientId, removeNodeConnectionByNodeId, getAllConnections } = require('../utilities/mysqlHandler');
// Global Vars
const log = new DebugBuilder("server", "leave");
const logAC = new DebugBuilder("server", "leave_autocorrect");
async function leaveServerWrapper(clientIdObject) {
if (!clientIdObject.clientId || !clientIdObject.name) return log.ERROR("Tried to leave server without client ID and/or Name");
@@ -34,30 +33,26 @@ async function leaveServerWrapper(clientIdObject) {
exports.leaveServerWrapper = leaveServerWrapper;
module.exports = {
data: new customSlashCommandBuilder()
data: new SlashCommandBuilder()
.setName('leave')
.setDescription('Disconnect a bot from the server')
.addStringOption(option =>
option.setName("bot")
.setDescription("The bot to disconnect from the server")
.setAutocomplete(true)),
.setAutocomplete(true)
.setRequired(true)),
example: "leave",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: true,
async autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
const connections = await getAllConnections();
const filtered = connections.filter(conn => String(conn.clientObject.name).startsWith(focusedValue)).map(conn => conn.clientObject.name);
logAC.DEBUG("Focused Value: ", focusedValue, connections, filtered);
await interaction.respond(
filtered.map(option => ({ name: option, value: option })),
);
const options = connections.map(conn => conn.clientObject.name);
await filterAutocompleteValues(interaction, options);
},
async execute(interaction) {
try{
const guildId = interaction.guild.id;
const botName = interaction.options.getString('bot');
log.DEBUG("Bot Name: ", botName)
const clinetIds = await getAllClientIds();

View File

@@ -7,7 +7,7 @@ const log = new DebugBuilder("server", "remove");
module.exports = {
data: new SlashCommandBuilder()
.setName('remove')
.setDescription('Remove an RSS source by it\' title')
.setDescription('Remove an RSS source by it\'s title')
.addStringOption(option =>
option.setName('title')
.setDescription('The title of the source to remove')

View File

@@ -108,6 +108,7 @@ exports.requestNodeJoinServer = async (req, res) => {
*/
exports.nodeMonitorService = class nodeMonitorService {
constructor() {
this.log = new DebugBuilder("server", "nodeMonitorService");
}
async start(){
@@ -130,21 +131,21 @@ exports.nodeMonitorService = class nodeMonitorService {
async checkInWithOnlineNodes(){
getOnlineNodes((nodes) => {
log.DEBUG("Online Nodes: ", nodes);
this.log.DEBUG("Online Nodes: ", nodes);
for (const node of nodes) {
const reqOptions = new requestOptions("/client/requestCheckIn", "GET", node.ip, node.port)
const request = sendHttpRequest(reqOptions, "", (responseObj) => {
sendHttpRequest(reqOptions, "", (responseObj) => {
if (responseObj) {
log.DEBUG("Response from: ", node.name, responseObj);
this.log.DEBUG("Response from: ", node.name, responseObj);
}
else {
log.DEBUG("No response from node, assuming it's offline");
this.log.DEBUG("No response from node, assuming it's offline");
const offlineNode = new nodeObject({ _online: 0, _id: node.id });
log.DEBUG("Offline node update object: ", offlineNode);
this.log.DEBUG("Offline node update object: ", offlineNode);
updateNodeInfo(offlineNode, (sqlResponse) => {
if (!sqlResponse) log.ERROR("No response from SQL object");
if (!sqlResponse) this.log.ERROR("No response from SQL object");
log.DEBUG("Updated node: ", sqlResponse);
this.log.DEBUG("Updated offline node: ", sqlResponse);
})
}
})

View File

@@ -148,7 +148,7 @@ client.on('ready', () => {
runHTTPServer();
log.DEBUG("Starting Node Monitoring Service");
//runNodeMonitorService();
runNodeMonitorService();
log.DEBUG("Starting RSS watcher");
runRssService();

View File

@@ -5,7 +5,7 @@ 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 mysql = require("mysql2");
const UserAgent = require("user-agents");
process.env.USER_AGENT_STRING = new UserAgent({ platform: 'Win32' }).toString();
@@ -32,17 +32,38 @@ var runningPostsToRemove = [{
}]
*/
var runningPostsToRemove = {};
const sourceFailureLimit = process.env.SOURCE_FAILURE_LIMIT ?? 3;
const sourceFailureLimit = process.env.SOURCE_FAILURE_LIMIT ?? 15;
/**
* Wrapper for feeds that cause errors. By default it will wait over a day for the source to come back online before deleting it.
*
* @param {*} sourceURL
* @param {string} sourceURL The URL of the feed source causing issues
*/
exports.removeSource = function removeSource(sourceURL) {
log.INFO("Removing source URL: ", sourceURL);
if (!sourceURL in runningPostsToRemove) {runningPostsToRemove[sourceURL] = 1; return;}
// Check to see if this is the first time this source has been attempted
if (!Object.keys(runningPostsToRemove).includes(sourceURL)) {
runningPostsToRemove[sourceURL] = { count: 1, timestamp: Date.now(), ignoredAttempts: 0 };
return;
}
if (runningPostsToRemove[sourceURL] < sourceFailureLimit) {runningPostsToRemove[sourceURL] += 1; return;}
const backoffDateTimeDifference = (Date.now() - new Date(runningPostsToRemove[sourceURL].timestamp));
const backoffWaitTime = (runningPostsToRemove[sourceURL].count * 30000);
log.DEBUG("Datetime", runningPostsToRemove[sourceURL], backoffDateTimeDifference, backoffWaitTime);
// Check to see if the last error occurred within the backoff period or if we should try again
if (backoffDateTimeDifference <= backoffWaitTime) {
runningPostsToRemove[sourceURL].ignoredAttempts +=1;
return;
}
// Increase the retry counter
if (runningPostsToRemove[sourceURL].count < sourceFailureLimit) {
runningPostsToRemove[sourceURL].count += 1;
runningPostsToRemove[sourceURL].timestamp = Date.now();
return;
}
feedStorage.getRecordBy('link', sourceURL, (err, record) => {
if (err) log.ERROR("Error getting record from feedStorage", err);
@@ -62,13 +83,14 @@ exports.removeSource = function removeSource(sourceURL) {
/**
* 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 (!Object.keys(runningPostsToRemove).includes(sourceURL)) return;
if (runningPostsToRemove[sourceURL] > sourceFailureLimit) return delete runningPostsToRemove[sourceURL];
delete runningPostsToRemove[sourceURL];
return
}
/**

View File

@@ -8,7 +8,7 @@ const { RSSSourceRecord, RSSPostRecord } = require("./utilities/recordHelper");
// Storage Specific Modules
// MySQL
const mysql = require("mysql");
const mysql = require("mysql2");
const rssFeedsTable = process.env.DB_RSS_FEEDS_TABLE;
const rssPostsTable = process.env.DB_RSS_POSTS_TABLE;
@@ -481,10 +481,10 @@ exports.PostStorage = class PostStorage extends Storage {
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)
}
log.DEBUG("Saving Post:", _postObject);
if (_postObject.link.length > 250) _postObject.link = _postObject.link.substring(0, 250);

View File

@@ -1,189 +0,0 @@
import re
import json
import pandas as pd
import requests
import os
import random
from fake_useragent import UserAgent
from bs4 import BeautifulSoup
from urllib.parse import urlparse, unquote, parse_qs
from time import sleep
ua = UserAgent()
#simply scrape
def scrape(url,**kwargs):
session=requests.Session()
session.headers.update({
'User-Agent': ua.random,
"authority": "www.zillow.com",
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"cache-control": "no-cache",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '^\^"Windows^\^"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
})
response=session.get(url,**kwargs)
return response
# Return all sections with key and attributes
def slurp(html, tag, attributes):
return BeautifulSoup(html, features="html.parser").findAll(tag, attributes)
# Returns the first number group from a given string
def return_numbers(string):
return int(re.findall(r'\d+', string)[0])
class Listing:
def __init__(self, address, bedrooms, bathrooms, sqft, price, link):
self.address = address
self.bedrooms = bedrooms
self.bathrooms = bathrooms
self.sqft = sqft
self.price = price
self.link = link
class ScrapeZillowListings:
def __init__(self, url):
self.parsed_original_url = self.init_check_url(urlparse(url))
self.html = scrape(url).text
self.listings = []
def init_check_url(self, parsed_url):
# Check to see if we are requesting listResults
print(parsed_url)
print(unquote(parsed_url.query))
print(parse_qs(parsed_url.query)['wants'])
for want in parse_qs(parsed_url.query)['wants']:
print(unquote(unquote(want)))
return parsed_url
def run(self):
self.listings.extend(self.scrape_listings(self.html))
pages = []
for page_nav in slurp(self.html, "nav", {"role":"navigation", "aria-label":"Pagination"}):
page_nav = f"<html><head><head/><body>{page_nav}<body/><html/>"
pages_list = slurp(page_nav, "li", {})
for page in pages_list:
if re.match("\d{1,2}", page.text) and not page.text == "1":
parsed_url = self.setup_url(page.find('a').get('href'))
sleep(random.randint(0,15))
temp_html = scrape(parsed_url.geturl()).text
self.listings.extend(self.scrape_listings(temp_html))
return self.listings
def print_listings(self):
index = 0
for temp_listing in self.listings:
print("--------")
print(f"Listing #{index}")
print(temp_listing.address)
print(temp_listing.price)
print(temp_listing.bedrooms)
print(temp_listing.bathrooms)
print(temp_listing.sqft)
print(temp_listing.link)
print("--------")
index += 1
def scrape_listings(self, html):
temp_listings = []
for listing in slurp(html, "article", {"data-test":"property-card"}):
listing = f"<html><head><head/><body>{listing}<body/><html/>"
uls = slurp(listing, "li", {})
beds = 0
baths = 0
sqft = 0
for ul in uls:
ul = ul.get_text()
if ("bds" in str(ul)):
beds = return_numbers(ul)
if ("ba" in str(ul)):
baths = return_numbers(ul)
if ("sqft" in str(ul)):
sqft = return_numbers(ul)
temp_listings.append(Listing(
address=slurp(listing, "address", {"data-test":"property-card-addr"})[0].get_text(),
bedrooms=beds,
bathrooms=baths,
sqft=sqft,
price=slurp(listing, "span", {"data-test":"property-card-price"})[0].get_text(),
link=slurp(listing, "a", {"data-test":"property-card-link"})[0].get('href'),
))
return temp_listings
def setup_url(self, url):
parsed_url = urlparse(url)
print(parsed_url)
if not parsed_url.netloc:
return urlparse(f"{self.parsed_original_url.scheme}://{self.parsed_original_url.netloc}{parsed_url.path}{self.parsed_original_url.query}{self.parsed_original_url.params}")
#create dataframe
def etl(response):
#regex to find the data
for listing in listings:
print("--------")
print(listing)
print("--------")
print("FORCE STOP")
exit()
#convert text to dict via json
dicts=[json.loads('{'+i+'}') for i in num]
#create dataframe
df=pd.DataFrame()
for ind,val in enumerate(text):
df[val]=dicts[ind].values()
df.index=dicts[ind].keys()
return df
def main():
#scrapper = ScrapeZillowListings('https://www.zillow.com/westchester-county-ny/?searchQueryState=%7B%22usersSearchTerm%22%3A%22Yorktown%20Heights%2C%20NY%22%2C%22mapBounds%22%3A%7B%22north%22%3A41.69948153143324%2C%22east%22%3A-72.68804025585938%2C%22south%22%3A40.83865274682678%2C%22west%22%3A-74.29479074414063%7D%2C%22isMapVisible%22%3Atrue%2C%22filterState%22%3A%7B%22price%22%3A%7B%22max%22%3A250000%7D%2C%22ah%22%3A%7B%22value%22%3Atrue%7D%2C%22sort%22%3A%7B%22value%22%3A%22days%22%7D%2C%22land%22%3A%7B%22value%22%3Afalse%7D%2C%22cmsn%22%3A%7B%22value%22%3Afalse%7D%2C%22sche%22%3A%7B%22value%22%3Afalse%7D%2C%22schm%22%3A%7B%22value%22%3Afalse%7D%2C%22schh%22%3A%7B%22value%22%3Afalse%7D%2C%22schp%22%3A%7B%22value%22%3Afalse%7D%2C%22schr%22%3A%7B%22value%22%3Afalse%7D%2C%22schc%22%3A%7B%22value%22%3Afalse%7D%2C%22schu%22%3A%7B%22value%22%3Afalse%7D%7D%2C%22isListVisible%22%3Atrue%2C%22regionSelection%22%3A%5B%7B%22regionId%22%3A3148%2C%22regionType%22%3A4%7D%2C%7B%22regionId%22%3A2694%2C%22regionType%22%3A4%7D%5D%2C%22pagination%22%3A%7B%7D%7D')
scrapper = ScrapeZillowListings("https://www.zillow.com/search/GetSearchPageState.htm?searchQueryState=^%^7B^%^22pagination^%^22^%^3A^%^7B^%^7D^%^2C^%^22usersSearchTerm^%^22^%^3A^%^22Yorktown^%^20Heights^%^2C^%^20NY^%^22^%^2C^%^22mapBounds^%^22^%^3A^%^7B^%^22north^%^22^%^3A42.99146217894271^%^2C^%^22east^%^22^%^3A-70.80209903627659^%^2C^%^22south^%^22^%^3A39.549453943310084^%^2C^%^22west^%^22^%^3A-77.00937442690159^%^7D^%^2C^%^22mapZoom^%^22^%^3A8^%^2C^%^22regionSelection^%^22^%^3A^%^5B^%^7B^%^22regionId^%^22^%^3A3148^%^2C^%^22regionType^%^22^%^3A4^%^7D^%^2C^%^7B^%^22regionId^%^22^%^3A2694^%^2C^%^22regionType^%^22^%^3A4^%^7D^%^5D^%^2C^%^22isMapVisible^%^22^%^3Atrue^%^2C^%^22filterState^%^22^%^3A^%^7B^%^22price^%^22^%^3A^%^7B^%^22max^%^22^%^3A250000^%^7D^%^2C^%^22isAllHomes^%^22^%^3A^%^7B^%^22value^%^22^%^3Atrue^%^7D^%^2C^%^22sortSelection^%^22^%^3A^%^7B^%^22value^%^22^%^3A^%^22days^%^22^%^7D^%^2C^%^22isLotLand^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isMiddleSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isHighSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22includeUnratedSchools^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isComingSoon^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isPublicSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isPrivateSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isElementarySchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isCharterSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^7D^%^2C^%^22isListVisible^%^22^%^3Atrue^%^7D&wants=^\{^%^22cat1^%^22:^\[^%^22mapResults^%^22^\]^\}&requestId=3")
listings = scrapper.run()
scrapper.print_listings()
#df=etl(response)
return
if __name__ == "__main__":
main()
#curl "https://www.zillow.com/search/GetSearchPageState.htm?searchQueryState=^%^7B^%^22pagination^%^22^%^3A^%^7B^%^7D^%^2C^%^22usersSearchTerm^%^22^%^3A^%^22Yorktown^%^20Heights^%^2C^%^20NY^%^22^%^2C^%^22mapBounds^%^22^%^3A^%^7B^%^22north^%^22^%^3A42.99146217894271^%^2C^%^22east^%^22^%^3A-70.80209903627659^%^2C^%^22south^%^22^%^3A39.549453943310084^%^2C^%^22west^%^22^%^3A-77.00937442690159^%^7D^%^2C^%^22mapZoom^%^22^%^3A8^%^2C^%^22regionSelection^%^22^%^3A^%^5B^%^7B^%^22regionId^%^22^%^3A3148^%^2C^%^22regionType^%^22^%^3A4^%^7D^%^2C^%^7B^%^22regionId^%^22^%^3A2694^%^2C^%^22regionType^%^22^%^3A4^%^7D^%^5D^%^2C^%^22isMapVisible^%^22^%^3Atrue^%^2C^%^22filterState^%^22^%^3A^%^7B^%^22price^%^22^%^3A^%^7B^%^22max^%^22^%^3A250000^%^7D^%^2C^%^22isAllHomes^%^22^%^3A^%^7B^%^22value^%^22^%^3Atrue^%^7D^%^2C^%^22sortSelection^%^22^%^3A^%^7B^%^22value^%^22^%^3A^%^22days^%^22^%^7D^%^2C^%^22isLotLand^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isMiddleSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isHighSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22includeUnratedSchools^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isComingSoon^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isPublicSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isPrivateSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isElementarySchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isCharterSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^7D^%^2C^%^22isListVisible^%^22^%^3Atrue^%^7D&wants=^\{^%^22cat1^%^22:^\[^%^22mapResults^%^22^\]^\}&requestId=3",
# "authority: www.zillow.com",
# "accept: */*",
# "accept-language: en-US,en;q=0.9",
# "cache-control: no-cache",
# "cookie: JSESSIONID=97FD1EB701E102B7353E8EA4528843CE; zguid=24^|^%^24825bd6e9-4f90-46df-a475-4a9910b5847c; zgsession=1^|a6a5b7ca-c651-45a2-93c8-c5b66fea68d3; AWSALB=oQ3DGTMPgyQOTPA6zLmQ0liqJ1oax2QoQ5rUSCsORkWP52C7k6G8H1gZnlxOtgU/zzO503UHUnQ7tUeivhOnupv7aYI6+E5LxUZl4TeE0JyhvT3pZ6LYeC9iFbTw; AWSALBCORS=oQ3DGTMPgyQOTPA6zLmQ0liqJ1oax2QoQ5rUSCsORkWP52C7k6G8H1gZnlxOtgU/zzO503UHUnQ7tUeivhOnupv7aYI6+E5LxUZl4TeE0JyhvT3pZ6LYeC9iFbTw; search=6^|1689549806090^%^7Crect^%^3D42.92311815473404^%^252C-70.80209903627659^%^252C39.62142250427077^%^252C-77.00937442690159^%^26rid^%^3D2694^%^26disp^%^3Dmap^%^26mdm^%^3Dauto^%^26p^%^3D1^%^26sort^%^3Ddays^%^26z^%^3D1^%^26listPriceActive^%^3D1^%^26type^%^3Dhouse^%^252Ccondo^%^252Capartment_duplex^%^252Cmobile^%^252Ctownhouse^%^26lt^%^3Dfsba^%^252Cfsbo^%^252Cfore^%^252Cnew^%^252Cauction^%^26price^%^3D0-250000^%^26fs^%^3D1^%^26fr^%^3D0^%^26mmm^%^3D0^%^26rs^%^3D0^%^26ah^%^3D0^%^26singlestory^%^3D0^%^26housing-connector^%^3D0^%^26abo^%^3D0^%^26garage^%^3D0^%^26pool^%^3D0^%^26ac^%^3D0^%^26waterfront^%^3D0^%^26finished^%^3D0^%^26unfinished^%^3D0^%^26cityview^%^3D0^%^26mountainview^%^3D0^%^26parkview^%^3D0^%^26waterview^%^3D0^%^26hoadata^%^3D1^%^26zillow-owned^%^3D0^%^263dhome^%^3D0^%^26featuredMultiFamilyBuilding^%^3D0^%^26commuteMode^%^3Ddriving^%^26commuteTimeOfDay^%^3Dnow^%^09^%^092694^%^09^%^09^%^09^%^09^%^09^%^09",
# "pragma: no-cache",
# "sec-ch-ua: ^\^"Not.A/Brand^\^";v=^\^"8^\^", ^\^"Chromium^\^";v=^\^"114^\^", ^\^"Google Chrome^\^";v=^\^"114^\^"",
# "sec-ch-ua-mobile: ?0",
# "sec-ch-ua-platform: ^\^"Windows^\^"",
# "sec-fetch-dest: empty",
# "sec-fetch-mode: cors",
# "sec-fetch-site: same-origin",
# "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
# "x-kl-ajax-request: Ajax_Request",
# --compressed

View File

@@ -1,4 +0,0 @@
pandas
requests
fake-useragent
beautifulsoup4

View File

@@ -21,7 +21,7 @@
"jsdoc": "^4.0.2",
"jsonfile": "^6.1.0",
"morgan": "^1.10.0",
"mysql": "^2.18.1",
"mysql2": "^3.3.5",
"node-html-markdown": "^1.3.0",
"node-html-parser": "^6.1.5",
"openai": "^3.2.1",

View File

@@ -1,46 +0,0 @@
const { SlashCommandBuilder, SlashCommandStringOption } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { BufferToJson } = require("../utilities/utils");
const log = new DebugBuilder("server", "customSlashCommandBuilder");
const { getAllNodes, getAllNodesSync } = require("../utilities/mysqlHandler");
exports.customSlashCommandBuilder = class customSlashCommandBuilder extends SlashCommandBuilder {
constructor() {
super();
}
async addAllSystemPresetOptions() {
const nodeObjects = await new Promise((recordResolve, recordReject) => {
getAllNodes((nodeRows) => {
recordResolve(nodeRows);
});
});
log.DEBUG("Node objects: ", nodeObjects);
var presetsAvailable = [];
for (const nodeObject of nodeObjects) {
log.DEBUG("Node object: ", nodeObject);
for (const presetName in nodeObject.nearbySystems) presetsAvailable.push(nodeObject.nearbySystems[presetName]);
}
log.DEBUG("All Presets available: ", presetsAvailable);
// Remove duplicates
presetsAvailable = [...new Set(presetsAvailable)];
log.DEBUG("DeDuped Presets available: ", presetsAvailable);
this.addStringOption(option => option.setName("preset").setRequired(true).setDescription("The channels"));
for (const preset of presetsAvailable){
log.DEBUG("Preset: ", preset);
this.options[0].addChoices({
'name': String(preset),
'value': String(preset)
});
}
log.DEBUG("Preset Options: ", this);
return this;
}
}

View File

@@ -4,6 +4,7 @@ const debug = require('debug');
require('dotenv').config();
// Modules
const { writeFile } = require('fs');
const { inspect } = require('util');
const logLocation = process.env.LOG_LOCATION;
@@ -34,31 +35,31 @@ exports.DebugBuilder = class DebugBuilder {
this.INFO = (...messageParts) => {
const _info = debug(`${appName}:${fileName}:INFO`);
_info(messageParts);
writeToLog(`${appName}:${fileName}:INFO\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:INFO\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
}
this.DEBUG = (...messageParts) => {
const _debug = debug(`${appName}:${fileName}:DEBUG`);
_debug(messageParts);
writeToLog(`${appName}:${fileName}:DEBUG\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:DEBUG\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
}
this.VERBOSE = (...messageParts) => {
const _verbose = debug(`${appName}:${fileName}:VERBOSE`);
_verbose(messageParts);
writeToLog(`${appName}:${fileName}:VERBOSE\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:VERBOSE\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
}
this.WARN = (...messageParts) => {
const _warn = debug(`${appName}:${fileName}:WARNING`);
_warn(messageParts);
writeToLog(`${appName}:${fileName}:WARNING\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:WARNING\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
}
this.ERROR = (...messageParts) => {
const _error = debug(`${appName}:${fileName}:ERROR`);
_error(messageParts);
writeToLog(`${appName}:${fileName}:ERROR\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:ERROR\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
if (process.env.EXIT_ON_ERROR && process.env.EXIT_ON_ERROR > 0) {
writeToLog("!--- EXITING ---!", appName);
setTimeout(process.exit, process.env.EXIT_ON_ERROR_DELAY ?? 0);

View File

@@ -11,14 +11,14 @@ const path = require('node:path');
const { DebugBuilder } = require("./debugBuilder");
const log = new DebugBuilder("server", "deployCommands");
const commands = [];
var commands = [];
// Grab all the command files from the commands directory you created earlier
const commandsPath = path.resolve(__dirname, '../commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
exports.deploy = (clientId, guildIDs) => {
log.DEBUG("Deploying commands for: ", guildIDs);
if (Array.isArray(guildIDs)) guildIDs = [guildIDs];
if (!Array.isArray(guildIDs)) guildIDs = [guildIDs];
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
for (const file of commandFiles) {
const command = require(`${path.resolve(commandsPath, file)}`);
@@ -48,3 +48,35 @@ exports.deploy = (clientId, guildIDs) => {
})()
}
};
/**
* Remove all commands for a given bot in a given guild
*
* @param {*} clientId The client ID of the bot to remove commands from
* @param {*} guildId The ID of the guild to remove the bot commands from
*/
exports.removeAll = (clientId, guildId) => {
if (!Array.isArray(guildId)) guildIDs = [guildId];
log.DEBUG("Removing commands for: ", clientId, guildIDs);
commands = [];
const rest = new REST({ version: '10' }).setToken(token);
for (const guildId of guildIDs){
(async () => {
try {
log.DEBUG(`Started refreshing ${commands.length} application (/) commands for guild ID: ${guildId}.`);
// The put method is used to fully refresh all commands in the guild with the current set
const data = await rest.put(
Routes.applicationGuildCommands(clientId, guildId),
{ body: commands },
);
log.DEBUG(`Successfully reloaded ${data.length} application (/) commands for guild ID: ${guildId}.`);
} catch (error) {
// And of course, make sure you catch and log any errors!
log.ERROR("ERROR Deploying commands: ", error, "Body from error: ", commands);
}
})()
}
}

View File

@@ -1,5 +1,5 @@
require('dotenv').config();
const mysql = require('mysql');
const mysql = require('mysql2');
const utils = require('./utils');
const { nodeObject, clientObject, connectionObject } = require("./recordHelper");
const { DebugBuilder } = require("../utilities/debugBuilder");
@@ -110,7 +110,7 @@ exports.getOnlineNodes = (callback) => {
* @param callback Callback function
*/
async function getNodeInfoFromId(nodeId, callback = undefined) {
if (!nodeId) throw new Error("No node ID given when trying to fetch node");
if (!nodeId || nodeId == '0' || nodeId == 0 ) throw new Error("No node ID given when trying to fetch node");
log.DEBUG("Getting node from ID: ", nodeId);
const sqlQuery = `SELECT * FROM ${nodesTable} WHERE id = ${nodeId}`

View File

@@ -3,6 +3,7 @@ const { DebugBuilder } = require("../utilities/debugBuilder");
const { clientObject } = require("./recordHelper");
const { readFileSync } = require('fs');
const log = new DebugBuilder("server", "utils");
const logAC = new DebugBuilder("server", "command-autocorrect");
const path = require('path');
// Convert a JSON object to a buffer for the DB
@@ -117,3 +118,19 @@ exports.getClientObjectByClientID = (clientId) => {
}
return undefined
}
exports.filterAutocompleteValues = async (interaction, options) => {
// Get the command used
const command = interaction.command;
// Find values that start with what the user is entering
const focusedValue = interaction.options.getFocused();
const filtered = options.filter(preset => preset.startsWith(focusedValue));
// Give the query response to the user
logAC.DEBUG("Focused Value: ", command, focusedValue, options, filtered);
await interaction.respond(
filtered.map(option => ({ name: option, value: option })),
);
}