├── .gitignore ├── config.example ├── noProto.json ├── links.json ├── scripts.json ├── roles.json ├── scripts.examples.json ├── queries.json └── config.json ├── commands ├── eventsCommand.js ├── helpCommand.js ├── geofenceCommand.js ├── noProtoCommand.js ├── scriptCommand.js ├── sendWorkerCommand.js ├── devicesCommand.js ├── systemStatsCommand.js ├── truncateCommand.js ├── grepCommand.js ├── linksCommand.js ├── pm2Command.js └── queryCommand.js ├── package.json ├── functions ├── links.js ├── slashRegistry.js ├── pogoDroid.js ├── help.js ├── queries.js ├── generateMadInfo.js ├── roles.js ├── geofenceConverter.js ├── events.js ├── deviceControl.js ├── pm2.js ├── scripts.js ├── interactions.js ├── truncate.js └── devices.js ├── madgruber.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | commands/boardCommand.js 3 | -------------------------------------------------------------------------------- /config.example/noProto.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "channelID": "", 4 | "deviceNames": ["ATV01", "ATV02"] 5 | }, 6 | { 7 | "channelID": "", 8 | "deviceNames": ["ATV03", "ATV04"] 9 | } 10 | ] -------------------------------------------------------------------------------- /config.example/links.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "label": "", 3 | "url": "", 4 | "emoji": "" 5 | }, { 6 | "label": "", 7 | "url": "", 8 | "emoji": "" 9 | }, { 10 | "label": "", 11 | "url": "", 12 | "emoji": "" 13 | }, { 14 | "label": "", 15 | "url": "", 16 | "emoji": "" 17 | }, { 18 | "label": "", 19 | "url": "", 20 | "emoji": "" 21 | }] -------------------------------------------------------------------------------- /commands/eventsCommand.js: -------------------------------------------------------------------------------- 1 | const { 2 | SlashCommandBuilder 3 | } = require('discord.js'); 4 | const fs = require('fs'); 5 | const Events = require('../functions/events.js'); 6 | const config = require('../config/config.json') 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName(config.discord.eventsCommand.toLowerCase().replaceAll(/[^a-z0-9]/gi, '_')) 11 | .setDescription("Get list of events that will reroll quests"), 12 | 13 | async execute(client, interaction) { 14 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 15 | if (config.truncate.eventGuildID) { 16 | interaction.deferReply() 17 | Events.listEvents(client, channel); 18 | } 19 | return "delete"; 20 | }, 21 | }; -------------------------------------------------------------------------------- /config.example/scripts.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "customName": "", 3 | "adminOnly": false, 4 | "description": "", 5 | "fullFilePath": "", 6 | "variables": [{ 7 | "varDescription": "", 8 | "varOptions": [] 9 | }, 10 | { 11 | "varDescription": "", 12 | "varOptions": [] 13 | }, 14 | { 15 | "varDescription": "", 16 | "varOptions": [] 17 | } 18 | ] 19 | }, { 20 | "customName": "", 21 | "adminOnly": false, 22 | "description": "", 23 | "fullFilePath": "", 24 | "variables": [] 25 | }, { 26 | "customName": "", 27 | "adminOnly": false, 28 | "description": "", 29 | "fullFilePath": "", 30 | "variables": [] 31 | }] -------------------------------------------------------------------------------- /commands/helpCommand.js: -------------------------------------------------------------------------------- 1 | const { 2 | SlashCommandBuilder 3 | } = require('discord.js'); 4 | const fs = require('fs'); 5 | const Help = require('../functions/help.js'); 6 | const config = require('../config/config.json'); 7 | 8 | module.exports = { 9 | data: new SlashCommandBuilder() 10 | .setName(config.discord.helpCommand.toLowerCase().replaceAll(/[^a-z0-9]/gi, '_')) 11 | .setDescription("Show help menu and user perms"), 12 | 13 | async execute(client, interaction) { 14 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 15 | let guild = await client.guilds.fetch(interaction.guildId).catch(console.error); 16 | interaction.deferReply(); 17 | Help.helpMenu(client, channel, guild, interaction.user); 18 | return "delete"; 19 | }, 20 | }; -------------------------------------------------------------------------------- /config.example/roles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "messageID": "", 4 | "roles": [ 5 | { 6 | "roleID": "", 7 | "emojiName": "" 8 | }, 9 | { 10 | "roleID": "", 11 | "emojiName": "" 12 | }, 13 | { 14 | "roleID": "", 15 | "emojiName": "" 16 | } 17 | ] 18 | }, 19 | { 20 | "messageID": "", 21 | "roles": [ 22 | { 23 | "roleID": "", 24 | "emojiName": "" 25 | }, 26 | { 27 | "roleID": "", 28 | "emojiName": "" 29 | }, 30 | { 31 | "roleID": "", 32 | "emojiName": "" 33 | } 34 | ] 35 | } 36 | ] -------------------------------------------------------------------------------- /config.example/scripts.examples.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "customName": "DeviceControl", 3 | "adminOnly": false, 4 | "description": "Alter PoE ports", 5 | "fullFilePath": "/home/mad/relay_poe_control.sh", 6 | "variables": [{ 7 | "varDescription": "Choose which PoE:", 8 | "varOptions": ["poe1", "poe2", "poe3", "poe4"] 9 | }, 10 | { 11 | "varDescription": "Choose action type:", 12 | "varOptions": ["start", "stop", "cycle"] 13 | }, 14 | { 15 | "varDescription": "Choose which port:", 16 | "varOptions": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] 17 | } 18 | ] 19 | }, { 20 | "customName": "Annoying Device", 21 | "adminOnly": false, 22 | "description": "PoE cycle 7", 23 | "fullFilePath": "/home/mad/relay_poe_control.sh poe1 cycle 7", 24 | "variables": [] 25 | }, { 26 | "customName": "Skynet", 27 | "adminOnly": false, 28 | "description": "Hasta la vista, baby", 29 | "fullFilePath": "/home/mad/activateSkynet.sh", 30 | "variables": [] 31 | }] -------------------------------------------------------------------------------- /commands/geofenceCommand.js: -------------------------------------------------------------------------------- 1 | const { 2 | SlashCommandBuilder 3 | } = require('discord.js'); 4 | const fs = require('fs'); 5 | const Roles = require('../functions/roles.js'); 6 | const Geofences = require('../functions/geofenceConverter.js'); 7 | const config = require('../config/config.json'); 8 | 9 | module.exports = { 10 | data: new SlashCommandBuilder() 11 | .setName(config.discord.geofenceCommand.toLowerCase().replaceAll(/[^a-z0-9]/gi, '_')) 12 | .setDescription("Convert MAD geofences"), 13 | 14 | async execute(client, interaction) { 15 | interaction.deferReply(); 16 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 17 | let guild = await client.guilds.fetch(interaction.guildId).catch(console.error); 18 | let userPerms = await Roles.getUserCommandPerms(guild, interaction.user); 19 | if (userPerms.includes('admin')) { 20 | Geofences.converterMain(channel, interaction.user); 21 | } else { 22 | channel.send(`User *${interaction.user.username}* does not have required admin perms to convert geofences`).catch(console.error); 23 | } 24 | return "delete"; 25 | }, 26 | }; -------------------------------------------------------------------------------- /commands/noProtoCommand.js: -------------------------------------------------------------------------------- 1 | const { 2 | SlashCommandBuilder 3 | } = require('discord.js'); 4 | const fs = require('fs'); 5 | const Roles = require('../functions/roles.js'); 6 | const Devices = require('../functions/devices.js'); 7 | const config = require('../config/config.json'); 8 | 9 | module.exports = { 10 | data: new SlashCommandBuilder() 11 | .setName(config.discord.noProtoCommand.toLowerCase().replaceAll(/[^a-z0-9]/gi, '_')) 12 | .setDescription("Show noProto devices"), 13 | 14 | async execute(client, interaction) { 15 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 16 | let guild = await client.guilds.fetch(interaction.guildId).catch(console.error); 17 | let userPerms = await Roles.getUserCommandPerms(guild, interaction.user); 18 | if (userPerms.includes('admin') || userPerms.includes('deviceInfoControl') || userPerms.includes('deviceInfo')) { 19 | interaction.deferReply(); 20 | Devices.noProtoDevices(client, channel, interaction.user, 'search'); 21 | } //End of if userPerms 22 | else { 23 | channel.send(`User *${interaction.user.username}* does not have required noProto perms`).catch(console.error); 24 | } 25 | return "delete"; 26 | }, 27 | }; -------------------------------------------------------------------------------- /commands/scriptCommand.js: -------------------------------------------------------------------------------- 1 | const { 2 | EmbedBuilder, 3 | SlashCommandBuilder 4 | } = require('discord.js'); 5 | const fs = require('fs'); 6 | const Roles = require('../functions/roles.js'); 7 | const Scripts = require('../functions/scripts.js'); 8 | const scriptList = JSON.parse(fs.readFileSync('./config/scripts.json')); 9 | const config = require('../config/config.json'); 10 | 11 | module.exports = { 12 | data: new SlashCommandBuilder() 13 | .setName(config.discord.scriptCommand.toLowerCase().replaceAll(/[^a-z0-9]/gi, '_')) 14 | .setDescription(`Get list of scripts`), 15 | 16 | async execute(client, interaction) { 17 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 18 | let guild = await client.guilds.fetch(interaction.guildId).catch(console.error); 19 | let userPerms = await Roles.getUserCommandPerms(guild, interaction.user); 20 | if (userPerms.includes('admin') || userPerms.includes('scripts')) { 21 | interaction.deferReply(); 22 | Scripts.sendScriptList(interaction, 'new') 23 | } //End of if userPerms 24 | else { 25 | channel.send(`User *${interaction.user.username}* does not have required script perms`).catch(console.error); 26 | } 27 | return "delete"; 28 | }, //End of execute() 29 | }; -------------------------------------------------------------------------------- /config.example/queries.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom": [{ 3 | "name": "Active Pokemon (All)", 4 | "query": "SELECT count(*) FROM pokemon WHERE encounter_id IS NOT NULL AND disappear_time >= utc_timestamp();" 5 | }, 6 | { 7 | "name": "Active Pokemon (IV)", 8 | "query": "SELECT count(*) FROM pokemon WHERE encounter_id IS NOT NULL AND cp IS NOT NULL AND disappear_time >= utc_timestamp();" 9 | }, 10 | { 11 | "name": "Gym Count", 12 | "query": "SELECT count(*) FROM gym; SELECT count(*) FROM gym WHERE team_id = 0; SELECT count(*) FROM gym WHERE team_id = 1; SELECT count(*) FROM gym WHERE team_id = 2; SELECT count(*) FROM gym WHERE team_id = 3; " 13 | }, 14 | { 15 | "name": "Spawn History Count", 16 | "query": "SELECT count(*) FROM pokemon;" 17 | }, 18 | { 19 | "name": "Pokestop Count", 20 | "query": "SELECT count(*) FROM pokestop;" 21 | }, 22 | { 23 | "name": "Quest Count", 24 | "query": "SELECT count(*) FROM trs_quest;" 25 | }, 26 | { 27 | "name": "Spawnpoint Count", 28 | "query": "SELECT count(*) FROM trs_spawn;" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "madgruber", 3 | "version": "6.3.5", 4 | "description": "Discord bot for controlling MAD, PM2 processes, and whatever else this idiot comes up with to add.", 5 | "main": "madgruber.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/RagingRectangle/MadGruber.git" 12 | }, 13 | "author": "RagingRectangle", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/RagingRectangle/MadGruber/issues" 17 | }, 18 | "homepage": "https://github.com/RagingRectangle/MadGruber#readme", 19 | "dependencies": { 20 | "@discordjs/rest": "0.4.1", 21 | "ansi-parser": "^3.2.10", 22 | "color-convert": "^2.0.1", 23 | "cron": "^1.8.2", 24 | "discord-api-types": "0.36.2", 25 | "discord.js": "14.0.1", 26 | "file-exists": "^5.0.1", 27 | "fs": "0.0.1-security", 28 | "geolocation-utils": "^1.2.5", 29 | "moment": "^2.29.1", 30 | "mysql": "^2.18.1", 31 | "mysql2": "^2.3.3", 32 | "node-fetch": "2.6.1", 33 | "pm2": "^5.1.2", 34 | "quickchart-js": "^2.0.3", 35 | "request": "^2.88.2", 36 | "reverse-line-reader": "^0.2.6", 37 | "shelljs": "^0.8.4", 38 | "sort-by": "^1.2.0" 39 | } 40 | } -------------------------------------------------------------------------------- /commands/sendWorkerCommand.js: -------------------------------------------------------------------------------- 1 | const { 2 | SlashCommandBuilder 3 | } = require('discord.js'); 4 | const fs = require('fs'); 5 | const Roles = require('../functions/roles.js'); 6 | const DeviceControl = require('../functions/deviceControl.js'); 7 | const config = require('../config/config.json'); 8 | 9 | module.exports = { 10 | data: new SlashCommandBuilder() 11 | .setName(config.discord.sendWorkerCommand.toLowerCase().replaceAll(/[^a-z0-9]/gi, '_')) 12 | .setDescription(`Send worker to location`) 13 | .addStringOption(option => 14 | option.setName('coordinates') 15 | .setDescription('Set worker location') 16 | .setRequired(true)), 17 | 18 | async execute(client, interaction) { 19 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 20 | let guild = await client.guilds.fetch(interaction.guildId).catch(console.error); 21 | let userPerms = await Roles.getUserCommandPerms(guild, interaction.user); 22 | if (userPerms.includes('admin') || userPerms.includes('deviceInfoControl')) { 23 | interaction.deferReply(); 24 | if (config.stats.database.host && config.deviceControl.path && config.discord.sendWorkerCommand) { 25 | let coords = `${interaction.options._hoistedOptions[0]['value']},${interaction.options._hoistedOptions[1]['value']}`; 26 | DeviceControl.sendWorker(client, channel, interaction.user, coords); 27 | } else { 28 | channel.send(`DeviceControl information missing from config.`); 29 | } 30 | } //End of if userPerms 31 | else { 32 | channel.send(`User *${interaction.user.username}* does not have required sendWorker perms`).catch(console.error); 33 | } 34 | return "delete"; 35 | }, //End of execute() 36 | }; -------------------------------------------------------------------------------- /functions/links.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | EmbedBuilder, 11 | ButtonBuilder, 12 | ButtonStyle, 13 | InteractionType, 14 | ChannelType 15 | } = require('discord.js'); 16 | const linksList = require('../config/links.json'); 17 | 18 | module.exports = { 19 | links: async function links(client, channel) { 20 | var buttonList = []; 21 | linksList.forEach(link => { 22 | if (link.url && link.label) { 23 | var button = new ButtonBuilder() 24 | .setURL(link.url) 25 | .setLabel(link.label) 26 | .setStyle(ButtonStyle.Link) 27 | if (link.emoji){ 28 | button.setEmoji(link.emoji); 29 | } 30 | buttonList.push(button); 31 | } 32 | }); 33 | if (buttonList.length == 0) { 34 | channel.send("No links are set in config.").catch(console.error); 35 | return; 36 | } 37 | let rowsNeeded = Math.ceil(buttonList.length / 5); 38 | let buttonsNeeded = buttonList.length; 39 | var buttonCount = 0; 40 | var messageComponents = []; 41 | for (var n = 0; n < rowsNeeded && n < 5; n++) { 42 | var buttonRow = new ActionRowBuilder() 43 | for (var r = 0; r < 5; r++) { 44 | if (buttonCount < buttonsNeeded) { 45 | buttonRow.addComponents(buttonList[buttonCount]); 46 | buttonCount++; 47 | } 48 | } //End of r loop 49 | messageComponents.push(buttonRow); 50 | } //End of n loop 51 | channel.send({ 52 | content: `Click to open:`, 53 | components: messageComponents 54 | }).catch(console.error); 55 | } //End of links() 56 | } -------------------------------------------------------------------------------- /commands/devicesCommand.js: -------------------------------------------------------------------------------- 1 | const { 2 | SlashCommandBuilder 3 | } = require('discord.js'); 4 | const fs = require('fs'); 5 | const Roles = require('../functions/roles.js'); 6 | const Devices = require('../functions/devices.js'); 7 | const config = require('../config/config.json'); 8 | let dbInfo = require('../MAD_Database_Info.json'); 9 | 10 | module.exports = { 11 | data: new SlashCommandBuilder() 12 | .setName(config.discord.devicesCommand.toLowerCase().replaceAll(/[^a-z0-9]/gi, '_')) 13 | .setDescription("Show status of devices") 14 | .addStringOption(option => 15 | option 16 | .setName('optional-device') 17 | .setDescription('Name of device')), 18 | 19 | async execute(client, interaction) { 20 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 21 | let guild = await client.guilds.fetch(interaction.guildId).catch(console.error); 22 | let userPerms = await Roles.getUserCommandPerms(guild, interaction.user); 23 | if (userPerms.includes('admin') || userPerms.includes('deviceInfoControl')) { 24 | interaction.deferReply(); 25 | var deviceID = ''; 26 | var specificCheck = false; 27 | try { 28 | if (interaction.options._hoistedOptions[0]['value']) { 29 | specificCheck = true; 30 | deviceID = interaction.options._hoistedOptions[0]['value']; 31 | console.log(deviceID) 32 | } 33 | } catch (err) {} 34 | if (specificCheck === true) { 35 | for (const [key, value] of Object.entries(dbInfo.devices)) { 36 | if (deviceID === value.name.toLowerCase()) { 37 | Devices.getDeviceInfo(channel, interaction.user, key); 38 | } 39 | } 40 | } else { 41 | Devices.deviceStatus(channel, interaction.user); 42 | } 43 | } else { 44 | channel.send(`User *${interaction.user.username}* does not have required device perms`).catch(console.error); 45 | } 46 | return "delete"; 47 | }, 48 | }; -------------------------------------------------------------------------------- /functions/slashRegistry.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | EmbedBuilder, 11 | ButtonBuilder, 12 | InteractionType, 13 | ChannelType 14 | } = require('discord.js'); 15 | 16 | const fs = require('fs'); 17 | const config = require('../config/config.json'); 18 | 19 | module.exports = { 20 | registerCommands: async function registerCommands(client) { 21 | var commands = []; 22 | const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js')); 23 | var finalCommands = []; 24 | const { 25 | REST 26 | } = require('@discordjs/rest'); 27 | const { 28 | Routes 29 | } = require('discord-api-types/v10'); 30 | for (const file of commandFiles) { 31 | if (config.discord[file.replace('.js', '')]) { 32 | const command = require(`../commands/${file}`); 33 | try { 34 | commands.push(command.data.toJSON()); 35 | finalCommands.push(file); 36 | } catch (err) { 37 | console.log(err); 38 | } 39 | } 40 | } 41 | for (const guildID of config.discord.slashGuildIDs) { 42 | const rest = new REST({ 43 | version: '10' 44 | }).setToken(config.discord.token); 45 | rest.put(Routes.applicationGuildCommands(client.user.id, guildID), { 46 | body: commands 47 | }) 48 | .then(() => console.log(`Registered slash commands for guild: ${guildID}`)) 49 | .catch(console.error); 50 | 51 | client.commands = new Collection(); 52 | for (const file of finalCommands) { 53 | const command = require(`../commands/${file}`); 54 | client.commands.set(command.data.name, command); 55 | } 56 | } //End of guildID 57 | } //End of registerCommands() 58 | } -------------------------------------------------------------------------------- /commands/systemStatsCommand.js: -------------------------------------------------------------------------------- 1 | const { 2 | SlashCommandBuilder 3 | } = require('discord.js'); 4 | const fs = require('fs'); 5 | const Roles = require('../functions/roles.js'); 6 | const Stats = require('../functions/stats.js'); 7 | const config = require('../config/config.json'); 8 | 9 | module.exports = { 10 | data: new SlashCommandBuilder() 11 | .setName(config.discord.systemStatsCommand.toLowerCase().replaceAll(/[^a-z0-9]/gi, '_')) 12 | .setDescription("Get system stats") 13 | .addStringOption(option => 14 | option 15 | .setName('type') 16 | .setDescription('Select type of stat') 17 | .setRequired(true) 18 | .addChoices({ 19 | name: `Despawn Time Left`, 20 | value: `despawn%` 21 | }, { 22 | name: `Hundos + Nundos + Shinies`, 23 | value: `hundoNundoShiny` 24 | }, { 25 | name: `Location Handling`, 26 | value: `locationHandling` 27 | }, { 28 | name: `Mons Scanned`, 29 | value: `monsScanned` 30 | }, { 31 | name: `Restarts + Reboots`, 32 | value: `restartsReboots` 33 | }, { 34 | name: `Uptime`, 35 | value: `uptime` 36 | })) 37 | .addStringOption(option => 38 | option 39 | .setName('rpl') 40 | .setDescription('Select report period length') 41 | .setRequired(true) 42 | .addChoices({ 43 | name: `Hourly`, 44 | value: `hourly` 45 | }, { 46 | name: `Daily`, 47 | value: `daily` 48 | })), 49 | 50 | async execute(client, interaction) { 51 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 52 | let guild = await client.guilds.fetch(interaction.guildId).catch(console.error); 53 | let userPerms = await Roles.getUserCommandPerms(guild, interaction.user); 54 | if (userPerms.includes('admin') || userPerms.includes('systemStats')) { 55 | interaction.deferReply(); 56 | let statType = interaction.options.getString('type'); 57 | let statDuration = interaction.options.getString('rpl'); 58 | Stats.systemStats(channel, interaction.user, statDuration, statType); 59 | } else { 60 | channel.send(`User *${interaction.user.username}* does not have required systemStats perms`).catch(console.error); 61 | } 62 | return "delete"; 63 | }, 64 | }; -------------------------------------------------------------------------------- /commands/truncateCommand.js: -------------------------------------------------------------------------------- 1 | const { 2 | EmbedBuilder, 3 | SlashCommandBuilder 4 | } = require('discord.js'); 5 | const fs = require('fs'); 6 | const Roles = require('../functions/roles.js'); 7 | const Truncate = require('../functions/truncate.js'); 8 | const config = require('../config/config.json'); 9 | 10 | module.exports = { 11 | data: new SlashCommandBuilder() 12 | .setName(config.discord.truncateCommand.toLowerCase().replaceAll(/[^a-z0-9]/gi, '_')) 13 | .setDescription("Get list of tables to truncate") 14 | .addStringOption(option => { 15 | option 16 | .setName('optional-table') 17 | .setDescription('Select table to truncate') 18 | for (var i = 0; i < config.truncate.truncateOptions.length; i++) { 19 | option.addChoices({ 20 | name: config.truncate.truncateOptions[i], 21 | value: config.truncate.truncateOptions[i] 22 | }); 23 | } 24 | return option; 25 | }), 26 | 27 | async execute(client, interaction) { 28 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 29 | let guild = await client.guilds.fetch(interaction.guildId).catch(console.error); 30 | let userPerms = await Roles.getUserCommandPerms(guild, interaction.user); 31 | if (userPerms.includes('admin') || userPerms.includes('truncate')) { 32 | interaction.deferReply(); 33 | var specificCheck = false; 34 | var tables = []; 35 | try { 36 | if (interaction.options._hoistedOptions[0]['value']) { 37 | specificCheck = true; 38 | tables = interaction.options._hoistedOptions[0]['value'].split('+'); 39 | } 40 | } catch (err) {} 41 | if (specificCheck === true) { 42 | if (config.truncate.truncateVerify === true) { 43 | Truncate.verifyTruncate(channel, interaction.user, tables); 44 | } else { 45 | channel.send({ 46 | embeds: [new EmbedBuilder().setTitle('Truncate Results:').setDescription('**Truncating...**')], 47 | components: [] 48 | }) 49 | .then(msg => { 50 | Truncate.truncateTables(msg, interaction.user, tables); 51 | }).catch(console.error); 52 | } 53 | } else { 54 | Truncate.sendTruncateMessage(channel); 55 | } 56 | } //End of if userPerms 57 | else { 58 | channel.send(`User *${interaction.user.username}* does not have required truncate perms`).catch(console.error); 59 | } 60 | return "delete"; 61 | }, 62 | }; -------------------------------------------------------------------------------- /commands/grepCommand.js: -------------------------------------------------------------------------------- 1 | const { 2 | MessageAttachment, 3 | SlashCommandBuilder 4 | } = require('discord.js'); 5 | const fs = require('fs'); 6 | const fetch = require('node-fetch'); 7 | const Roles = require('../functions/roles.js'); 8 | 9 | module.exports = { 10 | data: new SlashCommandBuilder() 11 | .setName('grep') 12 | .setDescription("Search file for lines that include string") 13 | .addStringOption(option => 14 | option 15 | .setName('search-string') 16 | .setDescription('Enter what should be searched for') 17 | .setRequired(true)) 18 | .addAttachmentOption(option => 19 | option 20 | .setName('file') 21 | .setDescription('Select the file to be searched') 22 | .setRequired(true)), 23 | 24 | async execute(client, interaction) { 25 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 26 | let guild = await client.guilds.fetch(interaction.guildId).catch(console.error); 27 | let userPerms = await Roles.getUserCommandPerms(guild, interaction.user); 28 | if (userPerms.includes('admin')) { 29 | interaction.deferReply({ 30 | ephemeral: true 31 | }); 32 | let searchString = interaction.options.getString('search-string').toLowerCase(); 33 | file = interaction.options.getAttachment('file'); 34 | console.log(`${interaction.user.username} searched ${file.name} for "${searchString}"`); 35 | await fetch(file.url) 36 | .then(function (response) { 37 | var destination = fs.createWriteStream(`./${file.name}.txt`); 38 | response.body.pipe(destination); 39 | }); 40 | await new Promise(done => setTimeout(done, 5000)); 41 | var linesFound = []; 42 | const textFile = fs.readFileSync(`./${file.name}.txt`, 'utf-8'); 43 | textFile.split(/\r?\n/).forEach(line => { 44 | if (line.toLowerCase().includes(searchString)) { 45 | linesFound.push(line); 46 | } 47 | }); 48 | 49 | fs.writeFileSync(`./${file.name}.txt`, linesFound.join('\n')); 50 | interaction.editReply({ 51 | files: [new MessageAttachment(`./${file.name}.txt`)], 52 | ephemeral: true 53 | }).catch(console.error); 54 | await new Promise(done => setTimeout(done, 5000)); 55 | try { 56 | fs.rmSync(`./${file.name}.txt`); 57 | } catch (err) {} 58 | } else { 59 | channel.send(`User *${interaction.user.username}* does not have required grep perms`).catch(console.error); 60 | } 61 | return ""; 62 | }, 63 | }; -------------------------------------------------------------------------------- /commands/linksCommand.js: -------------------------------------------------------------------------------- 1 | const { 2 | EmbedBuilder, 3 | SlashCommandBuilder 4 | } = require('discord.js'); 5 | const fs = require('fs'); 6 | const Roles = require('../functions/roles.js'); 7 | const Links = require('../functions/links.js'); 8 | const linksList = JSON.parse(fs.readFileSync('./config/links.json')); 9 | const config = require('../config/config.json'); 10 | 11 | module.exports = { 12 | data: new SlashCommandBuilder() 13 | .setName(config.discord.linksCommand.toLowerCase().replaceAll(/[^a-z0-9]/gi, '_')) 14 | .setDescription(`Get list of bookmarks`) 15 | .addStringOption(option => { 16 | option 17 | .setName('optional-shortcut') 18 | .setDescription('Get link to specific website') 19 | for (var i = 0; i < linksList.length; i++) { 20 | if (linksList[i]['label'] && linksList[i]['url']) { 21 | option.addChoices({ 22 | name: linksList[i]['label'], 23 | value: linksList[i]['url'] 24 | }); 25 | } 26 | } //End of i loop 27 | return option; 28 | }), 29 | 30 | async execute(client, interaction) { 31 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 32 | let guild = await client.guilds.fetch(interaction.guildId).catch(console.error); 33 | let userPerms = await Roles.getUserCommandPerms(guild, interaction.user); 34 | if (userPerms.includes('admin') || userPerms.includes('links')) { 35 | interaction.deferReply(); 36 | var specificCheck = false; 37 | var specificLabel = ''; 38 | var specificEmoji = ''; 39 | var specificURL = ''; 40 | try { 41 | if (interaction.options._hoistedOptions[0]['value']) { 42 | specificCheck = true; 43 | for (var i = 0; i < linksList.length; i++) { 44 | if (linksList[i]['url'] === interaction.options._hoistedOptions[0]['value']) { 45 | specificLabel = linksList[i]['label']; 46 | specificURL = linksList[i]['url']; 47 | specificEmoji = linksList[i]['emoji']; 48 | } 49 | } //End of i loop 50 | } 51 | } catch (err) {} 52 | if (specificCheck === true) { 53 | channel.send({ 54 | embeds: [new EmbedBuilder().setDescription(`${specificEmoji} [${specificLabel} Link](${specificURL})`)] 55 | }).catch(console.error); 56 | } else { 57 | Links.links(client, channel); 58 | } 59 | } //End of if userPerms 60 | else { 61 | channel.send(`User *${interaction.user.username}* does not have required link perms`).catch(console.error); 62 | } 63 | return "delete"; 64 | }, //End of execute() 65 | }; -------------------------------------------------------------------------------- /commands/pm2Command.js: -------------------------------------------------------------------------------- 1 | const { 2 | SlashCommandBuilder 3 | } = require('discord.js'); 4 | const fs = require('fs'); 5 | const pm2 = require('pm2'); 6 | const Roles = require('../functions/roles.js'); 7 | const PM2 = require('../functions/pm2.js'); 8 | const config = require('../config/config.json'); 9 | const dbInfo = require('../MAD_Database_Info.json'); 10 | 11 | module.exports = { 12 | data: new SlashCommandBuilder() 13 | .setName(config.discord.pm2Command.toLowerCase().replaceAll(/[^a-z0-9]/gi, '_')) 14 | .setDescription("Show PM2 controller") 15 | .addStringOption(option => { 16 | option 17 | .setName('restart') 18 | .setDescription('Restart PM2 Process') 19 | for (var i = 0; i < dbInfo.processList.length && i < 25; i++) { 20 | option.addChoices({ 21 | name: dbInfo.processList[i], 22 | value: dbInfo.processList[i] 23 | }); 24 | } 25 | return option; 26 | }) 27 | .addStringOption(option => { 28 | option 29 | .setName('start') 30 | .setDescription('Start PM2 Process') 31 | for (var i = 0; i < dbInfo.processList.length && i < 25; i++) { 32 | option.addChoices({ 33 | name: dbInfo.processList[i], 34 | value: dbInfo.processList[i] 35 | }); 36 | } 37 | return option; 38 | }) 39 | .addStringOption(option => { 40 | option 41 | .setName('stop') 42 | .setDescription('Stop PM2 Process') 43 | for (var i = 0; i < dbInfo.processList.length && i < 25; i++) { 44 | option.addChoices({ 45 | name: dbInfo.processList[i], 46 | value: dbInfo.processList[i] 47 | }); 48 | } 49 | return option; 50 | }), 51 | 52 | async execute(client, interaction) { 53 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 54 | let guild = await client.guilds.fetch(interaction.guildId).catch(console.error); 55 | let userPerms = await Roles.getUserCommandPerms(guild, interaction.user); 56 | if (userPerms.includes('admin') || userPerms.includes('pm2')) { 57 | interaction.deferReply(); 58 | let options = interaction.options._hoistedOptions; 59 | if (options.length == 0) { 60 | PM2.updateStatus(channel, 'new'); 61 | } else { 62 | for (var i = 0; i < options.length; i++) { 63 | PM2.runPM2(channel, interaction.user, `${options[i]['name']}~${options[i]['value']}`); 64 | } //End of i loop 65 | } 66 | //PM2.updateStatus(channel, 'new'); 67 | } //End of if userPerms 68 | else { 69 | channel.send(`User *${interaction.user.username}* does not have required PM2 perms`).catch(console.error); 70 | } 71 | return "delete"; 72 | }, 73 | }; -------------------------------------------------------------------------------- /functions/pogoDroid.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | Intents, 4 | MessageEmbed, 5 | Permissions, 6 | MessageActionRow, 7 | MessageSelectMenu, 8 | MessageButton 9 | } = require('discord.js'); 10 | const client = new Client({ 11 | intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_MESSAGE_REACTIONS, Intents.FLAGS.GUILD_MEMBERS, Intents.FLAGS.DIRECT_MESSAGES], 12 | partials: ['MESSAGE', 'CHANNEL', 'REACTION'], 13 | }); 14 | const request = require('request'); 15 | 16 | module.exports = { 17 | pogoDroid: async function pogoDroid(receivedMessage) { 18 | request('https://raw.githubusercontent.com/RagingRectangle/PD_Versions/main/versions.json', async function (err, response, html) { 19 | if (!err && response.statusCode == 200) { 20 | let versionList = await JSON.parse(html); 21 | createVersionButtons(versionList); 22 | } else { 23 | console.log("Failed to download pogoDroid versions", err); 24 | } 25 | }) //End of request() 26 | 27 | async function createVersionButtons(versionList) { 28 | var buttonList = []; 29 | versionList.forEach(version => { 30 | let button = new MessageButton() 31 | .setURL(`https://github.com/RagingRectangle/PD_Versions/raw/main/PogoDroid-${version}.apk`) 32 | .setLabel(version) 33 | .setStyle('LINK') 34 | buttonList.push(button); 35 | }); 36 | let buttonsNeeded = buttonList.length; 37 | let rowsNeeded = Math.ceil(buttonsNeeded / 5); 38 | var buttonCount = 0; 39 | var messageComponents = []; 40 | for (var n = 0; n < rowsNeeded && n < 5; n++) { 41 | var buttonRow = new MessageActionRow() 42 | for (var r = 0; r < 5; r++) { 43 | if (buttonCount < buttonsNeeded) { 44 | buttonRow.addComponents(buttonList[buttonCount]); 45 | buttonCount++; 46 | } 47 | } //End of r loop 48 | messageComponents.push(buttonRow); 49 | } //End of n loop 50 | sendVersionList(messageComponents); 51 | } //End of createVersionButtons 52 | 53 | async function sendVersionList(messageComponents) { 54 | receivedMessage.channel.send({ 55 | content: `**Available PogoDroid versions:**`, 56 | components: messageComponents 57 | }).catch(console.error); 58 | }//End of sendVersionList() 59 | } //End of pogoDroid() 60 | } -------------------------------------------------------------------------------- /functions/help.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | EmbedBuilder, 11 | ButtonBuilder, 12 | InteractionType, 13 | ChannelType 14 | } = require('discord.js'); 15 | const Roles = require('./roles.js'); 16 | const config = require('../config/config.json'); 17 | 18 | module.exports = { 19 | helpMenu: async function helpMenu(client, channel, guild, user) { 20 | let commands = config.discord; 21 | let prefix = config.discord.prefix; 22 | var pm2 = truncate = scripts = queries = links = devices = systemStats = sendWorker = events = 'N/A'; 23 | if (commands.pm2Command) { 24 | pm2 = `${prefix}${commands.pm2Command}`; 25 | } 26 | if (commands.truncateCommand) { 27 | truncate = `${prefix}${commands.truncateCommand}`; 28 | } 29 | if (commands.scriptCommand) { 30 | scripts = `${prefix}${commands.scriptCommand}`; 31 | } 32 | if (commands.queryCommand) { 33 | queries = `${prefix}${commands.queryCommand}`; 34 | } 35 | if (commands.linksCommand) { 36 | links = `${prefix}${commands.linksCommand}`; 37 | } 38 | if (commands.devicesCommand) { 39 | devices = `${prefix}${commands.devicesCommand}`; 40 | } 41 | if (commands.noProtoCommand) { 42 | noProto = `${prefix}${commands.noProtoCommand}`; 43 | } 44 | if (commands.systemStatsCommand) { 45 | systemStats = `${prefix}${commands.systemStatsCommand}`; 46 | } 47 | if (commands.sendWorkerCommand) { 48 | sendWorker = `${prefix}${commands.sendWorkerCommand}`; 49 | } 50 | if (commands.eventsCommand) { 51 | events = `${prefix}${commands.eventsCommand}`; 52 | } 53 | let userPerms = await Roles.getUserCommandPerms(guild, user); 54 | let authorName = user.username; 55 | var allowedCommands = `**${authorName} Permissions:**\n- ${userPerms.join('\n- ')}`; 56 | if (userPerms.length == 0) { 57 | allowedCommands = `**${authorName} Permissions:**\n- None`; 58 | } 59 | var description = `**Command Syntax:**\n- PM2: \`${pm2}\`\n- Truncate: \`${truncate}\`\n- Scripts: \`${scripts}\`\n- Queries: \`${queries}\`\n- Links: \`${links}\`\n- Devices: \`${devices}\`\n- Stats: \`${systemStats}\`\n- Send Worker: \`${sendWorker}\`\n- Events: \`${events}\`\n\n${allowedCommands}`; 60 | channel.send({ 61 | embeds: [new EmbedBuilder().setTitle("MadGruber Help Menu").setURL("https://github.com/RagingRectangle/MadGruber").setDescription(description)] 62 | }).catch(console.error); 63 | } //End of helpMenu() 64 | } -------------------------------------------------------------------------------- /commands/queryCommand.js: -------------------------------------------------------------------------------- 1 | const { 2 | SlashCommandBuilder 3 | } = require('discord.js'); 4 | const fs = require('fs'); 5 | const Roles = require('../functions/roles.js'); 6 | const Queries = require('../functions/queries.js'); 7 | const config = require('../config/config.json'); 8 | const queryConfig = require('../config/queries.json'); 9 | 10 | module.exports = { 11 | data: new SlashCommandBuilder() 12 | .setName(config.discord.queryCommand.toLowerCase().replaceAll(/[^a-z0-9]/gi, '_')) 13 | .setDescription("Run database query") 14 | .addStringOption(option => { 15 | option 16 | .setName('query-name') 17 | .setDescription('Select query') 18 | .setRequired(true) 19 | if (queryConfig.custom) { 20 | for (var i = 0; i < queryConfig.custom.length; i++) { 21 | option.addChoices({ 22 | name: queryConfig.custom[i]['name'], 23 | value: queryConfig.custom[i]['name'] 24 | }); 25 | } //End of i loop 26 | } else { 27 | console.log("MadGruber now has a new query config section. See here for info on how to update: https://discord.com/channels/923432745584177182/923445278995001354/1016092603067928637"); 28 | for (var i = 0; i < queryConfig.count.length; i++) { 29 | option.addChoices({ 30 | name: queryConfig.count[i]['type'], 31 | value: `SELECT COUNT(*) FROM ${queryConfig.count[i]['table']};` 32 | }); 33 | } //End of i loop 34 | } 35 | return option; 36 | }), 37 | 38 | async execute(client, interaction) { 39 | let channel = await client.channels.fetch(interaction.channelId).catch(console.error); 40 | let guild = await client.guilds.fetch(interaction.guildId).catch(console.error); 41 | let userPerms = await Roles.getUserCommandPerms(guild, interaction.user); 42 | if (userPerms.includes('admin') || userPerms.includes('queries')) { 43 | interaction.deferReply(); 44 | var specificCheck = false; 45 | var queryName = ''; 46 | var queryFull = ''; 47 | try { 48 | if (interaction.options._hoistedOptions[0]['value']) { 49 | queryName = interaction.options._hoistedOptions[0]['value']; 50 | for (var i in queryConfig.custom) { 51 | if (queryConfig.custom[i]['name'] === queryName && queryConfig.custom[i]['query']) { 52 | specificCheck = true; 53 | queryFull = queryConfig.custom[i]['query']; 54 | } 55 | } //End of i loop 56 | } 57 | } catch (err) {} 58 | if (specificCheck === true) { 59 | Queries.customQuery(channel, interaction.user, queryName, queryFull); 60 | } else { 61 | Queries.queries(channel); 62 | } 63 | } //End of if userPerms 64 | else { 65 | channel.send(`User *${interaction.user.username}* does not have required query perms`).catch(console.error); 66 | } 67 | return "delete"; 68 | }, 69 | }; -------------------------------------------------------------------------------- /functions/queries.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | EmbedBuilder, 11 | ButtonBuilder, 12 | InteractionType, 13 | ChannelType 14 | } = require('discord.js'); 15 | const fs = require('fs'); 16 | const mysql = require('mysql'); 17 | const config = require('../config/config.json'); 18 | const queryConfig = require('../config/queries.json'); 19 | 20 | module.exports = { 21 | queries: async function queries(channel) { 22 | let countList = queryConfig.count; 23 | var selectList = []; 24 | queryConfig.custom.forEach(query => { 25 | let listOption = { 26 | label: query.name, 27 | value: `${config.serverName}~customQuery~${query.name}` 28 | } 29 | selectList.push(listOption); 30 | }); 31 | let fullQueryList = new ActionRowBuilder() 32 | .addComponents( 33 | new SelectMenuBuilder() 34 | .setCustomId(`${config.serverName}~queryList`) 35 | .setPlaceholder('Custom query list') 36 | .setMinValues(1) 37 | .setMaxValues(1) 38 | .addOptions(selectList)) 39 | 40 | channel.send({ 41 | content: 'Select query below to run', 42 | components: [fullQueryList] 43 | }).catch(console.error); 44 | }, //End of queries() 45 | 46 | 47 | customQuery: async function customQuery(channel, user, queryName, queryFull) { 48 | var dbConfig = config.madDB; 49 | dbConfig.multipleStatements = true; 50 | let connection = mysql.createConnection(dbConfig); 51 | connection.connect(); 52 | var queryEmbed = new EmbedBuilder().setTitle(`${queryName} Results:`); 53 | connection.query(queryFull, function (err, results) { 54 | if (err) { 55 | console.log(`(${user.username}) custom query error:`, err); 56 | } else { 57 | console.log(`(${user.username}) ran custom query: ${queryName}`); 58 | var queryResults = results; 59 | if (!queryFull.replace(';','').includes(';')){ 60 | queryResults = [results]; 61 | } 62 | for (var r in queryResults) { 63 | for (const [key, value] of Object.entries(queryResults[r][0])) { 64 | queryEmbed.addFields({ 65 | name: key, 66 | value: value.toLocaleString() 67 | }); 68 | } 69 | } //End of r loop 70 | channel.send({ 71 | embeds: [queryEmbed], 72 | }).catch(console.error); 73 | } 74 | }) //End of query 75 | connection.end(); 76 | }, //End of customQuery() 77 | } -------------------------------------------------------------------------------- /functions/generateMadInfo.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const pm2 = require('pm2'); 3 | const mysql = require('mysql'); 4 | const config = require('../config/config.json'); 5 | 6 | module.exports = { 7 | generate: async function generate() { 8 | if (!config.madDB.host) { 9 | pm2Process({}); 10 | } else { 11 | let connection = mysql.createConnection(config.madDB); 12 | //Get instance info 13 | let instanceQuery = `SELECT * FROM madmin_instance`; 14 | connection.query(instanceQuery, function (err, results) { 15 | if (err) { 16 | console.log("Instance Query Error:", err); 17 | connection.end(); 18 | } else { 19 | var instances = {}; 20 | results.forEach(row => { 21 | instances[row.instance_id] = row.name; 22 | }); 23 | getDeviceInfo(connection, instances); 24 | } 25 | }) //End of query 26 | } 27 | 28 | async function getDeviceInfo(connection, instances) { 29 | let deviceQuery = `SELECT * FROM settings_device`; 30 | connection.query(deviceQuery, function (err, results) { 31 | if (err) { 32 | console.log("Walker Query Error:", err); 33 | connection.end(); 34 | } else { 35 | var devices = {}; 36 | results.forEach(device => { 37 | let deviceObj = { 38 | name: device.name, 39 | instance_id: device.instance_id, 40 | loginType: device.logintype, 41 | loginAccount: device.ggl_login_mail, 42 | mac: device.mac_address 43 | } 44 | devices[device.device_id] = deviceObj; 45 | }); 46 | getAreaInfo(connection, instances, devices); 47 | } 48 | }) //End of query 49 | } //End of getDeviceInfo() 50 | 51 | async function getAreaInfo(connection, instances, devices) { 52 | let areaQuery = `SELECT * FROM settings_area`; 53 | connection.query(areaQuery, function (err, results) { 54 | if (err) { 55 | console.log("Area Query Error:", err); 56 | connection.end(); 57 | } else { 58 | var areas = {}; 59 | results.forEach(area => { 60 | let areaObj = { 61 | name: area.name, 62 | instance_id: area.instance_id, 63 | mode: area.mode 64 | } 65 | areas[area.area_id] = areaObj; 66 | }); 67 | var dbInfo = { 68 | "instances": instances, 69 | "devices": devices, 70 | "areas": areas, 71 | } 72 | pm2Process(dbInfo); 73 | connection.end(); 74 | } 75 | }) //End of query 76 | } //End of getAreaInfo() 77 | 78 | async function pm2Process(dbInfo) { 79 | var processList = []; 80 | try { 81 | pm2.connect(async function (err) { 82 | if (err) { 83 | console.error(err) 84 | } else { 85 | pm2.list((err, response) => { 86 | if (err) { 87 | console.error(err); 88 | } else { 89 | response.forEach(process => { 90 | if (!config.pm2.ignore.includes(process['name'])) { 91 | processList.push(process['name']) 92 | } 93 | }) //End of forEach process 94 | } 95 | }) //End of pm2.list 96 | } 97 | }) //End of pm2.connect 98 | } catch (err) {} 99 | await new Promise(done => setTimeout(done, 5000)); 100 | processList.sort(function (a, b) { 101 | return a.toLowerCase().localeCompare(b.toLowerCase()); 102 | }); 103 | dbInfo.processList = processList; 104 | fs.writeFileSync('./MAD_Database_Info.json', JSON.stringify(dbInfo)); 105 | pm2.disconnect(); 106 | } //End of pm2Process 107 | } //End of generate() 108 | } -------------------------------------------------------------------------------- /config.example/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverName": "", 3 | "delaySeconds": 0, 4 | "discord": { 5 | "token": "", 6 | "prefix": "!", 7 | "adminIDs": ["123", "456"], 8 | "channelIDs": ["789", "420"], 9 | "useSlashCommands": false, 10 | "slashGuildIDs": [""], 11 | "helpCommand": "madgruber", 12 | "pm2Command": "pm2", 13 | "truncateCommand": "truncate", 14 | "scriptCommand": "scripts", 15 | "queryCommand": "query", 16 | "linksCommand": "links", 17 | "devicesCommand": "devices", 18 | "noProtoCommand": "noproto", 19 | "systemStatsCommand": "stats", 20 | "sendWorkerCommand": "sendworker", 21 | "eventsCommand": "events", 22 | "grepCommand": "grep", 23 | "geofenceCommand": "geofences" 24 | }, 25 | "pm2": { 26 | "mads": [""], 27 | "ignore": [""], 28 | "pm2ResponseDeleteSeconds": 0 29 | }, 30 | "roles": { 31 | "sendRoleMessage": true, 32 | "roleMessageDeleteSeconds": 30, 33 | "commandPermRoles": { 34 | "pm2": [], 35 | "truncate": [], 36 | "scripts": [], 37 | "queries": [], 38 | "links": [], 39 | "deviceInfo": [], 40 | "deviceInfoControl": [], 41 | "systemStats": [] 42 | } 43 | }, 44 | "truncate": { 45 | "truncateVerify": true, 46 | "truncateOptions": ["trs_quest", "pokemon", "trs_quest+pokemon"], 47 | "truncateQuestsByArea": false, 48 | "onlyRestartBeforeTime": 0, 49 | "eventAutomation": false, 50 | "eventGuildID": "", 51 | "eventDescriptionTrigger": "quest reroll", 52 | "eventAlertChannelID": "", 53 | "eventAlertDeleteSeconds": 0 54 | }, 55 | "scripts": { 56 | "scriptVerify": true, 57 | "scriptResponseDeleteSeconds": 0 58 | }, 59 | "madDB": { 60 | "host": "", 61 | "port": 3306, 62 | "user": "", 63 | "password": "", 64 | "database": "", 65 | "timezoneDifference": 0 66 | }, 67 | "devices": { 68 | "noProtoMinutes": 20, 69 | "noProtoCheckMinutes": 20, 70 | "noProtoChannelID": "", 71 | "noProtoIncludeIdle": false, 72 | "noProtoIgnoreDevices": [""], 73 | "useNoProtoJson": false, 74 | "checkDeleteMinutes": 20, 75 | "infoMessageDeleteSeconds": 0, 76 | "statusButtonsDeleteMinutes": 0, 77 | "buttonLabelRemove": ["ATV"], 78 | "displayOptions": { 79 | "deviceID": true, 80 | "instance": true, 81 | "restartInfo": true, 82 | "rebootInfo": true, 83 | "loginInfo": true 84 | } 85 | }, 86 | "deviceControl": { 87 | "path": "", 88 | "controlResponseDeleteSeconds": 0, 89 | "logcatDeleteSeconds": 0, 90 | "reverseLogcat": false, 91 | "screenshotDeleteSeconds": 0, 92 | "powerCycleType": "" 93 | }, 94 | "stats": { 95 | "database": { 96 | "host": "", 97 | "port": 3306, 98 | "user": "", 99 | "password": "", 100 | "database": "" 101 | }, 102 | "dataPointCount": { 103 | "hourly": 72, 104 | "daily": 30 105 | }, 106 | "colorPalette": { 107 | "color1": "orange", 108 | "color2": "green", 109 | "color3": "navy" 110 | }, 111 | "graphDeleteSeconds": 0, 112 | "deviceInfo": { 113 | "arch": false, 114 | "productmodel": false, 115 | "rom": true, 116 | "pogo": true, 117 | "rgc": true, 118 | "pogodroid": true, 119 | "vm_script": false, 120 | "55vmapper": false, 121 | "vmapper": false, 122 | "magisk": false, 123 | "magisk_modules": false, 124 | "pogo_update": true, 125 | "pd_update": true, 126 | "rgc_update": true, 127 | "vm_update": false, 128 | "pingreboot": false, 129 | "temperature": true, 130 | "ip": true, 131 | "ex_ip": false, 132 | "MACw": false, 133 | "MACe": true, 134 | "hostname": true, 135 | "gmail": false, 136 | "cycle": false 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /functions/roles.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | EmbedBuilder, 11 | ButtonBuilder, 12 | InteractionType, 13 | ChannelType 14 | } = require('discord.js'); 15 | const config = require('../config/config.json'); 16 | const roleConfig = require('../config/roles.json'); 17 | 18 | module.exports = { 19 | roles: async function roles(reaction, user, type) { 20 | roleConfig.forEach(role => { 21 | if (role.messageID === reaction.message.id) { 22 | role.roles.forEach(async emoji => { 23 | if (emoji.emojiName === reaction._emoji.name) { 24 | let guildUser = await reaction.message.guild.members.cache.find(m => m.id === user.id); 25 | let newRole = await reaction.message.guild.roles.cache.find(r => r.id === emoji.roleID); 26 | if (!newRole){ 27 | console.log(`Error fetching role for ${emoji.emojiName}`); 28 | return; 29 | } 30 | var errorCheck = false; 31 | if (type === 'add') { 32 | guildUser.roles.add(newRole).catch(err => { 33 | console.log(`Error giving ${newRole.name} role to user ${user.username}: ${err}`); 34 | errorCheck = true; 35 | }).catch(console.error) 36 | .then(() => { 37 | if (errorCheck !== true) { 38 | console.log(`${user.username} added ${newRole.name} role`); 39 | if (config.roles.sendRoleMessage === true) { 40 | reaction.message.channel.send(`${user} has added the ${newRole.name} role.`).catch(console.error) 41 | .then(msg => { 42 | if (config.roles.roleMessageDeleteSeconds > 0) { 43 | setTimeout(() => msg.delete().catch(err => console.log("Error deleting role message:", err)), (config.roles.roleMessageDeleteSeconds * 1000)) 44 | } 45 | }) 46 | } 47 | } 48 | }); 49 | } //End of role added 50 | else { 51 | guildUser.roles.remove(newRole).catch(err => { 52 | console.log(`Error removing ${newRole.name} role to user ${user.username}: ${err}`); 53 | errorCheck = true; 54 | }).catch(console.error) 55 | .then(() => { 56 | if (errorCheck !== true) { 57 | console.log(`${user.username} removed ${newRole.name} role`); 58 | if (config.roles.sendRoleMessage === true) { 59 | reaction.message.channel.send(`${user} has removed the ${newRole.name} role.`).catch(console.error) 60 | .then(msg => { 61 | if (config.roles.roleMessageDeleteSeconds > 0) { 62 | setTimeout(() => msg.delete().catch(err => console.log("Error deleting role message:", err)), (config.roles.roleMessageDeleteSeconds * 1000)) 63 | } 64 | }) 65 | } 66 | } 67 | }) 68 | } //End of role removed 69 | } 70 | }) //End of forEach(emoji) 71 | } 72 | }) //End of forEach(role) 73 | }, //End of roles() 74 | 75 | 76 | getUserCommandPerms: async function getUserCommandPerms(guild, user) { 77 | var userPerms = []; 78 | if (config.discord.adminIDs.includes(user.id)) { 79 | userPerms.push("admin"); 80 | } 81 | let member = await guild.members.fetch(user.id).catch(err => { 82 | console.log(err); 83 | }); 84 | if (member !== undefined) { 85 | let memberRoles = member._roles; 86 | let commandTypes = Object.keys(config.roles.commandPermRoles); 87 | let commandRoles = Object.values(config.roles.commandPermRoles); 88 | for (var t in commandTypes) { 89 | if (userPerms.includes('admin')) { 90 | userPerms.push(commandTypes[t]); 91 | } else { 92 | let roles = commandRoles[t]; 93 | for (var r in roles) { 94 | if (memberRoles && memberRoles.includes(roles[r])) { 95 | userPerms.push(commandTypes[t]); 96 | } 97 | } //End of r loop 98 | } 99 | } //End of t loop 100 | } 101 | return userPerms; 102 | } //End of getUserCommandPerms() 103 | } -------------------------------------------------------------------------------- /functions/geofenceConverter.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | AttachmentBuilder, 10 | MessageButton, 11 | EmbedBuilder, 12 | ButtonBuilder, 13 | InteractionType, 14 | ChannelType 15 | } = require('discord.js'); 16 | const fs = require('fs'); 17 | const mysql = require('mysql'); 18 | const config = require('../config/config.json'); 19 | 20 | module.exports = { 21 | converterMain: async function converterMain(channel, user) { 22 | let connection = mysql.createConnection(config.madDB); 23 | connection.connect(); 24 | let fenceListQuery = `SELECT name FROM settings_geofence`; 25 | connection.query(fenceListQuery, async function (err, results) { 26 | if (err) { 27 | console.log(`(${user.username}) Count Query Error:`, err); 28 | } else { 29 | var fenceList = []; 30 | for (var r = 0; r < results.length; r++) { 31 | fenceList.push({ 32 | label: results[r]['name'], 33 | value: `${results[r]['name']}~~${r}` 34 | }); 35 | } //End of i loop 36 | var componentList = []; 37 | for (var i = 0, j = fenceList.length; i < j; i += 25) { 38 | if (componentList.length < 5) { 39 | componentList.push(new ActionRowBuilder() 40 | .addComponents( 41 | new SelectMenuBuilder() 42 | .setCustomId(`${config.serverName}~geofenceList~${componentList.length}`) 43 | .setPlaceholder('Select geofence to convert') 44 | .addOptions(fenceList.slice(i, i + 25)) 45 | ) 46 | ); 47 | } 48 | } //End of i/j loop 49 | channel.send({ 50 | content: '**Convert MAD Geofences:**', 51 | components: componentList 52 | }).catch(console.error); 53 | } 54 | }); //End of query 55 | connection.end(); 56 | }, //End of converterMain() 57 | 58 | convert: async function convert(channel, user, fenceName) { 59 | let fenceQuery = `SELECT fence_data FROM settings_geofence WHERE name = "${fenceName}" LIMIT 1`; 60 | let connection = mysql.createConnection(config.madDB); 61 | connection.connect(); 62 | connection.query(fenceQuery, function (err, results) { 63 | if (err) { 64 | console.log(`(${user.username}) Geofence Converter Error:`, err); 65 | } else { 66 | console.log(`(${user.username}) selected the ${fenceName} geofence to convert`); 67 | let fenceData = JSON.parse(results[0]['fence_data']); 68 | //Multiple Geofences 69 | if (fenceData[0].startsWith('[')) { 70 | convertToMultipleGeofences(fenceName, fenceData); 71 | } 72 | //Single Geofence 73 | else { 74 | convertToSingleGeofence(fenceName, fenceData); 75 | } 76 | } 77 | }); //End of convert 78 | connection.end(); 79 | 80 | async function convertToSingleGeofence(fenceName, fenceData) { 81 | var geoCoordList = []; 82 | var simpleCoordList = []; 83 | for (var f = 0; f < fenceData.length; f++) { 84 | let point = fenceData[f].split(','); 85 | geoCoordList.push([Number(point[1]), Number(point[0])]); 86 | simpleCoordList.push([Number(point[0]), Number(point[1])]); 87 | } //End of f loop 88 | var geoJSON = { 89 | type: 'FeatureCollection', 90 | features: [{ 91 | type: "Feature", 92 | properties: { 93 | name: fenceName, 94 | id: 1, 95 | fill: `#${Math.floor(Math.random()*16777215).toString(16)}` 96 | }, 97 | geometry: { 98 | type: "Polygon", 99 | coordinates: [geoCoordList] 100 | } 101 | }] 102 | }; 103 | var simpleJSON = [{ 104 | name: fenceName, 105 | id: 1, 106 | color: `#${Math.floor(Math.random()*16777215).toString(16)}`, 107 | path: [simpleCoordList] 108 | }]; 109 | let geoAttachment = await new AttachmentBuilder(Buffer.from(JSON.stringify(geoJSON)), { 110 | name: `${fenceName}_GeoJSON.json` 111 | }); 112 | let simpleAttachment = await new AttachmentBuilder(Buffer.from(JSON.stringify(simpleJSON)), { 113 | name: `${fenceName}_SimpleJSON.json` 114 | }); 115 | channel.send({ 116 | content: `**Converted geofence for ${fenceName}:**`, 117 | files: [geoAttachment, simpleAttachment] 118 | }).catch(console.error); 119 | } //End of convertToSingleGeofence() 120 | 121 | async function convertToMultipleGeofences(fenceNameMain, fenceData) { 122 | var geoJSON = { 123 | type: 'FeatureCollection', 124 | features: [] 125 | } 126 | var simpleJSON = []; 127 | var fenceID = 1; 128 | var fenceName = fenceData[0].slice(1, -1); 129 | var geoCoordList = []; 130 | var simpleCoordList = []; 131 | for (var i = 1; i < fenceData.length; i++) { 132 | //Save geofence and start new 133 | if (fenceData[i].startsWith('[')) { 134 | geoJSON.features.push({ 135 | type: "Feature", 136 | properties: { 137 | name: fenceName, 138 | id: fenceID, 139 | fill: `#${Math.floor(Math.random()*16777215).toString(16)}` 140 | }, 141 | geometry: { 142 | type: "Polygon", 143 | coordinates: [geoCoordList] 144 | } 145 | }); 146 | simpleJSON.push({ 147 | name: fenceName, 148 | id: fenceID, 149 | color: `#${Math.floor(Math.random()*16777215).toString(16)}`, 150 | path: [simpleCoordList] 151 | }); 152 | fenceName = fenceData[i].slice(1, -1); 153 | geoCoordList = []; 154 | simpleCoordList = []; 155 | fenceID++; 156 | } 157 | //Add coords 158 | else { 159 | let point = fenceData[i].split(','); 160 | geoCoordList.push([Number(point[1]), Number(point[0])]); 161 | simpleCoordList.push([Number(point[0]), Number(point[1])]); 162 | } 163 | } //End of i loop 164 | //Save remaining geofence 165 | geoJSON.features.push({ 166 | type: "Feature", 167 | properties: { 168 | name: fenceName, 169 | id: fenceID, 170 | fill: `#${Math.floor(Math.random()*16777215).toString(16)}` 171 | }, 172 | geometry: { 173 | type: "Polygon", 174 | coordinates: [geoCoordList] 175 | } 176 | }); 177 | simpleJSON.push({ 178 | name: fenceName, 179 | id: fenceID, 180 | path: [simpleCoordList] 181 | }); 182 | let geoAttachment = await new AttachmentBuilder(Buffer.from(JSON.stringify(geoJSON)), { 183 | name: `${fenceNameMain}_GeoJSON.json` 184 | }); 185 | let simpleAttachment = await new AttachmentBuilder(Buffer.from(JSON.stringify(simpleJSON)), { 186 | name: `${fenceNameMain}_SimpleJSON.json` 187 | }); 188 | channel.send({ 189 | content: `**Converted geofence for ${fenceNameMain}:**`, 190 | files: [geoAttachment, simpleAttachment] 191 | }).catch(console.error); 192 | } //End of convertToMultipleGeofences 193 | } //End of convert() 194 | } -------------------------------------------------------------------------------- /functions/events.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | EmbedBuilder, 11 | ButtonBuilder, 12 | InteractionType, 13 | ChannelType, 14 | time 15 | } = require('discord.js'); 16 | 17 | const fs = require('fs'); 18 | const mysql = require('mysql'); 19 | const pm2 = require('pm2'); 20 | 21 | const config = require('../config/config.json'); 22 | 23 | module.exports = { 24 | listEvents: async function listEvents(client, channel) { 25 | var eventGuild = ''; 26 | var allEvents = ''; 27 | var activeEvents = []; 28 | var scheduledEvents = []; 29 | try { 30 | if (config.truncate.eventGuildID === "") { 31 | sendEventListMessage(new EmbedBuilder().setDescription("No event guild set in config.")); 32 | return; 33 | } else { 34 | eventGuild = await client.guilds.fetch(config.truncate.eventGuildID).catch(console.error); 35 | allEvents = JSON.parse(JSON.stringify(await eventGuild.scheduledEvents.fetch())); 36 | } 37 | } catch (err) { 38 | console.log(err) 39 | } 40 | for (var e = 0; e < allEvents.length; e++) { 41 | if (allEvents[e]['description'].toLowerCase().includes(config.truncate.eventDescriptionTrigger.toLowerCase())) { 42 | if (allEvents[e]['status'] === 'ACTIVE') { 43 | activeEvents.push(allEvents[e]); 44 | } else if (allEvents[e]['status'] === 'SCHEDULED') { 45 | scheduledEvents.push(allEvents[e]); 46 | } 47 | } //End of if trigger 48 | } //End of e loop 49 | 50 | //No events found 51 | if (activeEvents.length == 0 && scheduledEvents.length == 0) { 52 | sendEventListMessage(new EmbedBuilder().setDescription("No active or scheduled quest reroll events found.")); 53 | } 54 | 55 | //Events found 56 | else { 57 | //Active Events 58 | if (activeEvents.length == 0) { 59 | sendEventListMessage(new EmbedBuilder().setDescription("No active quest reroll events found.")); 60 | } else { 61 | activeEvents.sort(function (a, b) { 62 | return a.scheduledEndTimestamp - b.scheduledEndTimestamp; 63 | }); 64 | var activeEmbed = new EmbedBuilder().setAuthor({ 65 | name: `Active Quest Reroll Events:` 66 | }).setColor('00841E'); 67 | for (var a = 0; a < activeEvents.length; a++) { 68 | let endTime = new Date(activeEvents[a]['scheduledEndTimestamp']); 69 | activeEmbed.addFields({ 70 | name: `${activeEvents[a]['name']}`, 71 | value: (`- Ends ${time(endTime, 'R')}\n- ${activeEvents[a]['description']}`).replaceAll('- -', '-') 72 | }); 73 | } //End of a loop 74 | sendEventListMessage(activeEmbed); 75 | } //End of active events 76 | 77 | //Scheduled Events 78 | await new Promise(done => setTimeout(done, 1000)); 79 | if (scheduledEvents.length == 0) { 80 | sendEventListMessage(new EmbedBuilder().setDescription("No scheduled quest reroll events found.")); 81 | } else { 82 | scheduledEvents.sort(function (a, b) { 83 | return a.scheduledStartTimestamp - b.scheduledStartTimestamp; 84 | }); 85 | var scheduledEmbed = new EmbedBuilder().setAuthor({ 86 | name: `Scheduled Quest Reroll Events:` 87 | }).setColor('B4740E'); 88 | for (var s = 0; s < scheduledEvents.length; s++) { 89 | let startTime = new Date(scheduledEvents[s]['scheduledStartTimestamp']); 90 | scheduledEmbed.addFields({ 91 | name: `${scheduledEvents[s]['name']}`, 92 | value: (`- Starts ${time(startTime, 'R')}\n- ${scheduledEvents[s]['description']}`).replaceAll('- -', '-') 93 | }); 94 | } //End of s loop 95 | sendEventListMessage(scheduledEmbed); 96 | } 97 | } //End of events found 98 | async function sendEventListMessage(embed) { 99 | channel.send({ 100 | embeds: [embed] 101 | }).catch(console.error); 102 | } //End of sendEventListMessage 103 | }, //End of listEvents() 104 | 105 | 106 | checkEvents: async function checkEvents(client) { 107 | let eventGuild = await client.guilds.fetch(config.truncate.eventGuildID).catch(console.error); 108 | let allEvents = JSON.parse(JSON.stringify(await eventGuild.scheduledEvents.fetch())); 109 | var timeToWait = 0; 110 | var activeEvents = []; 111 | var scheduledEvents = []; 112 | for (var e = 0; e < allEvents.length; e++) { 113 | if (allEvents[e]['description'].toLowerCase().includes(config.truncate.eventDescriptionTrigger.toLowerCase())) { 114 | let timeUntilEnd = (allEvents[e]['scheduledEndTimestamp'] - Date.now()) * 1; 115 | let timeUntilStart = (allEvents[e]['scheduledStartTimestamp'] - Date.now()) * 1; 116 | if (allEvents[e]['status'] === 'ACTIVE' && timeUntilEnd < 120000) { 117 | activeEvents.push(allEvents[e]); 118 | timeToWait = timeUntilEnd; 119 | } else if (allEvents[e]['status'] === 'SCHEDULED' && timeUntilStart < 120000) { 120 | scheduledEvents.push(allEvents[e]); 121 | timeToWait = allEvents[e]['scheduledStartTimestamp'] - Date.now(); 122 | } 123 | } //End of if trigger 124 | } //End of e loop 125 | 126 | //Events found 127 | if (activeEvents.length > 0 || scheduledEvents.length > 0) { 128 | //console.log("Quest reroll event/s found."); 129 | //console.log("active:", activeEvents); 130 | //console.log("scheduled:", scheduledEvents); 131 | let date = new Date(); 132 | let hour = date.getHours(); 133 | //If no MAD restart 134 | var eventEmbed = new EmbedBuilder().setAuthor({ 135 | name: `Quests Have Been Rerolled:` 136 | }).setDescription(`MAD will not restart and quests will not be rescanned.`).setColor('00841E'); 137 | //MAD restart 138 | if (hour < (config.truncate.onlyRestartBeforeTime * 1) || config.truncate.onlyRestartBeforeTime == 0) { 139 | eventEmbed.setDescription(`MAD will restart and quests will be rescanned.`).setColor('00841E'); 140 | //Ending Events 141 | if (activeEvents.length > 0) { 142 | var endingEvents = []; 143 | for (var a = 0; a < activeEvents.length; a++) { 144 | endingEvents.push(activeEvents[a]['name']); 145 | } //End of a loop 146 | eventEmbed.addFields({ 147 | name: `Events Ending:`, 148 | value: `- ${endingEvents.join('\n- ')}` 149 | }); 150 | } //End of ending events 151 | 152 | //Starting Events 153 | if (scheduledEvents.length > 0) { 154 | var startingEvents = []; 155 | for (var s = 0; s < scheduledEvents.length; s++) { 156 | startingEvents.push(scheduledEvents[s]['name']); 157 | } //End of s loop 158 | eventEmbed.addFields({ 159 | name: `Events Starting:`, 160 | value: `- ${startingEvents.join('\n- ')}` 161 | }); 162 | } //End of starting events 163 | await new Promise(done => setTimeout(done, timeToWait)); 164 | truncateQuests(eventEmbed); 165 | } //End of MAD restart 166 | } //End of events found 167 | 168 | async function truncateQuests(eventEmbed) { 169 | var embed = eventEmbed; 170 | let connection = mysql.createConnection(config.madDB); 171 | connection.connect(); 172 | 173 | let truncateQuestQuery = `TRUNCATE trs_quest`; 174 | connection.query(truncateQuestQuery, function (err, results) { 175 | if (err) { 176 | console.log(err); 177 | embed.setColor('9E0000').setFooter({ 178 | text: `QUESTS FAILED TO BE TRUNCATED!` 179 | }); 180 | sendEventAlertMessage(embed); 181 | } else { 182 | connectPM2(); 183 | async function connectPM2() { 184 | await pm2.connect(async function (err) { 185 | if (err) { 186 | console.error(err); 187 | embed.setColor('9E0000').setFooter({ 188 | text: `QUESTS TRUNCATED | MAD RESTART FAILED` 189 | }); 190 | sendEventAlertMessage(embed); 191 | } else { 192 | console.log("Quests automatically truncated"); 193 | for (m in config.pm2.mads) { 194 | await pm2.restart(config.pm2.mads[m], (err, response) => { 195 | if (err) { 196 | console.log(err); 197 | embed.setColor('9E0000').setFooter({ 198 | text: `QUESTS TRUNCATED | MAD RESTART FAILED` 199 | }); 200 | sendEventAlertMessage(embed); 201 | return; 202 | } else { 203 | console.log("MAD restarted"); 204 | embed.setFooter({ 205 | text: `QUESTS TRUNCATED | MAD RESTARTED` 206 | }); 207 | } 208 | }) //End of pm2.restart 209 | } //End of m loop 210 | await new Promise(done => setTimeout(done, 5000)); 211 | sendEventAlertMessage(embed); 212 | } 213 | }) //End of pm2.connect 214 | } 215 | } 216 | }); //End of connection.query 217 | connection.end(); 218 | } //End of truncateQuests() 219 | 220 | async function sendEventAlertMessage(embed) { 221 | if (config.truncate.eventAlertChannelID) { 222 | try { 223 | let eventChannel = await client.channels.fetch(config.truncate.eventAlertChannelID).catch(console.error); 224 | eventChannel.send({ 225 | embeds: [embed] 226 | }).catch(console.error) 227 | .then(msg => { 228 | if (config.truncate.eventAlertDeleteSeconds > 0) { 229 | setTimeout(() => msg.delete().catch(err => console.log(`(${interaction.user.username}) Error deleting PM2 start response:`, err)), (config.pm2.pm2ResponseDeleteSeconds * 1000)); 230 | } 231 | }); 232 | } catch (err) { 233 | console.log(err); 234 | } 235 | } 236 | } //End of sendEventAlertMessage() 237 | } //End of checkEvents() 238 | } -------------------------------------------------------------------------------- /madgruber.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | EmbedBuilder, 11 | ButtonBuilder, 12 | ButtonStyle, 13 | InteractionType, 14 | ChannelType, 15 | } = require('discord.js'); 16 | const client = new Client({ 17 | intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildScheduledEvents, GatewayIntentBits.DirectMessages], 18 | partials: [Partials.Message, Partials.Channel, Partials.Reaction], 19 | }); 20 | const fs = require('fs'); 21 | const config = require('./config/config.json'); 22 | const GenerateMadInfo = require('./functions/generateMadInfo.js'); 23 | //Generate database info 24 | if (config.madDB.host) { 25 | GenerateMadInfo.generate(); 26 | } 27 | const pm2 = require('pm2'); 28 | const CronJob = require('cron').CronJob; 29 | const SlashRegistry = require('./functions/slashRegistry.js'); 30 | const Scripts = require('./functions/scripts.js'); 31 | const Queries = require('./functions/queries.js'); 32 | const Interactions = require('./functions/interactions.js'); 33 | const Pm2Buttons = require('./functions/pm2.js'); 34 | const Truncate = require('./functions/truncate.js'); 35 | const Links = require('./functions/links.js'); 36 | const Devices = require('./functions/devices.js'); 37 | const DeviceControl = require('./functions/deviceControl.js'); 38 | const Roles = require('./functions/roles.js'); 39 | const Stats = require('./functions/stats.js'); 40 | const Events = require('./functions/events.js'); 41 | const Geofences = require('./functions/geofenceConverter.js'); 42 | const Help = require('./functions/help.js'); 43 | const roleConfig = require('./config/roles.json'); 44 | var roleMessages = []; 45 | roleConfig.forEach(role => { 46 | if (role.messageID) { 47 | roleMessages.push(role.messageID); 48 | } 49 | }); 50 | 51 | 52 | client.on('ready', async () => { 53 | console.log("MadGruber Bot Logged In"); 54 | //No Proto Checker 55 | if (config.madDB.host && config.devices.noProtoCheckMinutes > 0) { 56 | let noProtoJob = new CronJob(`*/${config.devices.noProtoCheckMinutes} * * * *`, function () { 57 | Devices.noProtoDevices(client, '', '', 'cron'); 58 | }, null, true, null); 59 | noProtoJob.start(); 60 | } 61 | //Automated Quest Reroll 62 | if (config.madDB.host && config.truncate.eventAutomation === true && config.truncate.eventGuildID && config.truncate.eventAutomation === true) { 63 | if (config.truncate.truncateQuestsByArea === true) { 64 | console.log("Warning: eventAutomation disabled because truncateQuestsByArea = true."); 65 | } else { 66 | let questJob = new CronJob(`14-59/15 * * * *`, function () { 67 | Events.checkEvents(client); 68 | }, null, true, null); 69 | questJob.start(); 70 | } 71 | } 72 | //Register Slash Commands 73 | if (config.discord.useSlashCommands === true && config.discord.slashGuildIDs.length > 0) { 74 | SlashRegistry.registerCommands(client); 75 | } 76 | }); 77 | 78 | 79 | client.on('messageCreate', async (receivedMessage) => { 80 | let message = receivedMessage.content.toLowerCase(); 81 | var user = receivedMessage.author; 82 | //Ignore messages that don't start with prefix 83 | if (!message.startsWith(config.discord.prefix)) { 84 | return; 85 | } 86 | //Ignore DMs 87 | if (receivedMessage.channel.type === ChannelType.DM) { 88 | return; 89 | } 90 | //Ignore bot messages 91 | if (user.bot === true) { 92 | return; 93 | } 94 | //Not in channel list 95 | if (receivedMessage.channel.type === ChannelType.GuildText && !config.discord.channelIDs.includes(receivedMessage.channel.id)) { 96 | return; 97 | } 98 | let userPerms = await Roles.getUserCommandPerms(receivedMessage.guild, user); 99 | if (userPerms === []) { 100 | return; 101 | } 102 | //Send PM2 list/buttons 103 | if (config.discord.pm2Command && message === `${config.discord.prefix}${config.discord.pm2Command}`) { 104 | await new Promise(done => setTimeout(done, 1000 * config.delaySeconds)); 105 | if (userPerms.includes('admin') || userPerms.includes('pm2')) { 106 | Pm2Buttons.updateStatus(receivedMessage.channel, 'new'); 107 | } 108 | } 109 | //Truncate Quests 110 | else if (config.madDB.host && config.discord.truncateCommand && message === `${config.discord.prefix}${config.discord.truncateCommand}`) { 111 | await new Promise(done => setTimeout(done, 1000 * config.delaySeconds)); 112 | if (userPerms.includes('admin') || userPerms.includes('truncate')) { 113 | Truncate.sendTruncateMessage(receivedMessage.channel); 114 | } 115 | } 116 | //Run Scripts 117 | else if (config.discord.scriptCommand && message === `${config.discord.prefix}${config.discord.scriptCommand}`) { 118 | if (userPerms.includes('admin') || userPerms.includes('scripts')) { 119 | Scripts.sendScriptList(receivedMessage, 'new'); 120 | } 121 | } 122 | //Run Queries 123 | else if (config.madDB.host && config.discord.queryCommand && message === `${config.discord.prefix}${config.discord.queryCommand}`) { 124 | if (userPerms.includes('admin') || userPerms.includes('queries')) { 125 | Queries.queries(receivedMessage.channel); 126 | } 127 | } 128 | //Send Links 129 | else if (config.discord.linksCommand && message === `${config.discord.prefix}${config.discord.linksCommand}`) { 130 | if (userPerms.includes('admin') || userPerms.includes('links')) { 131 | Links.links(client, receivedMessage.channel); 132 | } 133 | } 134 | //Device Info All 135 | else if (config.madDB.host && config.discord.devicesCommand && message === `${config.discord.prefix}${config.discord.devicesCommand}`) { 136 | if (userPerms.includes('admin') || userPerms.includes('deviceInfoControl') || userPerms.includes('deviceInfo')) { 137 | Devices.deviceStatus(receivedMessage.channel, receivedMessage.author); 138 | } 139 | } 140 | //No Proto Devices 141 | else if (config.madDB.host && config.discord.noProtoCommand && message === `${config.discord.prefix}${config.discord.noProtoCommand}`) { 142 | if (userPerms.includes('admin') || userPerms.includes('deviceInfoControl') || userPerms.includes('deviceInfo')) { 143 | Devices.noProtoDevices(client, receivedMessage.channel, receivedMessage.author, 'search'); 144 | } 145 | } 146 | //Send Worker 147 | else if (config.stats.database.host && config.deviceControl.path && config.discord.sendWorkerCommand && message.startsWith(`${config.discord.prefix}${config.discord.sendWorkerCommand} `)) { 148 | if (userPerms.includes('admin') || userPerms.includes('deviceInfoControl')) { 149 | DeviceControl.sendWorker(client, receivedMessage.channel, receivedMessage.author, receivedMessage.content); 150 | } 151 | } 152 | //Stats 153 | else if (config.stats.database.host && config.discord.systemStatsCommand && message === `${config.discord.prefix}${config.discord.systemStatsCommand}`) { 154 | if (userPerms.includes('admin') || userPerms.includes('systemStats')) { 155 | Stats.stats(client, receivedMessage.channel, receivedMessage.author); 156 | } 157 | } 158 | //Events 159 | else if (config.madDB.host && config.discord.eventsCommand && message === `${config.discord.prefix}${config.discord.eventsCommand}`) { 160 | if (config.truncate.eventAutomation === true && config.truncate.eventGuildID) { 161 | Events.listEvents(client, receivedMessage.channel); 162 | } 163 | } 164 | //Geofence Converter 165 | else if (config.madDB.host && config.discord.geofenceCommand && message === `${config.discord.prefix}${config.discord.geofenceCommand}`) { 166 | Geofences.converterMain(receivedMessage.channel, receivedMessage.author); 167 | } 168 | //Help Menu 169 | else if (config.discord.helpCommand && receivedMessage.channel.type !== ChannelType.DM && message === `${config.discord.prefix}${config.discord.helpCommand}`) { 170 | Help.helpMenu(client, receivedMessage.channel, receivedMessage.guild, receivedMessage.author); 171 | } 172 | //Specific Device Info 173 | else if (userPerms.includes('admin') || userPerms.includes('deviceInfoControl') || userPerms.includes('deviceInfo')) { 174 | let dbInfo = require('./MAD_Database_Info.json'); 175 | for (const [key, value] of Object.entries(dbInfo.devices)) { 176 | if (receivedMessage.content.toLowerCase() === `${config.discord.prefix}${value.name.toLowerCase()}`) { 177 | Devices.getDeviceInfo(receivedMessage.channel, receivedMessage.author, key); 178 | } 179 | } 180 | } 181 | }); //End of client.on(message) 182 | 183 | 184 | client.on('interactionCreate', async interaction => { 185 | if (interaction.type !== InteractionType.MessageComponent) { 186 | return; 187 | } 188 | if (interaction.message.guildId === null) { 189 | return; 190 | } 191 | let user = interaction.member; 192 | //Verify interaction 193 | if (!interaction.customId.startsWith(config.serverName)) { 194 | return; 195 | } 196 | var interactionID = interaction.customId.replace(`${config.serverName}~`, ''); 197 | let userPerms = await Roles.getUserCommandPerms(interaction.message.guild, user); 198 | //Button interaction 199 | if (interaction.isButton()) { 200 | Interactions.buttonInteraction(interaction, interactionID, userPerms); 201 | } 202 | //List interaction 203 | else if (interaction.isSelectMenu()) { 204 | Interactions.listInteraction(interaction, interactionID, userPerms); 205 | } 206 | }); //End of client.on(interactionCreate) 207 | 208 | 209 | client.on('messageReactionAdd', async (reaction, user) => { 210 | if (user.bot == true) { 211 | return; 212 | } 213 | if (reaction.message.partial) await reaction.message.fetch(); 214 | if (reaction.partial) { 215 | try { 216 | await reaction.fetch(); 217 | } catch (error) { 218 | console.error('Error fetching the message:', error); 219 | return; 220 | } 221 | } 222 | if (roleMessages.includes(reaction.message.id)) { 223 | Roles.roles(reaction, user, "add"); 224 | } 225 | }); //End of messageReactionAdd 226 | 227 | 228 | client.on('messageReactionRemove', async (reaction, user) => { 229 | if (user.bot == true) { 230 | return; 231 | } 232 | if (reaction.message.partial) await reaction.message.fetch(); 233 | if (reaction.partial) { 234 | try { 235 | await reaction.fetch(); 236 | } catch (error) { 237 | console.error('Error fetching the message:', error); 238 | return; 239 | } 240 | } 241 | if (roleMessages.includes(reaction.message.id)) { 242 | Roles.roles(reaction, user, "remove"); 243 | } 244 | }); //End of messageReactionRemove 245 | 246 | 247 | //Slash commands 248 | client.on('interactionCreate', async interaction => { 249 | if (interaction.type !== InteractionType.ApplicationCommand) { 250 | return; 251 | } 252 | let user = interaction.user; 253 | if (user.bot == true) { 254 | return; 255 | } 256 | const command = client.commands.get(interaction.commandName); 257 | if (!command) { 258 | return; 259 | } 260 | 261 | //Not in channel list 262 | if (!config.discord.channelIDs.includes(interaction.channelId)) { 263 | interaction.reply(`Slash commands not allowed in channel *${interaction.channelId}*`); 264 | return; 265 | } 266 | 267 | try { 268 | let slashReturn = await command.execute(client, interaction); 269 | try { 270 | if (slashReturn === 'delete') { 271 | interaction.deleteReply().catch(err); 272 | } 273 | } catch (err) {} 274 | } catch (error) { 275 | console.error(error); 276 | await interaction.reply({ 277 | content: 'There was an error while executing this command!', 278 | ephemeral: true 279 | }).catch(console.error); 280 | } 281 | }); 282 | 283 | client.on("error", (e) => console.error(e)); 284 | client.on("warn", (e) => console.warn(e)); 285 | client.login(config.discord.token); -------------------------------------------------------------------------------- /functions/deviceControl.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | AttachmentBuilder, 11 | EmbedBuilder, 12 | ButtonBuilder, 13 | InteractionType, 14 | ChannelType 15 | } = require('discord.js'); 16 | const fs = require('fs'); 17 | const mysql = require('mysql'); 18 | const shell = require('shelljs'); 19 | const lineReader = require('reverse-line-reader'); 20 | const config = require('../config/config.json'); 21 | 22 | module.exports = { 23 | deviceControl: async function deviceControl(interaction) { 24 | let controlVariables = interaction.values[0].replace(`${config.serverName}~deviceControl~`, '').split('~'); 25 | let origin = controlVariables[0]; 26 | let controlType = controlVariables[1]; 27 | if (interaction.values[0].startsWith('raspberryRelay~') && config.deviceControl.powerCycleType.toLowerCase().replace(' ', '').replace('raspberryrelay', 'raspberry')) { 28 | return; 29 | } 30 | let dcPath = (`${config.deviceControl.path}/devicecontrol.sh`).replace('//', '/'); 31 | let bashControlCommand = (`bash ${dcPath} ${origin} ${controlType}`).replace('//', '/'); 32 | interaction.message.edit({ 33 | embeds: interaction.embeds, 34 | components: interaction.components 35 | }).catch(console.error); 36 | interaction.message.channel.send({ 37 | content: '**Running deviceControl script:**', 38 | embeds: [new EmbedBuilder().setDescription(`\`${bashControlCommand}\``).setColor('0D00CA').setFooter({ 39 | text: `${interaction.user.username}` 40 | })] 41 | }).catch(console.error) 42 | .then(async msg => { 43 | let logFile = []; 44 | shell.exec(bashControlCommand, async function (exitCode, output) { 45 | var color = '00841E'; 46 | var description = `${origin} ${controlType}`; 47 | if (exitCode !== 0) { 48 | color = '9E0000'; 49 | description = `**${origin} ${controlType} failed**\n\n**Error Response:**\n${output}`; 50 | console.log(`${interaction.user.username} failed to run devicecontrol.sh ${origin} ${controlType}`); 51 | } else { 52 | console.log(`${interaction.user.username} ran devicecontrol.sh ${origin} ${controlType}`); 53 | if (controlType === 'pauseDevice' || controlType === 'unpauseDevice') { 54 | changeIdleStatus(origin, controlType); 55 | } 56 | } 57 | if (controlType === 'logcatDevice') { 58 | try { 59 | fs.renameSync('./logcat.txt', `logcat_${origin}.txt`); 60 | if (config.deviceControl.reverseLogcat === true) { 61 | var reverseLog = []; 62 | await lineReader.eachLine(`logcat_${origin}.txt`, async function (line, last) { 63 | reverseLog.push(line); 64 | if (last === true) { 65 | fs.writeFileSync(`logcat_${origin}.txt`, reverseLog.join('\n')); 66 | } 67 | }); 68 | } 69 | logFile.push(await new AttachmentBuilder(fs.readFileSync(`logcat_${origin}.txt`), {name: `logcat_${origin}.txt`})); 70 | } catch (err) { 71 | console.log(`Error renaming logcat.txt to "logcat_${origin}.txt":`, err) 72 | } 73 | try { 74 | fs.renameSync('./vm.log', `vm_${origin}.log`); 75 | if (config.deviceControl.reverseLogcat === true) { 76 | var reverseLog = []; 77 | await lineReader.eachLine(`vm_${origin}.log`, async function (line, last) { 78 | reverseLog.push(line); 79 | if (last === true) { 80 | fs.writeFileSync(`vm_${origin}.log`, reverseLog.join('\n')); 81 | } 82 | }); 83 | } 84 | logFile.push(await new AttachmentBuilder(fs.readFileSync(`vm_${origin}.log`), {name: `vm_${origin}.log`})); 85 | } catch (err) {} 86 | try { 87 | fs.renameSync('./vmapper.log', `vmapper_${origin}.log`); 88 | if (config.deviceControl.reverseLogcat === true) { 89 | var reverseLog = []; 90 | await lineReader.eachLine(`vmapper_${origin}.log`, async function (line, last) { 91 | reverseLog.push(line); 92 | if (last === true) { 93 | fs.writeFileSync(`vmapper_${origin}.log`, reverseLog.join('\n')); 94 | } 95 | }); 96 | } 97 | logFile.push(await new AttachmentBuilder(fs.readFileSync(`vmapper_${origin}.log`), {name: `vmapper_${origin}.log`})); 98 | } catch (err) {} 99 | } 100 | if (controlType === 'screenshot') { 101 | try { 102 | fs.renameSync('./screenshot.jpg', `screenshot_${origin}.jpg`); 103 | logFile.push(await new AttachmentBuilder(fs.readFileSync(`screenshot_${origin}.jpg`), {name: `screenshot_${origin}.jpg`})); 104 | } catch (err) { 105 | console.log(`Error renaming screenshot.jpg to "screenshot_${origin}.jpg":`, err); 106 | } 107 | } 108 | msg.edit({ 109 | content: '**Ran deviceControl script:**', 110 | embeds: [new EmbedBuilder().setDescription(description).setColor(color).setFooter({ 111 | text: `${interaction.user.username}` 112 | })], 113 | }).catch(console.error); 114 | if (controlType === 'logcatDevice' && exitCode !== 1) { 115 | logFile.forEach(async file => { 116 | interaction.message.channel.send({ 117 | files: [file] 118 | }).catch(console.error) 119 | .then(logcatMsg => { 120 | if (config.deviceControl.logcatDeleteSeconds > 0) { 121 | setTimeout(() => logcatMsg.delete().catch(err => console.log(`(${interaction.user.username}) Error deleting logcat message:`, err)), (config.deviceControl.logcatDeleteSeconds * 1000)); 122 | } 123 | }) 124 | .then(() => { 125 | try { 126 | fs.rmSync(file.attachment); 127 | } catch (err) {} 128 | }) 129 | }) 130 | } 131 | if (controlType === 'screenshot' && exitCode !== 1) { 132 | interaction.message.channel.send({ 133 | files: logFile 134 | }).catch(console.error) 135 | .then(logcatMsg => { 136 | if (config.deviceControl.screenshotDeleteSeconds > 0) { 137 | setTimeout(() => logcatMsg.delete().catch(err => console.log(`(${interaction.user.username}) Error deleting screenshot message:`, err)), (config.deviceControl.screenshotDeleteSeconds * 1000)); 138 | } 139 | }) 140 | .then(() => { 141 | fs.rmSync(`screenshot_${origin}.jpg`); 142 | }); 143 | } 144 | if (config.deviceControl.controlResponseDeleteSeconds > 0) { 145 | setTimeout(() => msg.delete().catch(err => console.log(`(${interaction.user.username}) Error deleting deviceControl message:`, err)), (config.deviceControl.controlResponseDeleteSeconds * 1000)); 146 | } 147 | }) //End of shell.exec() 148 | }); //End of msg() 149 | 150 | async function changeIdleStatus(origin, controlType) { 151 | let dbInfo = require('../MAD_Database_Info.json'); 152 | for (const [key, value] of Object.entries(dbInfo.devices)) { 153 | if (value.name === origin) { 154 | var status = 0; 155 | if (controlType === 'pauseDevice') { 156 | status = 1; 157 | } 158 | let idleQuery = `UPDATE trs_status SET idle = ${status} WHERE device_id = ${key}`; 159 | let connectionIdle = mysql.createConnection(config.madDB); 160 | connectionIdle.query(idleQuery, function (err, statusResults) { 161 | if (err) { 162 | console.log(`Error manually updating idle status for ${origin} ${controlType}:`, err); 163 | } else { 164 | if (statusResults['changedRows'] == 1) { 165 | //Seems like only errors should be logged for this? 166 | //console.log(`Manually updated idle status for ${origin} ${statusResults}`); 167 | } 168 | } 169 | }); //End of query 170 | connectionIdle.end(); 171 | } 172 | } 173 | } //End of changeIdleStatus() 174 | }, //End of deviceControl() 175 | 176 | 177 | sendWorker: async function sendWorker(client, channel, user, content) { 178 | console.log("start sendWorker"); 179 | let coords = content.toLowerCase().replace(`${config.discord.prefix}${config.discord.sendWorkerCommand.toLowerCase()} `, '').replace(', ', '').replace('- ', '-'); 180 | let dcPath = (`${config.deviceControl.path}/devicecontrol.sh`).replace('//', '/'); 181 | let sendWorkerBash = `bash ${dcPath} origin sendWorker ${coords}`; 182 | channel.send({ 183 | embeds: [new EmbedBuilder().setDescription(`Sending closest worker...`).setColor('0D00CA').setFooter({ 184 | text: `${user.username}` 185 | })] 186 | }).catch(console.error) 187 | .then(async msg => { 188 | shell.exec(sendWorkerBash, async function (exitCode, output) { 189 | let splitOutput = output.split(' '); 190 | let response = splitOutput[1]; 191 | if (exitCode !== 0) { 192 | console.log(`${user.username} failed to send worker to: ${coords}`); 193 | msg.edit({ 194 | embeds: [new EmbedBuilder().setDescription(`Error sending worker:\n\n${output}`).setColor('9E0000').setFooter({ 195 | text: `${user.username}` 196 | })], 197 | }).catch(console.error); 198 | } else { 199 | console.log(`(${user.username}) ${response}`); 200 | msg.edit({ 201 | embeds: [new EmbedBuilder().setDescription(response.replace(coords, `[${coords}](https://www.google.com/maps/search/?api=1&query=${coords})`)).setColor('00841E').setFooter({ 202 | text: `${user.username}` 203 | })], 204 | }).catch(console.error); 205 | } 206 | if (config.deviceControl.controlResponseDeleteSeconds > 0) { 207 | setTimeout(() => msg.delete().catch(err => console.log(`(${user.username}) Error deleting sendWorker message:`, err)), (config.deviceControl.controlResponseDeleteSeconds * 1000)); 208 | } 209 | }) //End of shell 210 | }); 211 | } //End of sendWorker() 212 | } -------------------------------------------------------------------------------- /functions/pm2.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | EmbedBuilder, 11 | ButtonBuilder, 12 | ButtonStyle, 13 | InteractionType, 14 | ChannelType 15 | } = require('discord.js'); 16 | const pm2 = require('pm2'); 17 | const config = require('../config/config.json'); 18 | 19 | module.exports = { 20 | updateStatus: async function updateStatus(channelOrInteraction, type) { 21 | pm2.connect(async function (err) { 22 | if (err) { 23 | console.error(err); 24 | return; 25 | } 26 | pm2.list((err, response) => { 27 | if (err) { 28 | console.error("pm2.list error:", err); 29 | pm2.disconnect(); 30 | return; 31 | } 32 | var sortButtons = require('sort-by'), 33 | buttonList = []; 34 | response.forEach(process => { 35 | var buttonStyle = process['status']; 36 | if (buttonStyle === undefined) { 37 | buttonStyle = process['pm2_env']['status'] 38 | } 39 | buttonStyle = buttonStyle.replace('online', ButtonStyle.Success).replace('stopping', ButtonStyle.Danger).replace('stopped', ButtonStyle.Danger).replace('launching', ButtonStyle.Success).replace('errored', ButtonStyle.Danger).replace('one-launch-status', ButtonStyle.Danger).replace('waiting restart', ButtonStyle.Secondary); 40 | let buttonLabel = process['name']; 41 | let buttonID = `${config.serverName}~process~restart~${buttonLabel}`; 42 | let button = new ButtonBuilder().setCustomId(buttonID).setLabel(buttonLabel).setStyle(buttonStyle); 43 | if (!config.pm2.ignore.includes(buttonLabel)) { 44 | buttonList.push(button); 45 | } 46 | }) //End of response.forEach 47 | buttonList.sort(sortButtons('data.label')); 48 | let rowsNeeded = Math.ceil(buttonList.length / 5); 49 | let buttonsNeeded = buttonList.length; 50 | var buttonCount = 0; 51 | var messageComponents = []; 52 | for (var n = 0; n < rowsNeeded && n < 4; n++) { 53 | var buttonRow = new ActionRowBuilder() 54 | for (var r = 0; r < 5; r++) { 55 | if (buttonCount < buttonsNeeded) { 56 | buttonRow.addComponents(buttonList[buttonCount]); 57 | buttonCount++; 58 | } 59 | } //End of r loop 60 | messageComponents.push(buttonRow); 61 | } //End of n loop 62 | pm2.disconnect(); 63 | let optionRow = new ActionRowBuilder().addComponents( 64 | new ButtonBuilder().setCustomId(`${config.serverName}~restart`).setLabel(`Restart`).setStyle(ButtonStyle.Primary), 65 | new ButtonBuilder().setCustomId(`${config.serverName}~start`).setLabel(`Start`).setStyle(ButtonStyle.Success), 66 | new ButtonBuilder().setCustomId(`${config.serverName}~stop`).setLabel(`Stop`).setStyle(ButtonStyle.Danger), 67 | new ButtonBuilder().setCustomId(`${config.serverName}~status`).setLabel(`Status`).setStyle(ButtonStyle.Secondary) 68 | ) 69 | messageComponents.push(optionRow); 70 | if (type === 'new') { 71 | channelOrInteraction.send({ 72 | content: `**Status of ${config.serverName} Processes:**\n*Click to restart*`, 73 | components: messageComponents 74 | }).catch(console.error); 75 | } else if (type === 'edit') { 76 | channelOrInteraction.message.edit({ 77 | content: `**Status of ${config.serverName} Processes:**\n*Click to restart*`, 78 | components: messageComponents 79 | }).catch(console.error); 80 | } 81 | }) //End of pm2.list 82 | }) //End of pm2.connect 83 | }, //End of updateStatus() 84 | 85 | 86 | pm2MainMenu: async function pm2MainMenu(interaction, interactionID) { 87 | //Restart menu pressed 88 | if (interactionID === 'restart') { 89 | var newButtons = interaction.message.components; 90 | for (var r = 0; r < newButtons.length - 1; r++) { 91 | var row = newButtons[r]['components']; 92 | for (var b in row) { 93 | row[b]['data']['style'] = ButtonStyle.Primary; 94 | row[b]['data']['custom_id'] = `${config.serverName}~process~restart~${row[b]['label']}`; 95 | } //End of b loop 96 | } //End of r loop 97 | interaction.message.edit({ 98 | content: `**Restart ${config.serverName} Processes:**`, 99 | components: newButtons 100 | }).catch(console.error); 101 | } 102 | //Start menu pressed 103 | else if (interactionID === 'start') { 104 | var newButtons = interaction.message.components; 105 | for (var r = 0; r < newButtons.length - 1; r++) { 106 | var row = newButtons[r]['components']; 107 | for (var b in row) { 108 | row[b]['data']['style'] = ButtonStyle.Success; 109 | row[b]['data']['custom_id'] = `${config.serverName}~process~start~${row[b]['label']}`; 110 | } //End of b loop 111 | } //End of r loop 112 | interaction.message.edit({ 113 | content: `**Start ${config.serverName} Processes:**`, 114 | components: newButtons 115 | }).catch(console.error); 116 | } 117 | //Stop menu pressed 118 | else if (interactionID === 'stop') { 119 | var newButtons = interaction.message.components; 120 | for (var r = 0; r < newButtons.length - 1; r++) { 121 | var row = newButtons[r]['components']; 122 | for (var b in row) { 123 | row[b]['data']['style'] = ButtonStyle.Danger; 124 | row[b]['data']['custom_id'] = `${config.serverName}~process~stop~${row[b]['label']}`; 125 | } //End of b loop 126 | } //End of r loop 127 | interaction.message.edit({ 128 | content: `**Stop ${config.serverName} Processes:**`, 129 | components: newButtons 130 | }).catch(console.error); 131 | } 132 | }, //End of pm2MainMenu 133 | 134 | 135 | runPM2: async function runPM2(channel, user, interactionID) { 136 | pm2.connect(async function (err) { 137 | if (err) { 138 | console.log(`(${user.username}) pm2.connect error:`, err); 139 | } else { 140 | if (interactionID.startsWith('restart~')) { 141 | let processName = interactionID.replace('restart~', ''); 142 | pm2.restart(processName, (err, response) => { 143 | if (err) { 144 | console.log(`${user.username} failed to restart ${processName} PM2 process.`, err); 145 | channel.send({ 146 | embeds: [new EmbedBuilder().setDescription(`Failed to restart ${processName} process.`).setColor('9E0000').setFooter({ 147 | text: `${user.username}` 148 | })], 149 | }).catch(console.error) 150 | .then(msg => { 151 | if (config.pm2.pm2ResponseDeleteSeconds > 0) { 152 | setTimeout(() => msg.delete().catch(err => console.log(`(${user.username}) Error deleting PM2 restart response:`, err)), (config.pm2.pm2ResponseDeleteSeconds * 1000)); 153 | } 154 | }); 155 | } else { 156 | console.log(`${processName} restarted by ${user.username}`); 157 | channel.send({ 158 | embeds: [new EmbedBuilder().setDescription(`PM2 process restarted: ${processName}`).setColor('00841E').setFooter({ 159 | text: `${user.username}` 160 | })], 161 | }).catch(console.error) 162 | .then(msg => { 163 | if (config.pm2.pm2ResponseDeleteSeconds > 0) { 164 | setTimeout(() => msg.delete().catch(err => console.log(`(${user.username}) Error deleting PM2 restart response:`, err)), (config.pm2.pm2ResponseDeleteSeconds * 1000)); 165 | } 166 | }); 167 | } 168 | }); 169 | } else if (interactionID.startsWith('start~')) { 170 | let processName = interactionID.replace('start~', ''); 171 | pm2.start(processName, (err, response) => { 172 | if (err) { 173 | console.log(`${user.username} failed to start ${processName} PM2 process.`, err); 174 | channel.send({ 175 | embeds: [new EmbedBuilder().setDescription(`Failed to start ${processName} process.`).setColor('9E0000').setFooter({ 176 | text: `${user.username}` 177 | })], 178 | }).catch(console.error) 179 | .then(msg => { 180 | if (config.pm2.pm2ResponseDeleteSeconds > 0) { 181 | setTimeout(() => msg.delete().catch(err => console.log(`(${user.username}) Error deleting PM2 start response:`, err)), (config.pm2.pm2ResponseDeleteSeconds * 1000)); 182 | } 183 | }); 184 | } else { 185 | console.log(`${processName} started by ${user.username}`); 186 | channel.send({ 187 | embeds: [new EmbedBuilder().setDescription(`PM2 process started: ${processName}`).setColor('00841E').setFooter({ 188 | text: `${user.username}` 189 | })], 190 | }).catch(console.error) 191 | .then(msg => { 192 | if (config.pm2.pm2ResponseDeleteSeconds > 0) { 193 | setTimeout(() => msg.delete().catch(err => console.log(`(${user.username}) Error deleting PM2 start response:`, err)), (config.pm2.pm2ResponseDeleteSeconds * 1000)); 194 | } 195 | }); 196 | } 197 | }); 198 | } else if (interactionID.startsWith('stop~')) { 199 | let processName = interactionID.replace('stop~', ''); 200 | pm2.stop(processName, (err, response) => { 201 | if (err) { 202 | console.log(`${user.username} failed to stop ${processName} PM2 process.`, err); 203 | channel.send({ 204 | embeds: [new EmbedBuilder().setDescription(`Failed to stop ${processName} PM2 process.`).setColor('9E0000').setFooter({ 205 | text: `${user.username}` 206 | })], 207 | }).catch(console.error) 208 | .then(msg => { 209 | if (config.pm2.pm2ResponseDeleteSeconds > 0) { 210 | setTimeout(() => msg.delete().catch(err => console.log(`(${user.username}) Error deleting PM2 stop response:`, err)), (config.pm2.pm2ResponseDeleteSeconds * 1000)); 211 | } 212 | }); 213 | } else { 214 | console.log(`${processName} stopped by ${user.username}`); 215 | channel.send({ 216 | embeds: [new EmbedBuilder().setDescription(`PM2 process stopped: ${processName}`).setColor('00841E').setFooter({ 217 | text: `${user.username}` 218 | })], 219 | }).catch(console.error) 220 | .then(msg => { 221 | if (config.pm2.pm2ResponseDeleteSeconds > 0) { 222 | setTimeout(() => msg.delete().catch(err => console.log(`(${user.username}) Error deleting PM2 stop response:`, err)), (config.pm2.pm2ResponseDeleteSeconds * 1000)); 223 | } 224 | }); 225 | } 226 | }); 227 | } 228 | } 229 | }) //End of pm2.connect 230 | pm2.disconnect(); 231 | }, //End of runPM2() 232 | } -------------------------------------------------------------------------------- /functions/scripts.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | EmbedBuilder, 11 | ButtonBuilder, 12 | ButtonStyle, 13 | InteractionType, 14 | ChannelType 15 | } = require('discord.js'); 16 | const fs = require('fs'); 17 | const fileExists = require('file-exists'); 18 | const config = require('../config/config.json'); 19 | const scriptList = require('../config/scripts.json'); 20 | const shell = require('shelljs'); 21 | const ansiParser = require("ansi-parser"); 22 | 23 | module.exports = { 24 | sendScriptList: async function sendScriptList(messageOrInteraction, type) { 25 | var selectList = []; 26 | scriptList.forEach(script => { 27 | if (script.fullFilePath !== '') { 28 | var label = script.customName; 29 | if (script.adminOnly === true) { 30 | label = label.concat(' 🔒'); 31 | } 32 | let listOption = { 33 | label: label, 34 | description: script.description ? script.description : '\u00A0', 35 | value: `${config.serverName}~startScript~${script.customName}~${script.variables.length}` 36 | } 37 | selectList.push(listOption); 38 | } 39 | }); 40 | let fullList = new ActionRowBuilder() 41 | .addComponents( 42 | new SelectMenuBuilder() 43 | .setCustomId(`${config.serverName}~scriptList`) 44 | .setPlaceholder('List of Scripts') 45 | .addOptions(selectList)) 46 | if (type === 'new') { 47 | if (selectList.length == 0) { 48 | messageOrInteraction.channel.send({ 49 | embeds: [new EmbedBuilder().setDescription("Error: No scripts found in scripts.json").setColor('9E0000')] 50 | }).catch(console.error); 51 | return; 52 | } 53 | messageOrInteraction.channel.send({ 54 | content: 'Select a script below to run it.', 55 | components: [fullList] 56 | }).catch(console.error); 57 | } else if (type === 'restart') { 58 | messageOrInteraction.message.edit({ 59 | content: 'Select a script below to run it.', 60 | embeds: [], 61 | components: [fullList] 62 | }).catch(console.error); 63 | } 64 | }, //End of sendScriptList() 65 | 66 | 67 | startScript: async function startScript(interaction, userPerms, script, scriptName, variableCount) { 68 | //Check if admin only 69 | if (script.adminOnly === true && !userPerms.includes('admin')) { 70 | console.log(`Non-admin ${interaction.user.username} tried running ${scriptName}`); 71 | return; 72 | } 73 | //Check if file exists 74 | let tempPath = script.fullFilePath.split(' '); 75 | let fileTest = fileExists.sync(tempPath[0]); 76 | if (fileTest === false) { 77 | module.exports.sendScriptList(interaction, "restart"); 78 | interaction.deferUpdate(); 79 | console.log(`(${interaction.user.username}) Script not found: \`${tempPath[0]}\``); 80 | interaction.message.channel.send(`Script not found: \`${tempPath[0]}\``).catch(console.error); 81 | return; 82 | } 83 | //No variables 84 | if (variableCount == 0) { 85 | if (config.scripts.scriptVerify === false) { 86 | module.exports.runScript(interaction, scriptName, ''); 87 | } else { 88 | module.exports.verifyScript(interaction, scriptName, ''); 89 | } 90 | } 91 | //Has variables 92 | else { 93 | interaction.deferUpdate(); 94 | let currentVar = script.variables[0]; 95 | let varDescription = currentVar['varDescription']; 96 | let varOptions = currentVar['varOptions']; 97 | var selectList = []; 98 | varOptions.forEach(variable => { 99 | let bashVariables = variable; 100 | let listOption = { 101 | label: variable, 102 | value: `${config.serverName}~run:${scriptName}~var:1_${variableCount}~${bashVariables}` 103 | } 104 | selectList.push(listOption); 105 | }); 106 | let listsNeeded = Math.ceil(varOptions.length / 24); 107 | var varCounter = 0; 108 | var allComponents = []; 109 | for (var n = 0; n < listsNeeded && n < 5; n++) { 110 | var currentList = []; 111 | let cancelOption = { 112 | label: "CANCEL SCRIPT", 113 | value: `${config.serverName}~cancelScript` 114 | } 115 | currentList.push(cancelOption); 116 | var optionCounter = 0; 117 | for (var v = varCounter; v < varOptions.length && optionCounter < 24; v++) { 118 | currentList.push(selectList[v]); 119 | optionCounter++; 120 | varCounter++; 121 | } //End of v loop 122 | let fullList = new ActionRowBuilder() 123 | .addComponents( 124 | new SelectMenuBuilder() 125 | .setCustomId(`${config.serverName}~runScript${n}`) 126 | .setPlaceholder(`${varDescription}`) 127 | .addOptions(currentList)) 128 | if (listsNeeded > 1) { 129 | fullList.components[0].setPlaceholder(`${varDescription} (${currentList[1]['label']} - ${currentList[currentList.length - 1]['label']})`) 130 | } 131 | allComponents.push(fullList); 132 | } //End of n loop 133 | interaction.message.edit({ 134 | content: `Select variable 1 of ${variableCount} for ${script.customName}`, 135 | components: allComponents 136 | }).catch(console.error); 137 | } //End of has variables 138 | }, //End of startScript() 139 | 140 | 141 | scriptVariables: async function scriptVariables(interaction, userPerms) { 142 | let intItems = interaction.values[0].replace(`${config.serverName}~run:`, '').split('~'); 143 | let scriptName = intItems[0]; 144 | let currentVarCounts = intItems[1].replace('var:', '').split('_'); 145 | let varNumber = currentVarCounts[0] * 1 + 1; 146 | let varNeeded = currentVarCounts[1] * 1; 147 | var bashVariables = intItems[2].split('^'); 148 | for (var s in scriptList) { 149 | if (scriptList[s]['customName'] === scriptName) { 150 | let script = scriptList[s]; 151 | //Check if admin only 152 | if (script.adminOnly === true && !userPerms.includes('admin')) { 153 | console.log(`Non-admin ${interaction.user.username} tried selecting variable for ${scriptName}`); 154 | return; 155 | } 156 | //No more variables needed 157 | if (currentVarCounts[0] == currentVarCounts[1]) { 158 | if (config.scripts.scriptVerify === false) { 159 | module.exports.runScript(interaction, scriptName, bashVariables); 160 | } else { 161 | module.exports.verifyScript(interaction, scriptName, bashVariables); 162 | } 163 | } else { 164 | interaction.deferUpdate(); 165 | let currentVar = script.variables[varNumber - 1]; 166 | let varDescription = currentVar['varDescription']; 167 | let varOptions = currentVar['varOptions']; 168 | var selectList = []; 169 | varOptions.forEach(variable => { 170 | let newbashVariables = `${bashVariables} ${variable}`; 171 | let listOption = { 172 | label: variable, 173 | value: `${config.serverName}~run:${scriptName}~var:${varNumber}_${varNeeded}~${newbashVariables}` 174 | } 175 | selectList.push(listOption); 176 | }); 177 | let listsNeeded = Math.ceil(varOptions.length / 24); 178 | var varCounter = 0; 179 | var allComponents = []; 180 | for (var n = 0; n < listsNeeded && n < 5; n++) { 181 | var currentList = []; 182 | let cancelOption = { 183 | label: "CANCEL SCRIPT", 184 | value: `${config.serverName}~cancelScript` 185 | } 186 | currentList.push(cancelOption); 187 | var optionCounter = 0; 188 | for (var v = varCounter; v < varOptions.length && optionCounter < 24; v++) { 189 | currentList.push(selectList[v]); 190 | optionCounter++; 191 | varCounter++; 192 | } //End of v loop 193 | let fullList = new ActionRowBuilder() 194 | .addComponents( 195 | new SelectMenuBuilder() 196 | .setCustomId(`${config.serverName}~runScript_var${varCounter}`) 197 | .setPlaceholder(`${varDescription}`) 198 | .addOptions(currentList)) 199 | if (listsNeeded > 1) { 200 | fullList.components[0].setPlaceholder(`${varDescription} (${currentList[1]['label']} - ${currentList[currentList.length - 1]['label']})`) 201 | } 202 | allComponents.push(fullList); 203 | } //End of n loop 204 | interaction.message.edit({ 205 | content: `Select variable ${varNumber} of ${varNeeded} for ${scriptName}`, 206 | components: allComponents 207 | }).catch(console.error); 208 | break; 209 | } //End of more variables needed 210 | } //End of = scriptName 211 | } //End of s loop 212 | }, //End of scriptVariables() 213 | 214 | 215 | verifyScript: async function verifyScript(interaction, scriptName, variables) { 216 | interaction.deferUpdate(); 217 | for (var s in scriptList) { 218 | if (scriptList[s]['customName'] === scriptName) { 219 | let optionRow = new ActionRowBuilder().addComponents( 220 | new ButtonBuilder().setCustomId(`${config.serverName}~verifyScript~yes`).setLabel(`Yes`).setStyle(ButtonStyle.Success), 221 | new ButtonBuilder().setCustomId(`${config.serverName}~verifyScript~no`).setLabel(`No`).setStyle(ButtonStyle.Danger) 222 | ) 223 | var title = `**Run script: ${scriptName}?**`; 224 | if (scriptList[s]['adminOnly'] === true) { 225 | title = title.concat(' 🔒'); 226 | } 227 | interaction.message.edit({ 228 | content: title, 229 | embeds: [new EmbedBuilder().setDescription(`bash ${scriptList[s]['fullFilePath']} ${variables}`).setColor('0D00CA').setFooter({ 230 | text: `${interaction.user.username}` 231 | })], 232 | components: [optionRow] 233 | }).catch(console.error); 234 | } 235 | } //End of s loop 236 | }, //End of verifyScript() 237 | 238 | 239 | runScript: async function runScript(interaction, scriptName, variables) { 240 | interaction.deferUpdate(); 241 | var fullBashCommand = ''; 242 | for (var s in scriptList) { 243 | if (scriptList[s]['customName'] === scriptName) { 244 | fullBashCommand = `bash ${scriptList[s]['fullFilePath']} ${variables}`; 245 | } 246 | } //End of s loop 247 | if (fullBashCommand !== '') { 248 | interaction.message.edit({ 249 | content: '**Running script:**', 250 | embeds: [new EmbedBuilder().setDescription(`\`${fullBashCommand}\``).setColor('0D00CA').setFooter({ 251 | text: `${interaction.user.username}` 252 | })], 253 | components: [] 254 | }).catch(console.error); 255 | try { 256 | shell.exec(fullBashCommand, function (exitCode, output) { 257 | module.exports.sendScriptList(interaction, "restart"); 258 | var color = '00841E'; 259 | var description = `\`${fullBashCommand}\`\n\n**Response:**\n${ansiParser.removeAnsi(output).replaceAll('c','')}`; 260 | if (exitCode !== 0) { 261 | color = '9E0000'; 262 | description = `\`${fullBashCommand}\`\n\n**Error Response:**\n${ansiParser.removeAnsi(output).replaceAll('c','')}`; 263 | } 264 | console.log(`${interaction.user.username} ran script: \`${fullBashCommand}\``); 265 | 266 | interaction.message.channel.send({ 267 | embeds: [new EmbedBuilder().setTitle('Ran script:').setDescription(description).setColor(color).setFooter({ 268 | text: `${interaction.user.username}` 269 | })], 270 | }).catch(console.error) 271 | .then(msg => { 272 | if (config.scripts.scriptResponseDeleteSeconds > 0) { 273 | setTimeout(() => msg.delete().catch(err => console.log(`(${interaction.user.username}) Error deleting script response message:`, err)), (config.scripts.scriptResponseDeleteSeconds * 1000)); 274 | } 275 | }) 276 | }) 277 | } catch (err) { 278 | console.log(`Failed to run script: ${fullBashCommand}:`, err); 279 | module.exports.sendScriptList(interaction, "restart"); 280 | interaction.message.channel.send({ 281 | embeds: [new EmbedBuilder().setTitle('Failed to run script:').setDescription(fullBashCommand).setColor('9E0000').setFooter({ 282 | text: `${interaction.user.username}` 283 | })], 284 | }).catch(console.error) 285 | .then(msg => { 286 | if (config.scripts.scriptResponseDeleteSeconds > 0) { 287 | setTimeout(() => msg.delete().catch(err => console.log(`(${interaction.user.username}) Error deleting script response message:`, err)), (config.scripts.scriptResponseDeleteSeconds * 1000)); 288 | } 289 | }) 290 | } 291 | } 292 | }, //End of runScript() 293 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MadGruber Bot 2 | 3 | ## About 4 | A Discord bot used as a very basic GUI for your server along with some MAD-specific features. Examples shown below. 5 | 6 | ###### Disclaimer: This bot might look completely insane and the process it uses at times probably makes no sense but it almost always gets the damn job done, sometimes in a blaze of glory. **MADGRUBER!!!** 7 | 8 | Join the Discord server for any help and to keep up with updates: https://discord.gg/USxvyB9QTz 9 | 10 | 11 | **Current Features:** 12 | - PM2 controller (start/stop/restart + current status) 13 | - Truncate MAD quests and auto reload MAD processes 14 | - Automated quest rescanning 15 | - Convert MAD geofences to other formats 16 | - Custom SQL queries 17 | - Run custom scripts with optional variables 18 | - Quickly access URL bookmarks 19 | - Reaction role manager 20 | - Limit commands to only certain roles 21 | - Optional slash commands available 22 | - Options to verify certain actions first 23 | - See current status of MAD devices (as buttons) 24 | - Click device buttons to get basic info 25 | - Command to check for only devices that haven't been seen lately (automated checks optional) 26 | - RaspberryRelay integration to automatically power cycle noProto devices 27 | - dkmur's deviceControl integration (Pause/unpause/start/quit/reboot/clear data/logcat/screenshot/power cycle/send worker) 28 | - dkmur's Stats integration (expanded device info/graphs for different device and system stats) 29 | 30 |   31 |   32 |   33 | ## Requirements 34 | 1: Node 16+ installed on server 35 | 36 | 2: Discord bot with: 37 | - Server Members Intent 38 | - Message Content Intent 39 | - Read/write perms in channels 40 | - Manage Roles perm (if using role feature) 41 | 42 |   43 |   44 | 45 | ## Install 46 | ``` 47 | git clone https://github.com/RagingRectangle/MadGruber.git 48 | cd MadGruber 49 | cp -r config.example config 50 | npm install 51 | ``` 52 | 53 |   54 |   55 | 56 | ## Optional Projects to Install 57 | - [RaspberryRelay](https://github.com/RagingRectangle/RaspberryRelay) 58 | - [deviceControl by dkmur](https://github.com/dkmur/deviceControl) 59 | - [Stats by dkmur](https://github.com/dkmur/Stats) 60 | 61 |   62 |   63 | 64 | ## Config Setup 65 | - **serverName:** Custom name for your server. 66 | - **delaySeconds:** If used on multiple servers you can use this to make sure the bot always responds in a specific order. 67 | 68 | Discord: 69 | - **token:** Discord bot token. 70 | - **prefix:** Used in front of Discord commands. 71 | - **adminIDs:** List of Discord user IDs that can execute all commands. 72 | - **channelIDs:** List of channel IDs that the bot will respond in. Does not work with DMs to the bot 73 | - **useSlashCommands:** Whether or not to register slash commands in guilds (true/false). 74 | - Currently available: `helpCommand`, `pm2Command`, `truncateCommand`, `madQueryCommand`, `linksCommand`, `devicesCommand`, `noProtoCommand`, `eventsCommand`, `systemStatsCommand`, `sendWorkerCommand`, `grepCommand`, `geofenceCommand` 75 | - [Bot must have applications.commands scope](https://discordjs.guide/preparations/adding-your-bot-to-servers.html#creating-and-using-your-invite-link) 76 | - **slashGuildIDs:** List of guild IDs where commands should be registered. 77 | - **helpCommand:** Show correct syntax and what perms the user has. 78 | - **pm2Command:** Show the PM2 controller. 79 | - **truncateCommand:** Truncate quests and restart MAD instances. 80 | - **scriptCommand:** Show the list of scripts. 81 | - **queryCommand:** Show custom query list. 82 | - **linksCommand:** Show list of bookmarks. 83 | - **devicesCommand:** Get status of all devices. 84 | - **noProtoCommand:** Get noProto devices. 85 | - **systemStatsCommand:** See system stat options (if using dkmur's Stats). 86 | - **sendWorkerCommand:** Send closest worker to a location. `!sendworker lat,lon` (if using dkmur's deviceControl). 87 | - **eventsCommand:** View list of quest reroll events if enabled. 88 | - **grepCommand:** Search uploaded file for string and return the lines where it's included (Only slash command). 89 | - **geofenceCommand:** Convert MAD geofences to other formats to be used in other scanner projects (admins only). GeoJSON and "SimpleJSON" (geo.jasparke) formats. MAD geofences with multiple sections will be separated into their own areas with random colors. Best usage is for something like adding new Poracle area. Draw fence in MADmin, run command, copy/paste section into config file. 90 | 91 | PM2: 92 | - **mads:** List of MAD PM2 processes that should be restarted after truncating quests. 93 | - **ignore:** List of PM2 processes/modules to ignore if you don't want buttons for them. 94 | - **pm2ResponseDeleteSeconds:** How long to wait until pm2 response is deleted (Set to 0 to never delete). 95 | 96 | Roles: 97 | - **sendRoleMessage:** Whether or not to send role added/removed messages (true/false). 98 | - **roleMessageDeleteSeconds:** How long to wait until role message is deleted (Set to 0 to never delete). 99 | - **commandPermRoles:** List of command types and the role IDs that are allowed to use them (deviceInfo: users can only see info, deviceInfoControl: users can both see and control devices). 100 | 101 | Truncate: 102 | - **truncateVerify:** Whether or not to verify table truncate (true/false). 103 | - **truncateOptions:** List of tables to list as options to truncate. Can truncate multiple tables at once by combining them with '+'. Example: *["trs_quest", "pokemon", "trs_quest+pokemon"]* 104 | - **truncateQuestsByArea:** Select instance Pokestop areas to truncate instead of entire trs_quest table (true/false). 105 | - **onlyRestartBeforeTime:** Set this to limit when the bot will reload MAD instance (0-23). If set to 0 it will always reload MADs. If an event ends at 20:00 and you don't need to reload MAD because you won't rescan quests then enter "20". 106 | - **eventAutomation:** Whether or not to use Discord events to automate quest truncating (true/false). 107 | - **eventGuildID:** ID of the guild where events are located. 108 | - **eventDescriptionTrigger:** The trigger word/s that must be in the event description to automate truncating. 109 | - **eventAlertChannelID:** ID of the channel where automated alert will be posted. Footer will show whether or not quest truncated and MAD restarted successfully. 110 | - **eventAlertDeleteSeconds:** How long to wait until event alert is deleted (Set to 0 to never delete). 111 | 112 | Scripts: 113 | - **scriptVerify:** Whether or not to verify running script (true/false). 114 | - **scriptResponseDeleteSeconds:** How long to wait until script response is deleted (Set to 0 to never delete). 115 | 116 | madDB: 117 | - Enter your basic MAD database info. Make sure your user has access if the database is not local. Leave blank if you don't plan on connecting to MAD. 118 | - **timezoneDifference:** Timezone offset of MAD in hours. ONLY if MAD and the database are set to different timezones. 119 | 120 | Devices: 121 | - **noProtoMinutes:** Limit for how long it's been since the device has been heard from. 122 | - **noProtoCheckMinutes:** Automate checks for unseen devices (Set to 0 to disable auto-check). 123 | - **noProtoChannelID:** Channel ID for where automated warning should be posted. 124 | - **noProtoIncludeIdle:** Include paused and idle devices in automated checks (false to ignore). 125 | - **noProtoIgnoreDevices:** Array of devices to be ignored during noProto checks. 126 | - **useNoProtoJson:** Use noProto.json to split which channels noProto device alerts are sent to (true/false). If 'noProtoChannelID' is set then that will be the default channel for any devices not listed in noProto.json. 127 | - **checkDeleteMinutes:** How long to wait until auto check messages are deleted (Set to 0 to never delete). 128 | - **infoMessageDeleteSeconds:** How long to wait until device info responses are deleted (Set to 0 to never delete). 129 | - **statusButtonsDeleteMinutes:** How long to wait until messages with device buttons are deleted (Set to 0 to never delete). 130 | - **buttonLabelRemove:** List of strings to ignore when posting device buttons. Button rows can look [crappy](https://media.discordapp.net/attachments/923445551595401316/933223709265764442/MadGruber_DeviceName_Differences.png) on mobile so this can help. 131 | - **displayOptions:** Customize what info is displayed for devices (true/false). 132 | 133 | DeviceControl: 134 | - [Install info](https://github.com/dkmur/deviceControl) 135 | - **path:** Path to root folder. 136 | - **controlResponseDeleteSeconds:** How long to wait until script responses are deleted (Set to 0 to never delete). 137 | - **logcatDeleteSeconds:** How long to wait until logcats are deleted (Set to 0 to never delete). 138 | - **reverseLogcat:** Reverse logcat output so the most recent entry is on top (true/false). 139 | - **screenshotDeleteSeconds:** How long to wait until screenshots are deleted (Set to 0 to never delete). 140 | - **powerCycleType:** Set to "deviceControl" if using deviceControl to power cycle your devices. Set to "raspberry" if using RaspberryRelay. 141 | 142 | Stats: 143 | - [Install info](https://github.com/dkmur/Stats) 144 | - **database:** Basic stats database info. 145 | - **dataPointCount:** How many individual points on graphs for each type. 146 | - **colorPalette:** Colors used for stat graphs. Accepts all common color names. (Default 1:orange, 2:green, 3:navy) 147 | - **graphDeleteSeconds:** How long to wait until graphs are deleted (Set to 0 to never delete). 148 | - **deviceInfo:** Customize what is added to the device info displayed (true/false). 149 | 150 |   151 |   152 | 153 | ## Scripts Setup 154 | - Config file: */config/scripts.json* 155 | - Absolute paths must be used in scripts to work. Look in the scripts.example.json to get an idea of how they can work. 156 | - **customName:** Display name in list. 157 | - **adminOnly:** Script level overrides to ignore users with script role (true/false). 158 | - **description:** Short summary shown in list. 159 | - **fullFilePath:** The absolute path to the file. 160 | - Ex: `/home/mad/devicecontrol.sh` 161 | - Tip: If the same variables are always passed you can add them to the path. 162 | - Ex: `/home/mad/devicecontrol.sh poe4 cycle 20` 163 | 164 | - **variables:** Make sure each variable is in the correct order because that is how it will be sent with the script. 165 | - **varDescription:** Summary of this list of variables that will be shown. ("Pick which device" or "Choose the port"). 166 | - **varOptions:** The list of options that this variable can be ("1", "2", "3", "4", "5"). 167 | 168 |   169 |   170 | 171 | ## Links Setup 172 | - Config file: */config/links.json* 173 | - Add up to 25 links as buttons. 174 | - Emoji field is optional. 175 | - Full emoji string `<:mad:475050731032936448>` 176 | - Unicode form (Get correct form by escaping default emojis: `\😺`). 177 | 178 |   179 |   180 | 181 | ## Reaction Role Setup 182 | - Config file: */config/roles.json* 183 | - **messageID:** The ID of the message with the emojis users can select to add/remove roles. 184 | - **roleID:** The ID for the role that can be added/removed. 185 | - **emojiName:** The unicode emoji or the custom emoji name (only the name, NOT full emoji string). 186 | 187 |   188 |   189 | 190 | ## Custom Query Setup 191 | - Config file: */config/queries.json* 192 | - **name:** Query name to display in lists. 193 | - **query:** The SQL query to run. Multiple statements allowed separated by `;` (Response will show query results in this order). This will __not__ use the `timezoneDifference` config option so any adjustments will need to made in the query itself. 194 | 195 | 196 | 197 |   198 |   199 | 200 | ## Automated Quest Reroll Setup 201 | - Truncate quests and restart MAD (if needed) automatically using Discord's event feature. 202 | - If there are multiple events that start/stop at the same time everything will only be done once. 203 | - If you're OCD like myself and don't like seeing the event icon on the server image, create a throwaway guild and use that. 204 | - How to create events: 205 | 1: Open guild menu and select 'Create Event' 206 | 2: Select 'Somewhere Else' and enter anything for location such as 'PoGo' (will not be used) 207 | 3: Enter a name for the event 208 | 4: Select the start and end times when quests will reroll 209 | 5: Enter the `eventDescriptionTrigger` into the description along with any other info you'd like to include 210 | 211 |   212 |   213 | 214 | ## Usage 215 | - Start the bot in a console with `node madgruber.js` 216 | - Can also use PM2 to run it instead with `pm2 start madgruber.js` 217 | - Bot will reply with the PM2 controller message when you send `` 218 | - Press the Reload/Start/Stop buttons and then the processes you'd like to change. 219 | - Press the Status button to see the current status of processes. 220 | - Bot will truncate and reload MADs when you send `` 221 | - Bot will reply with runnable scripts when sent `` 222 | - Get runnable MAD database queries with `` 223 | - Get link buttons with `` 224 | - Get status of devices as buttons with `` 225 | - Press device button to get more info. 226 | - If deviceControl and/or Stats is installed then dropdown lists will appear. 227 | - Get info about specific device with `` 228 | - See any naughty devices with `` 229 | - See system stats with `` (Requires dkmur's Stats) 230 | - Send worker to location with ` ,` (Requires dkmur's Stats) 231 | - Get list of events that will reroll quests with `` 232 | - Search file for string and return lines with it included with `/` (slash only) 233 | - Convert MAD geofences to other formats with `` 234 | 235 |   236 | 237 | 238 | 239 | ## Examples 240 | ###### PM2 Controller: 241 | ![PM2](https://media.giphy.com/media/NXURwVTS9bdRXHMt49/giphy.gif) 242 | ###### Run Custom Scripts: 243 | ![Scripts](https://media.giphy.com/media/KVzaguhH4o99CLZs09/giphy.gif) 244 | ###### Truncate Tables With Option to Restart MAD: 245 | ![Truncate](https://media.giphy.com/media/St6S6xtcMFEbOh9z18/giphy.gif) 246 | ###### Get Basic Info About MAD Database: 247 | ![Queries](https://media.giphy.com/media/jNplcSy5fUYyci94Fg/giphy.gif) 248 | ###### Quick Links: 249 | ![Links](https://media.giphy.com/media/Mz1mf6OJyL727WnkGe/giphy.gif) 250 | ###### Device Status and Info: 251 | ![Links](https://media.giphy.com/media/Vy9Jj0mxnWlvoCjjJX/giphy.gif) 252 | ###### DeviceControl Options: 253 | ![DeviceControl](https://media.giphy.com/media/Ico3HomI8b1YozLfbM/giphy.gif) 254 | ###### Device Stats: 255 | ![StatsOptions](https://media.giphy.com/media/MtnvGOZG5dIamf5xqn/giphy.gif) 256 | ###### Stats Examples: 257 | ![StatsExamples](https://media.giphy.com/media/ddaBKKWjoxyWmruBl6/giphy.gif) -------------------------------------------------------------------------------- /functions/interactions.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | EmbedBuilder, 11 | ButtonBuilder, 12 | InteractionType, 13 | ChannelType 14 | } = require('discord.js'); 15 | const fs = require('fs'); 16 | const pm2 = require('pm2'); 17 | const shell = require('shelljs'); 18 | const ansiParser = require("ansi-parser"); 19 | const Pm2Buttons = require('./pm2.js'); 20 | const Truncate = require('./truncate.js'); 21 | const Scripts = require('./scripts.js'); 22 | const Queries = require('./queries.js'); 23 | const Devices = require('./devices.js'); 24 | const DeviceControl = require('./deviceControl.js'); 25 | const Stats = require('./stats.js'); 26 | const Geofences = require('./geofenceConverter.js'); 27 | const config = require('../config/config.json'); 28 | const queryConfig = require('../config/queries.json'); 29 | const scriptConfig = require('../config/scripts.json'); 30 | 31 | module.exports = { 32 | listInteraction: async function listInteraction(interaction, interactionID, userPerms) { 33 | //Scripts 34 | if (userPerms.includes('scripts')) { 35 | if (interactionID === 'scriptList') { 36 | let intValues = interaction.values[0].replace(`${config.serverName}~startScript~`, '').split('~'); 37 | let scriptName = intValues[0]; 38 | let variableCount = intValues[1] * 1; 39 | for (var s in scriptConfig) { 40 | if (scriptName === scriptConfig[s]['customName'] && scriptConfig[s]['fullFilePath']) { 41 | Scripts.startScript(interaction, userPerms, scriptConfig[s], scriptName, variableCount); 42 | } 43 | } 44 | } else if (interactionID.startsWith('runScript')) { 45 | if (interaction.values[0] === `${config.serverName}~cancelScript`) { 46 | interaction.deferUpdate(); 47 | Scripts.sendScriptList(interaction, 'restart'); 48 | } else { 49 | Scripts.scriptVariables(interaction, userPerms); 50 | } 51 | } 52 | } //End of Scripts 53 | 54 | //Queries 55 | if (userPerms.includes('queries')) { 56 | if (interactionID === 'queryList') { 57 | let queryName = interaction.values[0].replace(`${config.serverName}~customQuery~`, ''); 58 | interaction.update({}); 59 | for (var i in queryConfig.custom){ 60 | if (queryConfig.custom[i]['name'] === queryName){ 61 | Queries.customQuery(interaction.message.channel, interaction.user, queryName, queryConfig.custom[i]['query']); 62 | } 63 | }//End of i loop 64 | } 65 | } //End of queries 66 | 67 | //DeviceControl 68 | if (userPerms.includes('deviceInfoControl')) { 69 | if (interactionID === 'deviceControl') { 70 | interaction.deferUpdate(); 71 | DeviceControl.deviceControl(interaction); 72 | } 73 | } //End of DeviceControl 74 | 75 | //DeviceStats 76 | if (userPerms.includes('deviceInfoControl') || userPerms.includes('deviceInfo')) { 77 | if (interactionID.startsWith('deviceStats~')) { 78 | interaction.deferUpdate(); 79 | let statVariables = interaction.values[0].replace(`${config.serverName}~deviceStats~`, '').split('~'); 80 | let origin = statVariables[0]; 81 | let statVars = interaction.values[0].replace(`${config.serverName}~deviceStats~${origin}~`, ''); 82 | Stats.deviceStats(interaction, origin, statVars); 83 | } 84 | } //End of DeviceStats 85 | 86 | //SystemStats 87 | if (userPerms.includes('systemStats')) { 88 | if (interactionID.startsWith('systemStats~')) { 89 | interaction.deferUpdate(); 90 | let statDuration = interactionID.replace('systemStats~', ''); 91 | Stats.systemStats(interaction.message.channel, interaction.user, statDuration, interaction.values[0].replace(`${config.serverName}~systemStats~`, '')); 92 | } 93 | } //End of SystemStats 94 | 95 | //Truncate 96 | if (userPerms.includes('truncate')) { 97 | //Area quests 98 | if (interactionID.startsWith('truncateArea~')) { 99 | interaction.deferUpdate(); 100 | interaction.message.edit({ 101 | components: interaction.message.components 102 | }); 103 | let areaList = interaction.values; 104 | if (config.truncate.truncateVerify === false) { 105 | Truncate.collectAreaQuests(interaction.message.channel, interaction.user, interactionID.replace('truncateArea~', ''), areaList); 106 | } else { 107 | Truncate.verifyAreaQuests(interaction.message.channel, interaction.user, interactionID.replace('truncateArea~', ''), areaList); 108 | } 109 | } //End of area quests 110 | } //End of Truncate 111 | 112 | //Admin only 113 | if (userPerms.includes('admin')) { 114 | //GeofenceConverter 115 | if (interactionID.startsWith('geofenceList~')) { 116 | interaction.deferUpdate(); 117 | let intValue = interaction.values[0].split('~~'); 118 | Geofences.convert(interaction.message.channel, interaction.user, intValue[0]); 119 | interaction.message.edit({ 120 | components: interaction.message.components 121 | }).catch(console.error); 122 | } //End of GeofenceConverter 123 | } 124 | }, //End of listInteraction() 125 | 126 | 127 | buttonInteraction: async function buttonInteraction(interaction, interactionID, userPerms) { 128 | //PM2 129 | if (userPerms.includes('pm2')) { 130 | pm2MenuButtons = ["restart", "start", "stop"]; 131 | if (pm2MenuButtons.includes(interactionID)) { 132 | interaction.deferUpdate(); 133 | Pm2Buttons.pm2MainMenu(interaction, interactionID) 134 | } 135 | //Status menu pressed 136 | else if (interactionID === 'status') { 137 | interaction.deferUpdate(); 138 | Pm2Buttons.updateStatus(interaction, 'edit'); 139 | } 140 | //Run PM2 process 141 | else if (interactionID.startsWith('process~')) { 142 | interaction.deferUpdate(); 143 | Pm2Buttons.runPM2(interaction.message.channel, interaction.user, interactionID.replace('process~', '')); 144 | } 145 | } //End of pm2 146 | 147 | //Scripts 148 | if (userPerms.includes('scripts')) { 149 | if (interactionID.startsWith('verifyScript~')) { 150 | var scriptName = interaction.message.content.replace('Run script: ', ''); 151 | //Check if admin only 152 | if (interaction.message.content.endsWith('🔒')) { 153 | scriptName = scriptName.replace('? 🔒', ''); 154 | if (!userPerms.includes('admin')) { 155 | console.log(`Non-admin ${interaction.user.username} tried to verify running ${scriptName}`); 156 | return; 157 | } 158 | } else { 159 | scriptName = scriptName.slice(0, -1); 160 | } 161 | interaction.deferUpdate(); 162 | let runScript = interactionID.replace('verifyScript~', ''); 163 | if (runScript === 'no') { 164 | Scripts.sendScriptList(interaction, 'restart'); 165 | interaction.message.channel.send({ 166 | content: '**Did not run script:**', 167 | embeds: [new EmbedBuilder().setDescription(interaction.message.embeds[0]['description']).setColor('9E0000').setFooter({ 168 | text: `${interaction.user.username}` 169 | })], 170 | components: [] 171 | }).catch(console.error) 172 | .then(msg => { 173 | setTimeout(() => msg.delete().catch(err => console.log("Error deleting verify script message:", err)), 10000); 174 | }) 175 | } //End of no 176 | else if (runScript === 'yes') { 177 | let fullBashCommand = interaction.message.embeds[0]['description']; 178 | interaction.message.edit({ 179 | content: '**Running script:**', 180 | embeds: [new EmbedBuilder().setDescription(`\`${fullBashCommand}\``).setColor('0D00CA').setFooter({ 181 | text: `${interaction.user.username}` 182 | })], 183 | components: [] 184 | }).catch(console.error); 185 | try { 186 | shell.exec(fullBashCommand, function (exitCode, output) { 187 | Scripts.sendScriptList(interaction, "restart"); 188 | var color = '00841E'; 189 | var description = `${interaction.message.embeds[0]['description']}\n\n**Response:**\n${ansiParser.removeAnsi(output).replaceAll('c','')}`; 190 | if (exitCode !== 0) { 191 | color = '9E0000'; 192 | description = `${interaction.message.embeds[0]['description']}\n\n**Error Response:**\n${ansiParser.removeAnsi(output).replaceAll('c','')}`; 193 | } 194 | console.log(`${interaction.user.username} ran script: \`${fullBashCommand}\``); 195 | interaction.message.channel.send({ 196 | content: '**Ran script:**', 197 | embeds: [new EmbedBuilder().setDescription(description).setColor(color).setFooter({ 198 | text: `${interaction.user.username}` 199 | })], 200 | components: [] 201 | }).catch(console.error) 202 | .then(msg => { 203 | if (config.scripts.scriptResponseDeleteSeconds > 0) { 204 | setTimeout(() => msg.delete().catch(err => console.log(`(${interaction.user.username}) Error deleting script response message:`, err)), (config.scripts.scriptResponseDeleteSeconds * 1000)); 205 | } 206 | }) 207 | }); 208 | } catch (err) { 209 | console.log(`(${interaction.user.username}) Failed to run script: ${fullBashCommand}:`, err); 210 | Scripts.sendScriptList(interaction, "restart"); 211 | interaction.message.channel.send({ 212 | embeds: [new EmbedBuilder().setTitle('Failed to run script:').setDescription(interaction.message.embeds[0]['description']).setColor('9E0000').setFooter({ 213 | text: `${interaction.user.username}` 214 | })], 215 | components: [] 216 | }).catch(console.error) 217 | .then(msg => { 218 | if (config.scripts.scriptResponseDeleteSeconds > 0) { 219 | setTimeout(() => msg.delete().catch(err => console.log("Error deleting script response message:", err)), (config.scripts.scriptResponseDeleteSeconds * 1000)); 220 | } 221 | }) 222 | } 223 | } //End of yes 224 | } 225 | } //End of scripts 226 | 227 | //Truncate 228 | if (userPerms.includes('truncate')) { 229 | //Verify truncate 230 | if (interactionID.startsWith('verifyTruncate~') || interactionID.startsWith('verifyAreaQuests~')) { 231 | interaction.deferUpdate(); 232 | var instanceName = ''; 233 | var verify = interactionID.replace('verifyTruncate~', ''); 234 | if (interactionID.startsWith('verifyAreaQuests~')) { 235 | instanceName = interactionID.replace('verifyAreaQuests~', '').replace('~yes', '').replace('~no', ''); 236 | verify = interactionID.replace(`verifyAreaQuests~${instanceName}~`, ''); 237 | } 238 | if (verify === 'no') { 239 | interaction.message.edit({ 240 | embeds: [new EmbedBuilder().setTitle('Did not truncate:').setDescription(interaction.message.embeds[0]['description']).setColor('9E0000').setFooter({ 241 | text: `${interaction.user.username}` 242 | })], 243 | components: [] 244 | }).catch(console.error); 245 | setTimeout(() => interaction.message.delete().catch(err => console.log("Error deleting verify truncate message:", err)), 10000); 246 | } //End of no 247 | else if (verify === 'yes') { 248 | //Truncate table 249 | if (interactionID.startsWith('verifyTruncate~')) { 250 | let tables = interaction.message.embeds[0]['description'].split('\n'); 251 | Truncate.truncateTables(interaction.message, interaction.user, tables); 252 | } 253 | //Truncate Area Quests 254 | else if (interactionID.startsWith('verifyAreaQuests~')) { 255 | let areaList = interaction.message.embeds[0]['description'].split('\n'); 256 | Truncate.collectAreaQuests(interaction.message.channel, interaction.user, instanceName, areaList); 257 | interaction.message.delete().catch(console.error); 258 | } 259 | } //End of yes 260 | } //End of verify truncate 261 | 262 | //Run truncate 263 | if (interactionID.startsWith('truncate~')) { 264 | interaction.deferUpdate(); 265 | let buttonTables = interactionID.replace('truncate~', ''); 266 | let tables = buttonTables.split('+'); 267 | if (buttonTables === '!CANCEL!') { 268 | setTimeout(() => interaction.message.delete().catch(err => console.log("Error deleting truncate message:", err)), 1); 269 | } else { 270 | if (config.truncate.truncateVerify === false) { 271 | Truncate.truncateTables(interaction.message, interaction.user, tables); 272 | } else { 273 | Truncate.verifyTruncate(interaction.message.channel, interaction.user, tables); 274 | } 275 | } 276 | } //End of run truncate 277 | } //End of truncate 278 | 279 | //Devices 280 | if (userPerms.includes('deviceInfoControl') || userPerms.includes('deviceInfo')) { 281 | if (interactionID.startsWith('deviceInfo~')) { 282 | interaction.deferUpdate(); 283 | let deviceID = interactionID.replace('deviceInfo~', ''); 284 | Devices.getDeviceInfo(interaction.message.channel, interaction.user, deviceID); 285 | } 286 | } //End of devices 287 | }, //End of buttonInteraction() 288 | } -------------------------------------------------------------------------------- /functions/truncate.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | EmbedBuilder, 11 | ButtonBuilder, 12 | ButtonStyle, 13 | InteractionType, 14 | ChannelType 15 | } = require('discord.js'); 16 | const { 17 | insidePolygon 18 | } = require('geolocation-utils'); 19 | const mysql = require('mysql2'); 20 | const pm2 = require('pm2'); 21 | const fs = require('fs'); 22 | const config = require('../config/config.json'); 23 | const dbInfo = require('../MAD_Database_Info.json'); 24 | 25 | module.exports = { 26 | sendTruncateMessage: async function sendTruncateMessage(channel) { 27 | let truncateTableList = config.truncate.truncateOptions; 28 | var buttonList = []; 29 | if (truncateTableList.length === 0) { 30 | channel.send('No tables are set in config.').catch(console.error); 31 | return; 32 | } 33 | if (truncateTableList.length > 24) { 34 | console.log("ERROR: Max number of truncate options is 24"); 35 | channel.send("ERROR: Max number of truncate options is 24").catch(console.error); 36 | return; 37 | } 38 | for (var t in truncateTableList) { 39 | let tableLabel = truncateTableList[t].replace(/\+/g, " + "); 40 | let buttonID = `${config.serverName}~truncate~${truncateTableList[t].toLowerCase()}`; 41 | let button = new ButtonBuilder().setCustomId(buttonID).setLabel(tableLabel).setStyle(ButtonStyle.Primary); 42 | buttonList.push(button); 43 | } //End of t loop 44 | let cancelButton = new ButtonBuilder().setCustomId(`${config.serverName}~truncate~!CANCEL!`).setLabel('Cancel').setStyle(ButtonStyle.Danger); 45 | buttonList.push(cancelButton); 46 | var buttonsNeeded = buttonList.length; 47 | let rowsNeeded = Math.ceil(buttonList.length / 5); 48 | var buttonCount = 0; 49 | var messageComponents = []; 50 | for (var n = 0; n < rowsNeeded && n < 5; n++) { 51 | var buttonRow = new ActionRowBuilder() 52 | for (var r = 0; r < 5; r++) { 53 | if (buttonCount < buttonsNeeded) { 54 | buttonRow.addComponents(buttonList[buttonCount]); 55 | buttonCount++; 56 | } 57 | } //End of r loop 58 | messageComponents.push(buttonRow); 59 | } //End of n loop 60 | channel.send({ 61 | content: `**Truncate Table:**`, 62 | components: messageComponents 63 | }).catch(console.error); 64 | }, //End of sendTruncateMessage() 65 | 66 | truncateTables: async function truncateTables(msg, user, tables) { 67 | let connection = mysql.createConnection(config.madDB); 68 | connection.connect(); 69 | msg.edit({ 70 | embeds: [new EmbedBuilder().setTitle('Truncate Results:').setDescription('**Truncating...**')], 71 | components: [] 72 | }).catch(console.error); 73 | var good = []; 74 | var bad = []; 75 | var restartMAD = false; 76 | for (var t in tables) { 77 | if (tables[t] === 'trs_quest' && config.truncate.truncateQuestsByArea === true) { 78 | module.exports.areaQuestsMain(msg.channel, user); 79 | continue; 80 | } 81 | let truncateQuery = `TRUNCATE ${tables[t]}`; 82 | connection.query(truncateQuery, function (err, results) { 83 | if (err) { 84 | console.log(`(${user.username}) Error truncating ${config.madDB.database}.${tables[t]}:`, err); 85 | bad.push(tables[t]); 86 | } else { 87 | console.log(`${config.madDB.database}.${tables[t]} truncated by ${user.username}`); 88 | good.push(tables[t]); 89 | if (tables[t] === 'trs_quest' && config.pm2.mads.length > 0) { 90 | let date = new Date(); 91 | let hour = date.getHours(); 92 | var onlyRestartTime = 0; 93 | if (config.truncate.onlyRestartBeforeTime !== '') { 94 | onlyRestartTime = config.truncate.onlyRestartBeforeTime * 1; 95 | } 96 | if (onlyRestartTime == 0 || hour < config.truncate.onlyRestartBeforeTime) { 97 | restartMAD = true; 98 | } 99 | } 100 | } 101 | }); 102 | await new Promise(done => setTimeout(done, 5000)); 103 | } //End of t loop 104 | connection.end(); 105 | if (good.length == 0 && bad.length == 0) { 106 | return; 107 | } 108 | var color = '00841E'; 109 | var description = `Successful:\n- ${good.join('\n- ')}`; 110 | if (good.length == 0) { 111 | description = ''; 112 | } 113 | if (bad.length > 0) { 114 | color = '9E0000'; 115 | description = description.concat(`\n\nFailed:\n- ${bad.join('\n- ')}`); 116 | } 117 | msg.edit({ 118 | embeds: [new EmbedBuilder().setTitle('Truncate Results:').setDescription(description).setColor(color).setFooter({ 119 | text: `${user.username}` 120 | })], 121 | components: [] 122 | }).catch(console.error); 123 | if (restartMAD === true) { 124 | module.exports.restartMADs(msg, user, description, color); 125 | } 126 | }, //End of truncateTables() 127 | 128 | 129 | verifyTruncate: async function verifyTruncate(channel, user, tables) { 130 | let optionRow = new ActionRowBuilder().addComponents( 131 | new ButtonBuilder().setCustomId(`${config.serverName}~verifyTruncate~yes`).setLabel(`Yes`).setStyle(ButtonStyle.Success), 132 | new ButtonBuilder().setCustomId(`${config.serverName}~verifyTruncate~no`).setLabel(`No`).setStyle(ButtonStyle.Danger) 133 | ); 134 | var tableList = []; 135 | for (var t in tables) { 136 | if (tables[t] === 'trs_quest' && config.truncate.truncateQuestsByArea === true) { 137 | module.exports.areaQuestsMain(channel, user); 138 | } else { 139 | tableList.push(tables[t]); 140 | } 141 | } 142 | if (tableList.length == 0) { 143 | return; 144 | } 145 | var title = 'Truncate the following table?'; 146 | if (tableList.length > 1) { 147 | title = 'Truncate the following tables?'; 148 | } 149 | channel.send({ 150 | embeds: [new EmbedBuilder().setTitle(title).setDescription(tableList.join('\n')).setColor('0D00CA')], 151 | components: [optionRow] 152 | }).catch(console.error); 153 | }, //End of verifyTruncate() 154 | 155 | 156 | restartMADs: async function restartMADs(msg, user, description, color) { 157 | msg.edit({ 158 | embeds: [new EmbedBuilder().setTitle('Truncate Results:').setDescription(`${description}\n\n**Restarting MADs...**`).setColor(color)], 159 | components: [] 160 | }).catch(console.error); 161 | let mads = config.pm2.mads; 162 | var good = []; 163 | var bad = []; 164 | await pm2.connect(async function (err) { 165 | if (err) { 166 | console.error(err); 167 | } else { 168 | for (m in mads) { 169 | let processName = mads[m]; 170 | pm2.restart(processName, (err, response) => { 171 | if (err) { 172 | console.error(`(${user.username}) PM2 ${mads[m]} restart error:`, err); 173 | bad.push(mads[m]); 174 | } else { 175 | console.log(`${mads[m]} restarted by ${user.username}`); 176 | good.push(mads[m]); 177 | } 178 | }) //End of pm2.restart 179 | await new Promise(done => setTimeout(done, 5000)); 180 | } //End of m loop 181 | } 182 | }) //End of pm2.connect 183 | await new Promise(done => setTimeout(done, 5000 * mads.length + 1000)); 184 | pm2.disconnect(); 185 | color = '00841E'; 186 | var newDescription = `${description}\n\n**MAD Restart Results:**\nSuccessful:\n- ${good.join('\n- ')}`; 187 | if (good.length == 0) { 188 | newDescription = newDescription.concat(`None!`); 189 | } 190 | if (bad.length > 0) { 191 | color = '9E0000'; 192 | newDescription = newDescription.concat(`\n\nFailed:\n- ${bad.join('\n- ')}`); 193 | } 194 | msg.edit({ 195 | embeds: [new EmbedBuilder().setTitle('Truncate Results:').setDescription(newDescription).setColor(color).setFooter({ 196 | text: `${user.username}` 197 | })], 198 | components: [] 199 | }).catch(console.error); 200 | }, //End of restartMADs() 201 | 202 | 203 | areaQuestsMain: async function areaQuestsMain(channel, user) { 204 | var madDB = config.madDB; 205 | madDB.multipleStatements = true; 206 | var connection = mysql.createConnection(madDB); 207 | let areaListQuery = `SELECT a.name "area", b.name "instance" FROM settings_area a, madmin_instance b WHERE a.mode = "pokestops" AND a.instance_id = b.instance_id;`; 208 | connection.query(areaListQuery, function (err, results) { 209 | if (err) { 210 | console.log(err); 211 | channel.send(`Error selecting areas from database, check console for more info.`); 212 | connection.end(); 213 | return; 214 | } else { 215 | connection.end(); 216 | if (results.length > 0) { 217 | groupAreas(results); 218 | } 219 | } 220 | }); //End of connection 221 | 222 | async function groupAreas(areaResults) { 223 | var instanceAreas = {}; 224 | for (const [key, instance] of Object.entries(dbInfo.instances)) { 225 | var areaList = []; 226 | for (var a in areaResults) { 227 | if (areaResults[a]['instance'] === instance) { 228 | areaList.push(areaResults[a]['area']); 229 | } 230 | } //End of a loop 231 | if (areaList.length > 0) { 232 | areaList.sort(); 233 | instanceAreas[instance] = areaList; 234 | } 235 | } //End of instances 236 | 237 | //Create message for each instance 238 | var selectMenuList = []; 239 | for (const [instance, areaList] of Object.entries(instanceAreas)) { 240 | var listOptions = []; 241 | for (var a = 0; a < areaList.length && a < 25; a++) { 242 | listOptions.push({ 243 | label: areaList[a], 244 | value: `${areaList[a]}` 245 | }); 246 | } //End of a loop 247 | selectMenuList.push(await new ActionRowBuilder().addComponents( 248 | new SelectMenuBuilder() 249 | .setCustomId(`${config.serverName}~truncateArea~${instance}`) 250 | .setPlaceholder(`${instance} areas`) 251 | .setMinValues(1) 252 | .addOptions(listOptions) 253 | )); 254 | } //End of instanceAreas loop 255 | groupInstanceLists(selectMenuList); 256 | } //End of groupAreas() 257 | 258 | async function groupInstanceLists(selectMenuList) { 259 | var messagesNeeded = Math.ceil(selectMenuList.length / 5); 260 | var listNumber = 0; 261 | for (var m = 0; m < messagesNeeded; m++) { 262 | var currentLists = []; 263 | for (var i = 0; i < 5 && listNumber < selectMenuList.length; i++) { 264 | currentLists.push(selectMenuList[listNumber]); 265 | listNumber++; 266 | } //End of i loop 267 | if (m == 0) { 268 | channel.send({ 269 | content: `**Select quest areas to truncate:**`, 270 | components: currentLists 271 | }).catch(console.error); 272 | } else { 273 | channel.send({ 274 | components: currentLists 275 | }).catch(console.error); 276 | } 277 | } //End of m loop 278 | } //End of groupInstanceLists() 279 | }, //End of areaQuestsMain() 280 | 281 | 282 | verifyAreaQuests: async function verifyAreaQuests(channel, user, instanceName, areaList) { 283 | let optionRow = new ActionRowBuilder().addComponents( 284 | new ButtonBuilder().setCustomId(`${config.serverName}~verifyAreaQuests~${instanceName}~yes`).setLabel(`Yes`).setStyle(ButtonStyle.Success), 285 | new ButtonBuilder().setCustomId(`${config.serverName}~verifyAreaQuests~${instanceName}~no`).setLabel(`No`).setStyle(ButtonStyle.Danger) 286 | ); 287 | var title = 'Truncate Quests From Area?'; 288 | if (areaList.length > 1) { 289 | title = 'Truncate Quests From Areas?'; 290 | } 291 | channel.send({ 292 | embeds: [new EmbedBuilder().setTitle(title).setDescription(areaList.join('\n')).setColor('0D00CA')], 293 | components: [optionRow] 294 | }).catch(console.error); 295 | }, //End of verifyAreaQuests 296 | 297 | 298 | collectAreaQuests: async function collectAreaQuests(channel, user, instanceName, areaList) { 299 | console.log(`Quests truncated by ${user.username} in: ${areaList.join(', ')}`); 300 | var queryList = []; 301 | for (var a in areaList) { 302 | queryList.push(`SELECT c.fence_data "geofence" FROM settings_area a, settings_area_pokestops b, settings_geofence c, madmin_instance d WHERE a.area_id = b.area_id AND b.geofence_included = c.geofence_id AND c.instance_id = d.instance_id AND a.name = "${areaList[a]}" and d.name = "${instanceName}";`); 303 | } //End of a loop 304 | var madDB = config.madDB; 305 | madDB.multipleStatements = true; 306 | var connection = mysql.createConnection(madDB); 307 | connection.query(queryList.join(' '), async function (err, results) { 308 | if (err || results.length == 0) { 309 | console.log(err); 310 | channel.send(`Error truncating area quests. Check console for more info.`); 311 | connection.end(); 312 | return; 313 | } else { 314 | connection.end(); 315 | var geoList = []; 316 | if (areaList.length == 1) { 317 | geoList.push(results[0]); 318 | } else { 319 | for (var a = 0; a < areaList.length; a++) { 320 | geoList.push(results[a][0]); 321 | } 322 | } 323 | convertGeofences(geoList); 324 | } 325 | }); //End of connection 326 | 327 | async function convertGeofences(madGeofenceList) { 328 | var geofenceList = []; 329 | for (var m in madGeofenceList) { 330 | let geofenceData = JSON.parse(madGeofenceList[m]['geofence']); 331 | var currentGeofence = []; 332 | for (var g in geofenceData) { 333 | //Has sub geofences 334 | if (geofenceData[g].startsWith('[')) { 335 | if (currentGeofence.length > 0) { 336 | geofenceList.push(currentGeofence); 337 | } 338 | currentGeofence = []; 339 | } 340 | //Has single geofence 341 | else { 342 | let currentPoint = geofenceData[g].split(','); 343 | currentGeofence.push([currentPoint[0] * 1, currentPoint[1] * 1]); 344 | } 345 | } //End of g loop 346 | geofenceList.push(currentGeofence); 347 | } //End of m loop 348 | getPokestops(geofenceList) 349 | } //End of convertGeofences() 350 | 351 | async function getPokestops(geofenceList) { 352 | var madDB = config.madDB; 353 | madDB.multipleStatements = true; 354 | var connection = mysql.createConnection(madDB); 355 | let pokestopQuery = `SELECT a.pokestop_id, a.latitude, a.longitude FROM pokestop a, trs_quest b WHERE a.pokestop_id = b.GUID;`; 356 | connection.query(pokestopQuery, function (err, results) { 357 | if (err) { 358 | console.log(err); 359 | channel.send(`Error selecting quests from database. Check console for more info.`); 360 | connection.end(); 361 | return; 362 | } else { 363 | connection.end(); 364 | createQuestList(geofenceList, results); 365 | } 366 | }); //End of connection 367 | } //End of getPokestops() 368 | 369 | async function createQuestList(geofenceList, pokestopList) { 370 | var guidList = []; 371 | for (var g in geofenceList) { 372 | for (var p in pokestopList) { 373 | let isInside = insidePolygon([pokestopList[p]['latitude'], pokestopList[p]['longitude']], geofenceList[g]); 374 | if (isInside === true) { 375 | guidList.push(pokestopList[p]['pokestop_id']); 376 | } 377 | } //End of p loop 378 | } //End of g loop 379 | guidList = [...new Set(guidList)]; 380 | module.exports.truncateAreaQuests(channel, guidList); 381 | } //End of createQuestList() 382 | }, //End of collectAreaQuests() 383 | 384 | 385 | truncateAreaQuests: async function truncateAreaQuests(channel, guidList) { 386 | let deleteList = `"${guidList.join(`","`)}"`; 387 | var madDB = config.madDB; 388 | madDB.multipleStatements = true; 389 | var connection = mysql.createConnection(madDB); 390 | let pokestopQuery = `DELETE FROM trs_quest WHERE GUID IN (${deleteList});`; 391 | connection.query(pokestopQuery, function (err, results) { 392 | if (err) { 393 | console.log(err); 394 | channel.send(`Error selecting quests from database. Check console for more info.`); 395 | connection.end(); 396 | return; 397 | } else { 398 | connection.end(); 399 | channel.send(`${results.affectedRows} quests deleted from database.`); 400 | } 401 | }); //End of connection 402 | } //End of truncateAreaQuests() 403 | } -------------------------------------------------------------------------------- /functions/devices.js: -------------------------------------------------------------------------------- 1 | const { 2 | Client, 3 | GatewayIntentBits, 4 | Partials, 5 | Collection, 6 | Permissions, 7 | ActionRowBuilder, 8 | SelectMenuBuilder, 9 | MessageButton, 10 | EmbedBuilder, 11 | ButtonBuilder, 12 | ButtonStyle, 13 | InteractionType, 14 | ChannelType 15 | } = require('discord.js'); 16 | const fs = require('fs'); 17 | const mysql = require('mysql'); 18 | const moment = require('moment'); 19 | const config = require('../config/config.json'); 20 | const noProtoJson = require('../config/noProto.json'); 21 | 22 | module.exports = { 23 | deviceStatus: async function deviceStatus(channel, user) { 24 | let dbInfo = require('../MAD_Database_Info.json'); 25 | console.log(`${user.username} requested the status of all devices`); 26 | let connectionMAD = mysql.createConnection(config.madDB); 27 | let statusQuery = `SELECT * FROM trs_status`; 28 | var offsetMinutes = 0; 29 | if (config.madDB.timezoneDifference) { 30 | offsetMinutes = config.madDB.timezoneDifference * 60; 31 | } 32 | connectionMAD.query(statusQuery, function (err, results) { 33 | if (err) { 34 | console.log("Status Query Error:", err); 35 | } else { 36 | var instanceList = []; 37 | var sortBy = require('sort-by'), 38 | buttonArray = []; 39 | results.filter(areaTest => areaTest.area_id).forEach(device => { 40 | instanceList.push(dbInfo.instances[device.instance_id]); 41 | let minutesSinceSeen = (((Math.abs(Date.now() - Date.parse(device.lastProtoDateTime)) / (1000 * 3600)) * 60) + offsetMinutes).toFixed(0); 42 | var deviceName = dbInfo.devices[device.device_id]['name']; 43 | for (var b = 0; b < config.devices.buttonLabelRemove.length; b++) { 44 | let remove = config.devices.buttonLabelRemove[b]; 45 | if (deviceName.includes(remove)) { 46 | deviceName = deviceName.replace(remove, ''); 47 | break; 48 | } 49 | } //End of b loop 50 | var buttonLabel = deviceName; 51 | var buttonStyle = ButtonStyle.Success; 52 | //If idle 53 | if (device.idle === 1) { 54 | buttonStyle = ButtonStyle.Primary; 55 | //If paused 56 | if (dbInfo.areas[device.area_id]['mode'] !== 'idle') { 57 | buttonStyle = ButtonStyle.Secondary; 58 | buttonLabel = `${deviceName} (${minutesSinceSeen}m)`; 59 | } 60 | } else if (minutesSinceSeen > config.devices.noProtoMinutes) { 61 | buttonStyle = ButtonStyle.Danger; 62 | } 63 | if (minutesSinceSeen > config.devices.noProtoMinutes) { 64 | buttonLabel = `${deviceName} (${minutesSinceSeen}m)`; 65 | if (Math.round(minutesSinceSeen) > 119) { 66 | let hoursSince = (Math.round(minutesSinceSeen) / 60).toFixed(0); 67 | buttonLabel = `${deviceName} (${hoursSince}h)`; 68 | if (hoursSince > 47) { 69 | let daysSince = (Math.round(hoursSince / 24)).toFixed(0); 70 | buttonLabel = `${deviceName} (${daysSince}d)`; 71 | } 72 | } 73 | } 74 | let buttonID = `${config.serverName}~deviceInfo~${device.device_id}`; 75 | let button = new ButtonBuilder().setCustomId(buttonID).setLabel(buttonLabel).setStyle(buttonStyle); 76 | let buttonObj = { 77 | name: deviceName, 78 | instance: dbInfo.instances[device.instance_id], 79 | button: button 80 | } 81 | buttonArray.push(buttonObj); 82 | }); //End of forEach 83 | instanceList = Array.from(new Set(instanceList)); 84 | buttonArray.sort(sortBy('name')); 85 | //Split by instance 86 | instanceList.forEach(instance => { 87 | var content = `**Status of ${instance} Devices:**`; 88 | var instanceButtons = []; 89 | buttonArray.forEach(buttonObj => { 90 | if (instance === buttonObj.instance) { 91 | instanceButtons.push(buttonObj.button); 92 | } 93 | }) //End of forEach(buttonObj) 94 | let messagesNeeded = Math.ceil(instanceButtons.length / 25); 95 | for (var m = 0; m < messagesNeeded; m++) { 96 | let buttonsNeeded = Math.min(25, instanceButtons.length); 97 | let rowsNeeded = Math.ceil(buttonsNeeded / 5); 98 | var buttonCount = 0; 99 | var messageComponents = []; 100 | for (var n = 0; n < rowsNeeded && n < 5; n++) { 101 | var buttonRow = new ActionRowBuilder(); 102 | for (var r = 0; r < 5; r++) { 103 | if (buttonCount < buttonsNeeded) { 104 | buttonRow.addComponents(instanceButtons[buttonCount]); 105 | buttonCount++; 106 | } 107 | } //End of r loop 108 | messageComponents.push(buttonRow); 109 | } //End of n loop 110 | channel.send({ 111 | content: content, 112 | components: messageComponents 113 | }).catch(console.error) 114 | .then(msg => { 115 | if (config.devices.statusButtonsDeleteMinutes > 0) { 116 | setTimeout(() => msg.delete().catch(err => console.log(`Error deleting device status message:`, err)), (config.devices.statusButtonsDeleteMinutes * 1000 * 60)); 117 | } 118 | }) 119 | content = '‎'; 120 | let tempButtons = instanceButtons.slice(25); 121 | instanceButtons = tempButtons; 122 | } //End of message m loop 123 | }) //End of forEach(instance) 124 | } 125 | }); //End of query 126 | connectionMAD.end(); 127 | }, //End of deviceStatus() 128 | 129 | noProtoDevices: async function noProtoDevices(client, channel, user, type) { 130 | if (type === 'cron' && !config.devices.noProtoChannelID && config.devices.useNoProtoJson !== true) { 131 | console.log("Error: 'noProtoChannelID' not set in config.json"); 132 | return; 133 | } 134 | let dbInfo = require('../MAD_Database_Info.json'); 135 | if (type === 'search') { 136 | console.log(`${user.username} requested the status of all noProto devices`); 137 | } 138 | let connectionMAD = mysql.createConnection(config.madDB); 139 | let statusQuery = `SELECT * FROM trs_status`; 140 | var offsetMinutes = 0; 141 | if (config.madDB.timezoneDifference) { 142 | offsetMinutes = config.madDB.timezoneDifference * 60; 143 | } 144 | connectionMAD.query(statusQuery, function (err, results) { 145 | if (err) { 146 | console.log("noProto Status Query Error:", err); 147 | } else { 148 | var instanceList = []; 149 | var sortBy = require('sort-by'), 150 | buttonArray = []; 151 | results.forEach(device => { 152 | let minutesSinceSeen = (((Math.abs(Date.now() - Date.parse(device.lastProtoDateTime)) / (1000 * 3600)) * 60) + offsetMinutes).toFixed(0); 153 | var deviceName = dbInfo.devices[device.device_id]['name']; 154 | for (var b = 0; b < config.devices.buttonLabelRemove.length; b++) { 155 | let remove = config.devices.buttonLabelRemove[b]; 156 | if (deviceName.includes(remove)) { 157 | deviceName = deviceName.replace(remove, ''); 158 | break; 159 | } 160 | } //End of b loop 161 | if (minutesSinceSeen > config.devices.noProtoMinutes) { 162 | instanceList.push(dbInfo.instances[device.instance_id]); 163 | var buttonStyle = ButtonStyle.Danger; 164 | //If idle 165 | if (device.idle === 1) { 166 | buttonStyle = ButtonStyle.Primary; 167 | //If paused 168 | if (dbInfo.areas[device.area_id]['mode'] !== 'idle') { 169 | buttonStyle = ButtonStyle.Secondary; 170 | } 171 | } 172 | var buttonLabel = `${deviceName} (${minutesSinceSeen}m)`; 173 | if (Math.round(minutesSinceSeen) > 119) { 174 | let hoursSince = (Math.round(minutesSinceSeen) / 60).toFixed(0); 175 | buttonLabel = `${deviceName} (${hoursSince}h)`; 176 | if (hoursSince > 47) { 177 | let daysSince = (Math.round(hoursSince / 24)).toFixed(0); 178 | buttonLabel = `${deviceName} (${daysSince}d)`; 179 | } 180 | } 181 | var buttonID = `${config.serverName}~deviceInfo~${device.device_id}`; 182 | let button = new ButtonBuilder().setCustomId(buttonID).setLabel(buttonLabel).setStyle(buttonStyle); 183 | let buttonObj = { 184 | name: deviceName, 185 | instance: dbInfo.instances[device.instance_id], 186 | button: button 187 | } 188 | if (type === "search") { 189 | buttonArray.push(buttonObj); 190 | } else { 191 | if (device.idle != 1) { 192 | buttonArray.push(buttonObj); 193 | } else if (config.devices.noProtoIncludeIdle === true) { 194 | buttonArray.push(buttonObj); 195 | } 196 | } 197 | } //End of noProto breach 198 | }); //End of forEach(device) 199 | instanceList = Array.from(new Set(instanceList)); 200 | buttonArray.sort(sortBy('name')); 201 | if (config.devices.useNoProtoJson === true && type === 'cron') { 202 | splitByNoProtoJson(buttonArray); 203 | } else { 204 | splitByInstance(instanceList, buttonArray); 205 | } 206 | var completedDevices = []; 207 | async function splitByNoProtoJson(buttonArray) { 208 | noProtoJson.forEach(async item => { 209 | if (item.channelID) { 210 | try { 211 | let noProtoChannel = await client.channels.fetch(item.channelID); 212 | var channelButtons = []; 213 | for (var b = 0; b < buttonArray.length; b++) { 214 | for (var n = 0; n < item.deviceNames.length; n++) { 215 | for (var r = 0; r < config.devices.buttonLabelRemove.length; r++) { 216 | if (item.deviceNames[n].replace(config.devices.buttonLabelRemove[r], '') === buttonArray[b]['name']) { 217 | channelButtons.push(buttonArray[b]['button']); 218 | completedDevices.push(buttonArray[b]['name']); 219 | } 220 | } //End of r loop 221 | } //End of n loop 222 | } 223 | if (channelButtons.length > 0) { 224 | createComponents(noProtoChannel, channelButtons); 225 | } 226 | } catch (err) { 227 | console.log("Failed to fetch noProto channel:", err); 228 | } 229 | } 230 | }); 231 | if (config.devices.noProtoChannelID) { 232 | try { 233 | let noProtoChannel = await client.channels.fetch(config.devices.noProtoChannelID); 234 | var channelButtons = []; 235 | buttonArray.forEach(button => { 236 | if (completedDevices.includes(button['name']) || config.devices.noProtoIgnoreDevices.includes(button['name'])) { 237 | //Skip button 238 | } else { 239 | channelButtons.push(button['button']); 240 | } 241 | }); 242 | if (channelButtons.length > 0) { 243 | createComponents(noProtoChannel, channelButtons); 244 | } 245 | } catch (err) { 246 | console.log("Failed to fetch default noProto channel:", err); 247 | } 248 | } 249 | async function createComponents(noProtoChannel, channelButtons) { 250 | let messagesNeeded = Math.ceil(channelButtons.length / 25); 251 | for (var m = 0; m < messagesNeeded; m++) { 252 | let buttonsNeeded = Math.min(25, channelButtons.length); 253 | let rowsNeeded = Math.ceil(buttonsNeeded / 5); 254 | var buttonCount = 0; 255 | var messageComponents = []; 256 | for (var n = 0; n < rowsNeeded && n < 5; n++) { 257 | var buttonRow = new ActionRowBuilder(); 258 | for (var r = 0; r < 5; r++) { 259 | if (buttonCount < buttonsNeeded) { 260 | buttonRow.addComponents(channelButtons[buttonCount]); 261 | buttonCount++; 262 | } 263 | } //End of r loop 264 | messageComponents.push(buttonRow); 265 | } //End of n loop 266 | let content = `**${channelButtons.length} No Proto Devices:**`; 267 | sendCronMessage(noProtoChannel, content, messageComponents); 268 | } //End of m loop 269 | } //End of createComponents() 270 | } //End of splitByNoProtoJson() 271 | 272 | async function splitByInstance(instanceList, buttonArray) { 273 | instanceList.forEach(async instance => { 274 | var instanceButtons = []; 275 | var noProtoCount = 0; 276 | buttonArray.forEach(buttonObj => { 277 | if (instance === buttonObj.instance && !config.devices.noProtoIgnoreDevices.includes(buttonObj['name'])) { 278 | instanceButtons.push(buttonObj.button); 279 | noProtoCount++; 280 | } 281 | }) //End of forEach(buttonObj) 282 | let messagesNeeded = Math.ceil(instanceButtons.length / 25); 283 | for (var m = 0; m < messagesNeeded; m++) { 284 | let buttonsNeeded = Math.min(25, instanceButtons.length); 285 | let rowsNeeded = Math.ceil(buttonsNeeded / 5); 286 | var buttonCount = 0; 287 | var messageComponents = []; 288 | for (var n = 0; n < rowsNeeded && n < 5; n++) { 289 | var buttonRow = new ActionRowBuilder(); 290 | for (var r = 0; r < 5; r++) { 291 | if (buttonCount < buttonsNeeded) { 292 | buttonRow.addComponents(instanceButtons[buttonCount]); 293 | buttonCount++; 294 | } 295 | } //End of r loop 296 | messageComponents.push(buttonRow); 297 | } //End of n loop 298 | let content = `**${instance}: ${noProtoCount} No Proto Devices:**`; 299 | if (type === 'search') { 300 | channel.send({ 301 | content: content, 302 | components: messageComponents 303 | }).catch(console.error) 304 | .then(msg => { 305 | if (config.devices.statusButtonsDeleteMinutes > 0) { 306 | setTimeout(() => msg.delete().catch(err => console.log(`Error deleting noProto status message:`, err)), (config.devices.statusButtonsDeleteMinutes * 1000 * 60)); 307 | } 308 | }) 309 | } //End of search 310 | else if (type === 'cron') { 311 | try { 312 | let postChannel = await client.channels.fetch(config.devices.noProtoChannelID); 313 | sendCronMessage(postChannel, content, messageComponents); 314 | } catch (err) { 315 | console.log("Failed to fetch noProto post channel:", err); 316 | } 317 | } //End of cron 318 | content = '‎'; 319 | let tempButtons = instanceButtons.slice(25); 320 | instanceButtons = tempButtons; 321 | } //End of message m loop 322 | }); //End of forEach(instance) 323 | } //End of splitByInstance() 324 | 325 | async function sendCronMessage(postChannel, content, messageComponents) { 326 | postChannel.send({ 327 | content: content, 328 | components: messageComponents 329 | }).catch(console.error) 330 | .then(msg => { 331 | if (config.devices.checkDeleteMinutes > 0) { 332 | setTimeout(() => msg.delete().catch(err => console.log(`Error deleting noProto check message:`, err)), (config.devices.checkDeleteMinutes * 333 | 1000 * 60)); 334 | } 335 | }) 336 | } //End of sendCronMessage() 337 | 338 | if (instanceList.length == 0 && type == "search") { 339 | channel.send("No problems detected!") 340 | .catch(console.error) 341 | .then(msg => { 342 | if (config.devices.statusButtonsDeleteMinutes > 0) { 343 | setTimeout(() => msg.delete().catch(err => console.log(`Error deleting noProto status message:`, err)), (config.devices.statusButtonsDeleteMinutes * 1000 * 60)); 344 | } 345 | }) 346 | } 347 | } 348 | }); //End of query 349 | 350 | connectionMAD.end(); 351 | }, //End of noProtoDevices() 352 | 353 | getDeviceInfo: async function getDeviceInfo(channel, user, deviceID) { 354 | let dbInfo = require('../MAD_Database_Info.json'); 355 | let connectionMAD = mysql.createConnection(config.madDB); 356 | let deviceQuery = `SELECT * FROM trs_status WHERE device_id = "${deviceID}"`; 357 | connectionMAD.query(deviceQuery, function (err, deviceResults) { 358 | if (err) { 359 | console.log("Device Info Query Error:", err); 360 | } else { 361 | parseDeviceInfo(deviceResults[0]); 362 | } 363 | }); //End of query 364 | connectionMAD.end(); 365 | async function parseDeviceInfo(device) { 366 | var offsetMinutes = 0; 367 | if (config.madDB.timezoneDifference) { 368 | offsetMinutes = config.madDB.timezoneDifference * 60; 369 | } 370 | let minutesSinceSeen = (((Math.abs(Date.now() - Date.parse(device.lastProtoDateTime)) / (1000 * 3600)) * 60) + offsetMinutes).toFixed(0); 371 | var paused = ''; 372 | let origin = dbInfo.devices[device.device_id]['name']; 373 | var deviceInfoArray = []; 374 | //Running well 375 | var color = '00841E'; 376 | //If idle 377 | if (device.idle == 1) { 378 | color = '5865F2'; 379 | //If paused 380 | if (dbInfo.areas[device.area_id]['mode'] !== 'idle') { 381 | color = '696969'; 382 | paused = '**- Paused:** true\n'; 383 | } 384 | } else if (minutesSinceSeen > config.devices.noProtoMinutes) { 385 | color = '9E0000'; 386 | } 387 | var offsetHours = 0; 388 | if (config.madDB.timezoneDifference) { 389 | offsetHours = config.madDB.timezoneDifference; 390 | } 391 | deviceInfoArray.push(`**area:** ${dbInfo.areas[device.area_id]['name']} (${dbInfo.areas[device.area_id]['mode']})`); 392 | deviceInfoArray.push(`**last seen:** ${moment(device.lastProtoDateTime).add(offsetHours, 'hours').from(moment())}`); 393 | if (config.devices.displayOptions.restartInfo === true) { 394 | deviceInfoArray.push(`**last restart:** ${moment(device.lastPogoRestart).add(offsetHours, 'hours').from(moment())} (#${device.globalrestartcount})`); 395 | } 396 | if (config.devices.displayOptions.rebootInfo === true) { 397 | deviceInfoArray.push(`**last reboot:** ${moment(device.lastPogoReboot).add(offsetHours, 'hours').from(moment())} (#${device.globalrebootcount})`); 398 | } 399 | let cycleStatPosition = deviceInfoArray.length; 400 | if (config.devices.displayOptions.deviceID === true) { 401 | deviceInfoArray.push(`**deviceID:** ${device.device_id}`); 402 | } 403 | if (config.devices.displayOptions.instance === true) { 404 | deviceInfoArray.push(`**instance:** ${dbInfo.instances[device.instance_id]}`); 405 | } 406 | if (config.devices.displayOptions.loginInfo === true) { 407 | deviceInfoArray.push(`**login type:** ${dbInfo.devices[device.device_id]['loginType']}\n- **login account:** ${dbInfo.devices[device.device_id]['loginAccount']}`); 408 | } 409 | if (!config.stats.database.host) { 410 | sendDeviceInfo(origin, color, deviceInfoArray, ''); 411 | } else { 412 | let connectionDeviceInfo = mysql.createConnection(config.stats.database); 413 | let basicStatsQuery = `SELECT * FROM ATVgeneral WHERE origin = "${origin}" AND arch != "null" ORDER BY datetime DESC`; 414 | connectionDeviceInfo.query(basicStatsQuery, function (err, statsResults) { 415 | if (err) { 416 | console.log("Stats Info Query Error:", err); 417 | } else { 418 | getStatsDeviceInfo(origin, color, deviceInfoArray, statsResults[0], cycleStatPosition); 419 | } 420 | }); //End of query 421 | connectionDeviceInfo.end(); 422 | } 423 | } //End of parseDeviceInfo() 424 | 425 | async function getStatsDeviceInfo(origin, color, deviceInfoArray, statsDevice, cycleStatPosition) { 426 | try { 427 | for (const [key, value] of Object.entries(statsDevice)) { 428 | if (config.stats.deviceInfo[key] === true) { 429 | deviceInfoArray.push(`**${key}:** ${value}`); 430 | } 431 | } 432 | } catch (err) {} 433 | if (config.stats.deviceInfo.cycle === true && config.deviceControl.powerCycleType.toLowerCase() === "devicecontrol") { 434 | let connectionCycleInfo = mysql.createConnection(config.stats.database); 435 | let cycleQuery = `SELECT * FROM relay WHERE origin = "${origin}"`; 436 | connectionCycleInfo.query(cycleQuery, function (err, cycleResults) { 437 | if (err || cycleResults.length == 0) { 438 | console.log("Cycle Stat Query Error:", err); 439 | createStatsList(origin, color, deviceInfoArray); 440 | } else { 441 | deviceInfoArray.splice(cycleStatPosition, 0, `**last cycle:** ${moment(cycleResults[0].lastCycle).from(moment())} (#${cycleResults[0].totCycle})`); 442 | createStatsList(origin, color, deviceInfoArray); 443 | } 444 | }); //End of query 445 | connectionCycleInfo.end(); 446 | } else { 447 | createStatsList(origin, color, deviceInfoArray); 448 | } 449 | } //End of getStatsDeviceInfo() 450 | 451 | async function createStatsList(origin, color, deviceInfoArray) { 452 | let statsSelectListHourly = [{ 453 | label: `Locations Handled (hourly)`, 454 | value: `${config.serverName}~deviceStats~${origin}~locationsHandled~hourly` 455 | }, 456 | { 457 | label: `Locations Success (hourly)`, 458 | value: `${config.serverName}~deviceStats~${origin}~locationsSuccess~hourly` 459 | }, 460 | { 461 | label: `Locations Time (hourly)`, 462 | value: `${config.serverName}~deviceStats~${origin}~locationsTime~hourly` 463 | }, 464 | { 465 | label: `Mons Scanned (hourly)`, 466 | value: `${config.serverName}~deviceStats~${origin}~monsScanned~hourly` 467 | }, 468 | { 469 | label: `Proto Success Rate (hourly)`, 470 | value: `${config.serverName}~deviceStats~${origin}~protoSuccess~hourly` 471 | }, 472 | { 473 | label: `Restarts/Reboots (hourly)`, 474 | value: `${config.serverName}~deviceStats~${origin}~restartReboot~hourly` 475 | }, 476 | ] 477 | let statsSelectListDaily = [{ 478 | label: `Locations Handled (daily)`, 479 | value: `${config.serverName}~deviceStats~${origin}~locationsHandled~daily` 480 | }, 481 | { 482 | label: `Location Success (daily)`, 483 | value: `${config.serverName}~deviceStats~${origin}~locationsSuccess~daily` 484 | }, 485 | { 486 | label: `Locations Time (daily)`, 487 | value: `${config.serverName}~deviceStats~${origin}~locationsTime~daily` 488 | }, 489 | { 490 | label: `Mons Scanned (daily)`, 491 | value: `${config.serverName}~deviceStats~${origin}~monsScanned~daily` 492 | }, 493 | { 494 | label: `Proto Success Rate (daily)`, 495 | value: `${config.serverName}~deviceStats~${origin}~protoSuccess~daily` 496 | }, 497 | { 498 | label: `Restarts/Reboots (daily)`, 499 | value: `${config.serverName}~deviceStats~${origin}~restartReboot~daily` 500 | }, 501 | { 502 | label: `Temperature`, 503 | value: `${config.serverName}~deviceStats~${origin}~temperature~daily` 504 | }, 505 | ]; 506 | let statsComponentHourly = new ActionRowBuilder() 507 | .addComponents( 508 | new SelectMenuBuilder() 509 | .setCustomId(`${config.serverName}~deviceStats~hourly`) 510 | .setPlaceholder(`${origin} Hourly Stats`) 511 | .addOptions(statsSelectListHourly) 512 | ); 513 | let statsComponentDaily = new ActionRowBuilder() 514 | .addComponents( 515 | new SelectMenuBuilder() 516 | .setCustomId(`${config.serverName}~deviceStats~daily`) 517 | .setPlaceholder(`${origin} Daily Stats`) 518 | .addOptions(statsSelectListDaily) 519 | ) 520 | let statsListArray = [statsComponentHourly, statsComponentDaily]; 521 | sendDeviceInfo(origin, color, deviceInfoArray, statsListArray); 522 | } //End of createStatsList() 523 | 524 | async function sendDeviceInfo(origin, color, deviceInfoArray, statsListArray) { 525 | var deviceComponents = []; 526 | if (config.deviceControl.path) { 527 | let controlSelectList = [{ 528 | label: `Pause ${origin}`, 529 | value: `${config.serverName}~deviceControl~${origin}~pauseDevice` 530 | }, 531 | { 532 | label: `Unpause ${origin}`, 533 | value: `${config.serverName}~deviceControl~${origin}~unpauseDevice` 534 | }, 535 | { 536 | label: `Start PoGo on ${origin}`, 537 | value: `${config.serverName}~deviceControl~${origin}~startPogo` 538 | }, 539 | { 540 | label: `Quit PoGo on ${origin}`, 541 | value: `${config.serverName}~deviceControl~${origin}~quitPogo` 542 | }, 543 | { 544 | label: `Reboot ${origin}`, 545 | value: `${config.serverName}~deviceControl~${origin}~rebootDevice` 546 | }, 547 | { 548 | label: `Logcat for ${origin}`, 549 | value: `${config.serverName}~deviceControl~${origin}~logcatDevice` 550 | }, 551 | { 552 | label: `Clear game data on ${origin}`, 553 | value: `${config.serverName}~deviceControl~${origin}~clearGame` 554 | }, 555 | { 556 | label: `Screenshot of ${origin}`, 557 | value: `${config.serverName}~deviceControl~${origin}~screenshot` 558 | } 559 | ] 560 | if (config.deviceControl.powerCycleType.toLowerCase().replace(' ', '') === 'devicecontrol') { 561 | controlSelectList.push({ 562 | label: `Power cycle ${origin}`, 563 | value: `${config.serverName}~deviceControl~${origin}~cycle` 564 | }) 565 | } else if (config.deviceControl.powerCycleType.toLowerCase().replace(' ', '').replace('raspberryrelay', 'raspberry') === 'raspberry') { 566 | controlSelectList.push({ 567 | label: `Power cycle ${origin}`, 568 | value: `raspberryRelay~${origin}` 569 | }) 570 | } 571 | let controlListComponent = new ActionRowBuilder() 572 | .addComponents( 573 | new SelectMenuBuilder() 574 | .setCustomId(`${config.serverName}~deviceControl`) 575 | .setPlaceholder(`${origin} DeviceControl`) 576 | .addOptions(controlSelectList), 577 | ); 578 | deviceComponents.push(controlListComponent); 579 | } //End of deviceControl 580 | if (statsListArray !== '') { 581 | deviceComponents.push(statsListArray[0], statsListArray[1]); 582 | } 583 | console.log(`${user.username} looked for ${origin} device info.`); 584 | channel.send({ 585 | embeds: [new EmbedBuilder().setTitle(`${origin} Info:`).setDescription(`- ${deviceInfoArray.join('\n- ')}`).setColor(color).setFooter({ 586 | text: `${user.username}` 587 | })], 588 | components: deviceComponents 589 | }).catch(console.error) 590 | .then(msg => { 591 | if (config.devices.infoMessageDeleteSeconds > 0) { 592 | setTimeout(() => msg.delete().catch(err => console.log(`Error deleting ${origin} device message:`, err)), (config.devices.infoMessageDeleteSeconds * 1000)); 593 | } 594 | }); 595 | } //End of sendDeviceInfo() 596 | }, //End of getDeviceInfo() 597 | } --------------------------------------------------------------------------------