From 51a6f2e566dc29c66c5a57fdc90b1567c269d003 Mon Sep 17 00:00:00 2001 From: toast-ts <96593068+toast-ts@users.noreply.github.com> Date: Wed, 30 Aug 2023 18:34:59 +1000 Subject: [PATCH] Move essentials out of `client.ts` --- src/client.ts | 119 +++------------------------------- src/commands/ban.ts | 3 +- src/commands/case.ts | 7 +- src/commands/dev.ts | 18 ++--- src/commands/kick.ts | 3 +- src/commands/mp.ts | 9 +-- src/commands/mute.ts | 3 +- src/commands/ping.ts | 3 +- src/commands/softban.ts | 3 +- src/commands/statistics.ts | 12 ++-- src/commands/warn.ts | 3 +- src/funcs/MPLoop.ts | 15 ++--- src/funcs/Punish.ts | 20 ++++++ src/funcs/YTLoop.ts | 18 +++++ src/helpers/FormatBytes.ts | 5 ++ src/helpers/FormatPlayer.ts | 22 +++++++ src/helpers/FormatTime.ts | 35 ++++++++++ src/helpers/UsernameHelper.ts | 18 +++++ src/index.ts | 13 ++-- src/models/punishments.ts | 5 +- src/typings/interfaces.d.ts | 4 -- 21 files changed, 176 insertions(+), 162 deletions(-) create mode 100644 src/funcs/Punish.ts create mode 100644 src/funcs/YTLoop.ts create mode 100644 src/helpers/FormatBytes.ts create mode 100644 src/helpers/FormatPlayer.ts create mode 100644 src/helpers/FormatTime.ts create mode 100644 src/helpers/UsernameHelper.ts diff --git a/src/client.ts b/src/client.ts index 56bb430..381ebd7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,6 +1,6 @@ -import Discord, {Client, GatewayIntentBits, Partials} from 'discord.js'; +import Discord from 'discord.js'; import {readFileSync, readdirSync} from 'node:fs'; -import {formatTimeOpt, Tokens, Config, repeatedMessages, type MPServerCache} from './typings/interfaces'; +import {Tokens, Config, repeatedMessages, type MPServerCache} from './typings/interfaces'; import bannedWords from './models/bannedWords.js'; import userLevels from './models/userLevels.js'; import suggestion from './models/suggestion.js'; @@ -23,7 +23,7 @@ try{ console.log('Using production config') } -export default class TClient extends Client { +export default class TClient extends Discord.Client { invites: Map; commands: Discord.Collection; registry: Array; @@ -36,7 +36,6 @@ export default class TClient extends Client { attachmentBuilder: any; moment: typeof moment; xjs: typeof xjs; - ms: any; userLevels: userLevels; punishments: punishments; bonkCount: bonkCount; @@ -51,13 +50,13 @@ export default class TClient extends Client { constructor(){ super({ intents: [ - GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildModeration, GatewayIntentBits.GuildInvites, - GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildPresences, - GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages, - GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.DirectMessages + Discord.GatewayIntentBits.Guilds, Discord.GatewayIntentBits.GuildMembers, + Discord.GatewayIntentBits.GuildModeration, Discord.GatewayIntentBits.GuildInvites, + Discord.GatewayIntentBits.GuildMessageReactions, Discord.GatewayIntentBits.GuildPresences, + Discord.GatewayIntentBits.MessageContent, Discord.GatewayIntentBits.GuildMessages, + Discord.GatewayIntentBits.GuildVoiceStates, Discord.GatewayIntentBits.DirectMessages ], partials: [ - Partials.Channel, Partials.Reaction, Partials.Message + Discord.Partials.Channel, Discord.Partials.Reaction, Discord.Partials.Message ], allowedMentions: {users:[],roles:[]} }) this.invites = new Map(); @@ -75,7 +74,6 @@ export default class TClient extends Client { this.attachmentBuilder = Discord.AttachmentBuilder; this.moment = moment; this.xjs = xjs; - this.ms = import('ms').then(i=>i); this.userLevels = new userLevels(this); this.bonkCount = new bonkCount(this); this.punishments = new punishments(this); @@ -109,106 +107,7 @@ export default class TClient extends Client { } } } - formatTime(integer: number, accuracy = 1, options?: formatTimeOpt){ - let achievedAccuracy = 0; - let text:any = ''; - for (const timeName of [ - {name: 'year', length: 31536000000}, - {name: 'month', length: 2592000000}, - {name: 'week', length: 604800000}, - {name: 'day', length: 86400000}, - {name: 'hour', length: 3600000}, - {name: 'minute', length: 60000}, - {name: 'second', length: 1000} - ]){ - if (achievedAccuracy < accuracy){ - const fullTimelengths = Math.floor(integer/timeName.length); - if (fullTimelengths === 0) continue; - achievedAccuracy++; - text += fullTimelengths + (options?.longNames ? (' '+timeName.name+(fullTimelengths === 1 ? '' : 's')) : timeName.name.slice(0, timeName.name === 'month' ? 2 : 1)) + (options?.commas ? ', ' : ' '); - integer -= fullTimelengths*timeName.length; - } else break; - } - if (text.length == 0) text = integer + (options?.longNames ? ' milliseconds' : 'ms') + (options?.commas ? ', ' : ''); - if (options?.commas){ - text = text.slice(0, -2); - if (options?.longNames){ - text = text.split(''); - text[text.lastIndexOf(',')] = ' and'; - text = text.join(''); - } - } return text.trim(); - } - formatPlayerUptime(oldTime:number){ - var Hours=0; - oldTime=Math.floor(Number(oldTime)); - if(oldTime>=60){ - var Hours=Math.floor(Number(oldTime)/60); - var Minutes=(Number(oldTime)-(Hours*60)); - } else Minutes=Number(oldTime) - if(Hours>=24){ - var Days=Math.floor(Number(Hours)/24); - var Hours=(Hours-(Days*24)); - } return (Days>0?Days+' d ':'')+(Hours>0?Hours+' h ':'')+(Minutes>0?Minutes+' m':'') - } isStaff = (guildMember:Discord.GuildMember)=>this.config.mainServer.staffRoles.map((x: string)=>this.config.mainServer.roles[x]).some((x: string)=>guildMember.roles.cache.has(x)); - youNeedRole = (interaction:Discord.CommandInteraction, role:string)=>interaction.reply(`This command is restricted to <@&${this.config.mainServer.roles[role]}>`); - logTime = ()=>`[${this.moment().format('DD/MM/YY HH:mm:ss')}]`; - - async punish(interaction: Discord.ChatInputCommandInteraction<'cached'>, type: string){ - if (!this.isStaff(interaction.member as Discord.GuildMember)) return this.youNeedRole(interaction, "dcmod"); - - const time = interaction.options.getString('time') ?? undefined; - const reason = interaction.options.getString('reason') ?? 'Reason unspecified'; - const GuildMember = interaction.options.getMember('member') ?? undefined; - const User = interaction.options.getUser('member', true); - - console.log(this.logTime(), `[PunishmentLog] ${GuildMember?.user?.username ?? User?.username ?? 'No user data'} ${time ? ['warn', 'kick'].includes(this.punishments.type) ? 'and no duration set' : `and ${time} (duration)` : ''} was used in /${interaction.commandName} for ${reason}`); - (this.channels.cache.get(this.config.mainServer.channels.punishment_log) as Discord.TextChannel).send({embeds:[new this.embed().setColor(this.config.embedColor).setAuthor({name: interaction?.user?.username, iconURL: interaction?.user?.displayAvatarURL({size:2048})}).setTitle('Punishment Log').setDescription(`${GuildMember?.user?.username ?? User?.username ?? 'No user data'} ${time ? ['warn', 'kick'].includes(this.punishments.type) ? 'and no duration set' : `and ${time} (duration)` : ''} was used in \`/${interaction.commandName}\` for \`${reason}\``).setTimestamp()]}); - if (interaction.user.id === User.id) return interaction.reply(`You cannot ${type} yourself.`); - if (!GuildMember && type != 'unban') return interaction.reply(`You cannot ${type} someone who is not in the server.`); - if (User.bot) return interaction.reply(`You cannot ${type} a bot!`); - - await interaction.deferReply(); - await this.punishments.addPunishment(type, { time, interaction }, interaction.user.id, reason, User, GuildMember); - } - async YTLoop(YTChannelID: string, YTChannelName: string, DCChannelID: string){ - let Data:any; - - try { - await fetch(`https://www.youtube.com/feeds/videos.xml?channel_id=${YTChannelID}`, {signal: AbortSignal.timeout(6000),headers:{'User-Agent':`Daggerbot - Notification/undici`}}).then(async xml=>Data = this.xjs.xml2js(await xml.text(), {compact: true})) - } catch(err){ - console.log(this.logTime(), `${YTChannelName} YT fail`) - } - - if (!Data) return; - if (!this.YTCache[YTChannelID]) return this.YTCache[YTChannelID] = Data.feed.entry[0]['yt:videoId']._text; - if (Data.feed.entry[1]['yt:videoId']._text === this.YTCache[YTChannelID]){ - this.YTCache[YTChannelID] = Data.feed.entry[0]['yt:videoId']._text; - (this.channels.resolve(DCChannelID) as Discord.TextChannel).send(`**${YTChannelName}** just uploaded a video!\n${Data.feed.entry[0].link._attributes.href}`) - } - } - formatBytes(bytes:number, decimals:number = 2) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals < 0 ? 0 : decimals)) + ' ' + ['Bytes', 'KB', 'MB', 'GB'][i]; - } - removeUsername = (text: string)=>{ - let matchesLeft = true; - const dirSlash = process.platform === 'linux' ? '\/' : '\\'; - const array = text.split(dirSlash); - while (matchesLeft){ - let usersIndex = array.indexOf(process.platform === 'linux' ? 'home' : 'Users'); - if (usersIndex<1) matchesLeft = false; - else { - let usernameIndex = usersIndex+1; - if(array[usernameIndex].length == 0) usernameIndex += 1; - array[usernameIndex] = '・'.repeat(array[usernameIndex].length); - array[usersIndex] = process.platform === 'linux' ? 'ho\u200bme' : 'Us\u200bers'; - } - } return array.join(dirSlash); - } } diff --git a/src/commands/ban.ts b/src/commands/ban.ts index f0d091f..c818bff 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -1,8 +1,9 @@ import Discord from 'discord.js'; import TClient from '../client.js'; +import Punish from '../funcs/Punish.js'; export default { run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ - client.punish(interaction, 'ban'); + Punish(client, interaction, 'ban'); }, data: new Discord.SlashCommandBuilder() .setName('ban') diff --git a/src/commands/case.ts b/src/commands/case.ts index 73125fa..182ceaf 100644 --- a/src/commands/case.ts +++ b/src/commands/case.ts @@ -1,5 +1,6 @@ -import Discord from "discord.js"; +import Discord from 'discord.js'; import TClient from '../client.js'; +import FormatTime from '../helpers/FormatTime.js'; export default { run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ if (!client.isStaff(interaction.member)) return client.youNeedRole(interaction, 'dcmod'); @@ -21,7 +22,7 @@ export default { {name: '🔹 Moderator', value: `<@${punishment.moderator}> \`${punishment.moderator}\``, inline: true}, {name: '\u200b', value: '\u200b', inline: true}, {name: '🔹 Reason', value: `\`${punishment.reason || 'Reason unspecified'}\``, inline: true}) - if (punishment.duration) embed.addFields({name: '🔹 Duration', value: client.formatTime(punishment.duration, 100)}) + if (punishment.duration) embed.addFields({name: '🔹 Duration', value: `${FormatTime(punishment.duration, 100)}`}) if (punishment.expired) embed.addFields({name: '🔹 Expired', value: `This case has been overwritten by Case #${cancelledBy.id} for reason \`${cancelledBy.reason}\``}) if (punishment.cancels) embed.addFields({name: '🔹 Overwrites', value: `This case overwrites Case #${cancels.id} with reason \`${cancels.reason}\``}) interaction.reply({embeds: [embed]}); @@ -34,7 +35,7 @@ export default { const userPunishment = userPunishmentData.sort((a,b)=>a.time-b.time).map((punishment)=>{ return { name: `${punishment.type[0].toUpperCase()+punishment.type.slice(1)} | Case #${punishment.id}`, - value: `Reason: \`${punishment.reason}\`\n${punishment.duration ? `Duration: ${client.formatTime(punishment.duration, 3)}\n` : ''}Moderator: <@${punishment.moderator}>${punishment.expired ? `\nOverwritten by Case #${punishments.find(x=>x.cancels===punishment._id)?._id}` : ''}${punishment.cancels ? `\nOverwrites Case #${punishment.cancels}` : ''}` + value: `Reason: \`${punishment.reason}\`\n${punishment.duration ? `Duration: ${FormatTime(punishment.duration, 3)}\n` : ''}Moderator: <@${punishment.moderator}>${punishment.expired ? `\nOverwritten by Case #${punishments.find(x=>x.cancels===punishment._id)?._id}` : ''}${punishment.cancels ? `\nOverwrites Case #${punishment.cancels}` : ''}` } }); if (!punishments || !userPunishment) return interaction.reply(`**${user.username}** has a clean record.`) diff --git a/src/commands/dev.ts b/src/commands/dev.ts index ee54adc..4599b31 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -3,6 +3,8 @@ import {Octokit} from '@octokit/rest'; import {createTokenAuth} from '@octokit/auth-token'; import {exec} from 'node:child_process'; import MessageTool from '../helpers/MessageTool.js'; +import UsernameHelper from '../helpers/UsernameHelper.js'; +import FormatTime from '../helpers/FormatTime.js'; import fs from 'node:fs'; import util from 'node:util'; import TClient from '../client.js'; @@ -27,7 +29,7 @@ export default { const filter = (x:any)=>x.content === 'stack' && x.author.id === interaction.user.id const messagecollector = (interaction.channel as Discord.TextChannel).createMessageCollector({filter, max: 1, time: 60000}); messagecollector.on('collect', collected=>{ - collected.reply({content: `\`\`\`\n${client.removeUsername(err.stack)}\n\`\`\``, allowedMentions: {repliedUser: false}}); + collected.reply({content: `\`\`\`\n${UsernameHelper.stripName(err.stack)}\n\`\`\``, allowedMentions: {repliedUser: false}}); }); }); } @@ -40,7 +42,7 @@ export default { ].forEach(x=>output = output.replace(new RegExp(x as string,'g'),':noblank: No token?')); const embed = new client.embed().setColor(client.config.embedColor).setTitle('__Eval__').addFields( {name: 'Input', value: `\`\`\`js\n${code.slice(0,1010)}\n\`\`\``}, - {name: 'Output', value: `\`\`\`${client.removeUsername(output).slice(0,1016)}\n\`\`\``} + {name: 'Output', value: `\`\`\`${UsernameHelper.stripName(output).slice(0,1016)}\n\`\`\``} ); interaction.reply({embeds: [embed]}).catch(()=>(interaction.channel as Discord.TextChannel).send({embeds: [embed]})); }, @@ -63,11 +65,11 @@ export default { } }; exec('git pull',{windowsHide:true},(err:Error,stdout)=>{ - if (err) clarkson.edit(`\`\`\`${client.removeUsername(err.message)}\`\`\``) + if (err) clarkson.edit(`\`\`\`${UsernameHelper.stripName(err.message)}\`\`\``) else if (stdout.includes('Already up to date')) clarkson.edit('I am already up to date with the upstream repository.') else clarkson.edit('Compiling TypeScript files...').then(()=>exec('yarn tsc', {windowsHide:true}, (err:Error)=>{ - if (err) clarkson.edit(`\`\`\`${client.removeUsername(err.message)}\`\`\``) - if (interaction.options.getBoolean('restart')) clarkson.edit(`[Commit:](<${github.fetchCommit.url}>) **${github.fetchCommit.msg}**\nCommit author: **${github.fetchCommit.author}**\n\n__Commit changes__\nTotal: **${github.fetchChanges.total}**\nAdditions: **${github.fetchChanges.addition}**\nDeletions: **${github.fetchChanges.deletion}**\n\nSuccessfully compiled TypeScript files into JavaScript!\nUptime before restarting: **${client.formatTime(client.uptime, 3, {commas: true, longNames: true})}**`).then(()=>exec('pm2 restart Daggerbot', {windowsHide:true})); + if (err) clarkson.edit(`\`\`\`${UsernameHelper.stripName(err.message)}\`\`\``) + if (interaction.options.getBoolean('restart')) clarkson.edit(`[Commit:](<${github.fetchCommit.url}>) **${github.fetchCommit.msg}**\nCommit author: **${github.fetchCommit.author}**\n\n__Commit changes__\nTotal: **${github.fetchChanges.total}**\nAdditions: **${github.fetchChanges.addition}**\nDeletions: **${github.fetchChanges.deletion}**\n\nSuccessfully compiled TypeScript files into JavaScript!\nUptime before restarting: **${FormatTime(client.uptime, 3, {commas: true, longNames: true})}**`).then(()=>exec('pm2 restart Daggerbot', {windowsHide:true})); else clarkson.edit(`[Commit:](<${github.fetchCommit.url}>) **${github.fetchCommit.msg}**\nCommit author: **${github.fetchCommit.author}**\n\n__Commit changes__\nTotal: **${github.fetchChanges.total}**\nAdditions: **${github.fetchChanges.addition}**\nDeletions: **${github.fetchChanges.deletion}**\n\nSuccessfully compiled TypeScript files into JavaScript!`) })) }) @@ -112,15 +114,15 @@ export default { restart: async()=>{ const i = await interaction.reply({content: 'Compiling TypeScript files...', fetchReply: true}); exec('yarn tsc',{windowsHide:true},(err:Error)=>{ - if (err) i.edit(`\`\`\`${client.removeUsername(err.message)}\`\`\``) - else i.edit(`Successfully compiled TypeScript files into JavaScript!\nUptime before restarting: **${client.formatTime(client.uptime, 3, {commas: true, longNames: true})}**`).then(()=>exec('pm2 restart Daggerbot', {windowsHide:true})) + if (err) i.edit(`\`\`\`${UsernameHelper.stripName(err.message)}\`\`\``) + else i.edit(`Successfully compiled TypeScript files into JavaScript!\nUptime before restarting: **${FormatTime(client.uptime, 3, {commas: true, longNames: true})}**`).then(()=>exec('pm2 restart Daggerbot', {windowsHide:true})) }) }, file: ()=>interaction.reply({files:[`./src/database/${interaction.options.getString('name')}.json`]}).catch(()=>'Filesize is too large, upload cancelled.'), wake_device: async()=>{ const i = await interaction.reply({content: 'Spawning a task...', fetchReply: true}); exec(`cd "../../Desktop/System Tools/wakemeonlan" && WakeMeOnLan.exe /wakeup ${interaction.options.getString('name')}`, {windowsHide:true}, (err:Error)=>{ - if (err) i.edit(client.removeUsername(err.message)) + if (err) i.edit(UsernameHelper.stripName(err.message)) else i.edit('Your device should be awake by now!\n||Don\'t blame me if it isn\'t on.||') }) } diff --git a/src/commands/kick.ts b/src/commands/kick.ts index 35dce7e..93c1b60 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -1,8 +1,9 @@ import Discord from 'discord.js'; import TClient from '../client.js'; +import Punish from '../funcs/Punish.js'; export default { run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ - client.punish(interaction, 'kick'); + Punish(client, interaction, 'kick'); }, data: new Discord.SlashCommandBuilder() .setName('kick') diff --git a/src/commands/mp.ts b/src/commands/mp.ts index b15cd35..374fe6b 100644 --- a/src/commands/mp.ts +++ b/src/commands/mp.ts @@ -2,9 +2,10 @@ import Discord from 'discord.js'; import TClient from '../client.js'; import path from 'node:path'; import canvas from 'canvas'; +import FormatPlayer from '../helpers/FormatPlayer.js'; import MessageTool from '../helpers/MessageTool.js'; import {readFileSync} from 'node:fs'; -import {FSData} from 'src/typings/interfaces.js'; +import {FSData} from '../typings/interfaces.js'; const serverChoices = [ {name: 'Main Server', value: 'mainServer'}, @@ -149,11 +150,7 @@ export default { else if (endpoint.slots.used > 8) Color = client.config.embedColorYellow; else Color = client.config.embedColorGreen; - for (const player of endpoint.slots.players.filter(x=>x.isUsed)){ - let decorator = player.isAdmin ? ':detective:' : ''; - decorator += player.name.includes('Toast') ? '<:toastv2:1132681026662056079>' : ''; - playerData.push(`**${player.name}${decorator}**\nFarming for ${client.formatPlayerUptime(player.uptime)}`) - } + for (const player of endpoint.slots.players.filter(x=>x.isUsed)) playerData.push(`**${player.name}${FormatPlayer.decoratePlayerIcons(player)}**\nFarming for ${FormatPlayer.uptimeFormat(player.uptime)}`) const slot = `${endpoint.slots.used}/${endpoint.slots.capacity}`; const ingameTime = `${('0'+Math.floor((endpoint.server.dayTime/3600/1000))).slice(-2)}:${('0'+Math.floor((endpoint.server.dayTime/60/1000)%60)).slice(-2)}`; diff --git a/src/commands/mute.ts b/src/commands/mute.ts index aa7f5a3..c8c4b14 100644 --- a/src/commands/mute.ts +++ b/src/commands/mute.ts @@ -1,8 +1,9 @@ import Discord from 'discord.js'; import TClient from '../client.js'; +import Punish from '../funcs/Punish.js'; export default { run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ - client.punish(interaction, 'mute'); + Punish(client, interaction, 'mute'); }, data: new Discord.SlashCommandBuilder() .setName('mute') diff --git a/src/commands/ping.ts b/src/commands/ping.ts index ae16428..c87d764 100644 --- a/src/commands/ping.ts +++ b/src/commands/ping.ts @@ -1,10 +1,11 @@ import Discord from 'discord.js'; import TClient from '../client.js'; +import FormatTime from '../helpers/FormatTime.js'; export default { async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ if (client.uptime < 15500) return interaction.reply('I just restarted, wait 15 seconds and try again.') const msg = await interaction.reply({content: 'Pinging...', fetchReply: true}) - msg.edit(`API Latency: \`${client.formatTime(client.ws.ping, 3, {longNames: false, commas: true})}\`\nBot Latency: \`${client.formatTime(msg.createdTimestamp - interaction.createdTimestamp, 3, {longNames: false, commas: true})}\``) + msg.edit(`API Latency: \`${FormatTime(client.ws.ping, 3, {longNames: false, commas: true})}\`\nBot Latency: \`${FormatTime(msg.createdTimestamp - interaction.createdTimestamp, 3, {longNames: false, commas: true})}\``) }, data: new Discord.SlashCommandBuilder() .setName('ping') diff --git a/src/commands/softban.ts b/src/commands/softban.ts index eb4e23a..fa66895 100644 --- a/src/commands/softban.ts +++ b/src/commands/softban.ts @@ -1,8 +1,9 @@ import Discord from 'discord.js'; import TClient from '../client.js'; +import Punish from '../funcs/Punish.js'; export default { run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ - client.punish(interaction, 'softban'); + Punish(client, interaction, 'softban'); }, data: new Discord.SlashCommandBuilder() .setName('softban') diff --git a/src/commands/statistics.ts b/src/commands/statistics.ts index 192738a..38d8c13 100644 --- a/src/commands/statistics.ts +++ b/src/commands/statistics.ts @@ -1,6 +1,8 @@ import Discord from 'discord.js'; import pkg from 'typescript'; import MessageTool from '../helpers/MessageTool.js'; +import FormatBytes from '../helpers/FormatBytes.js'; +import FormatTime from '../helpers/FormatTime.js'; import si from 'systeminformation'; import TClient from '../client.js'; import os from 'node:os'; @@ -16,7 +18,7 @@ export default { const columns = ['Command name', 'Count']; const includedCommands = client.commands.filter(x=>x.uses).sort((a,b)=>b.uses - a.uses); - if (includedCommands.size === 0) return interaction.reply(`No commands have been used yet.\nUptime: **${client.formatTime(client.uptime, 3, {longNames: true, commas: true})}**`); + if (includedCommands.size === 0) return interaction.reply(`No commands have been used yet.\nUptime: **${FormatTime(client.uptime, 3, {longNames: true, commas: true})}**`); const nameLength = Math.max(...includedCommands.map(x=>x.command.default.data.name.length), columns[0].length) + 2; const amountLength = Math.max(...includedCommands.map(x=>x.uses.toString().length), columns[1].length) + 1; const rows = [`${columns[0] + ' '.repeat(nameLength - columns[0].length)}|${' '.repeat(amountLength - columns[1].length) + columns[1]}\n`, '-'.repeat(nameLength) + '-'.repeat(amountLength) + '\n']; @@ -50,13 +52,13 @@ export default { {name: '> __Host__', value: MessageTool.concatMessage( `**Operating System:** ${osInfo.distro + ' ' + osInfo.release}`, `**CPU:** ${cpu.manufacturer} ${cpu.brand}`, - `**Memory:** ${client.formatBytes(ram.used)}/${client.formatBytes(ram.total)}`, - `**Process:** ${client.formatBytes(process.memoryUsage().heapUsed)}/${client.formatBytes(process.memoryUsage().heapTotal)}`, + `**Memory:** ${FormatBytes(ram.used)}/${FormatBytes(ram.total)}`, + `**Process:** ${FormatBytes(process.memoryUsage().heapUsed)}/${FormatBytes(process.memoryUsage().heapTotal)}`, `**Load Usage:**\nUser: ${currentLoad.currentLoadUser.toFixed(1)}%\nSystem: ${currentLoad.currentLoadSystem.toFixed(1)}%`, - `**Uptime:**\nHost: ${client.formatTime((os.uptime()*1000), 2, {longNames: true, commas: true})}\nBot: ${client.formatTime(client.uptime, 2, {commas: true, longNames: true})}` + `**Uptime:**\nHost: ${FormatTime((os.uptime()*1000), 2, {longNames: true, commas: true})}\nBot: ${FormatTime(client.uptime, 2, {commas: true, longNames: true})}` )} ); - waitForData.edit({content:null,embeds:[embed]}).then(x=>x.edit({embeds:[new client.embed(x.embeds[0].data).setFooter({text: `Load time: ${client.formatTime(x.createdTimestamp - interaction.createdTimestamp, 2, {longNames: true, commas: true})}`})]})) + waitForData.edit({content:null,embeds:[embed]}).then(x=>x.edit({embeds:[new client.embed(x.embeds[0].data).setFooter({text: `Load time: ${FormatTime(x.createdTimestamp - interaction.createdTimestamp, 2, {longNames: true, commas: true})}`})]})) }, data: new Discord.SlashCommandBuilder() .setName('statistics') diff --git a/src/commands/warn.ts b/src/commands/warn.ts index 39db46a..bbacebd 100644 --- a/src/commands/warn.ts +++ b/src/commands/warn.ts @@ -1,8 +1,9 @@ import Discord from 'discord.js'; import TClient from '../client.js'; +import Punish from '../funcs/Punish.js'; export default { run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ - client.punish(interaction, 'warn'); + Punish(client, interaction, 'warn'); }, data: new Discord.SlashCommandBuilder() .setName('warn') diff --git a/src/funcs/MPLoop.ts b/src/funcs/MPLoop.ts index fbd5a04..44176c4 100644 --- a/src/funcs/MPLoop.ts +++ b/src/funcs/MPLoop.ts @@ -1,5 +1,6 @@ import Discord from 'discord.js'; import TClient from '../client'; +import FormatPlayer from '../helpers/FormatPlayer.js'; import {writeFileSync, readFileSync} from 'node:fs'; import {FSPlayer, FSData, FSCareerSavegame, TServer} from '../typings/interfaces'; @@ -12,12 +13,6 @@ export default async(client:TClient, Channel:string, Message:string, Server:TSer const serverErrorEmbed = new client.embed().setColor(client.config.embedColorRed).setTitle('Host did not respond back in time'); const genericEmbed = new client.embed(); - const decoPlayer = (player:FSPlayer)=>{ - let decorator = player.isAdmin ? ':detective:' : ''; - decorator += player.name.includes('Toast') ? '<:toastv2:1132681026662056079>' : ''; - return decorator - } - const HITALL = async()=>{ let sessionInit = {signal: AbortSignal.timeout(8200),headers:{'User-Agent':`Daggerbot - HITALL/undici`}}; try { @@ -42,17 +37,17 @@ export default async(client:TClient, Channel:string, Message:string, Server:TSer // Join/Leave log function playerLogEmbed(player:FSPlayer,joinLog:boolean){ - const logEmbed = new client.embed().setDescription(`**${player.name}${decoPlayer(player)}** ${joinLog ? 'joined' : 'left'} **${client.MPServerCache[ServerName].name}** at `); + const logEmbed = new client.embed().setDescription(`**${player.name}${FormatPlayer.decoratePlayerIcons(player)}** ${joinLog ? 'joined' : 'left'} **${client.MPServerCache[ServerName].name}** at `); if (joinLog) return logEmbed.setColor(client.config.embedColorGreen); - else if (player.uptime > 0) return logEmbed.setColor(client.config.embedColorRed).setFooter({text:`Farmed for ${client.formatPlayerUptime(player.uptime)}`}); + else if (player.uptime > 0) return logEmbed.setColor(client.config.embedColorRed).setFooter({text:`Farmed for ${FormatPlayer.uptimeFormat(player.uptime)}`}); else return logEmbed.setColor(client.config.embedColorRed); } const serverLog = client.channels.resolve(client.config.mainServer.channels.fs_server_log) as Discord.TextChannel; const playersOnServer = hitDSS.slots?.players.filter(x=>x.isUsed); const playersInCache = client.MPServerCache[ServerName].players; - if (!playersOnServer ?? playersOnServer === undefined) return new Error('[MPLoop] Empty array, ignoring...'); // For the love of god, stop throwing errors everytime. - playersOnServer.forEach(player=>playerData.push(`**${player.name}${decoPlayer(player)}**\nFarming for ${client.formatPlayerUptime(player.uptime)}`)); + if (!playersOnServer ?? playersOnServer === undefined) return console.log('[MPLoop] Empty array, ignoring...'); // For the love of god, stop throwing errors everytime. + playersOnServer.forEach(player=>playerData.push(`**${player.name}${FormatPlayer.decoratePlayerIcons(player)}**\nFarming for ${FormatPlayer.uptimeFormat(player.uptime)}`)); // Player leaving for (const player of playersInCache.filter(x=>!playersOnServer.some(y=>y.name === x.name))){ diff --git a/src/funcs/Punish.ts b/src/funcs/Punish.ts new file mode 100644 index 0000000..6946015 --- /dev/null +++ b/src/funcs/Punish.ts @@ -0,0 +1,20 @@ +import Discord from 'discord.js'; +import TClient from '../client.js'; + +export default async(client:TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>, type: string)=>{ + if (!client.isStaff(interaction.member as Discord.GuildMember)) return client.youNeedRole(interaction, 'dcmod'); + + const time = interaction.options.getString('time') ?? undefined; + const reason = interaction.options.getString('reason') ?? 'Reason unspecified'; + const GuildMember = interaction.options.getMember('member') ?? undefined; + const User = interaction.options.getUser('member', true); + + console.log(client.logTime(), `[PunishmentLog] ${GuildMember?.user?.username ?? User?.username ?? 'No user data'} ${time ? ['warn', 'kick'].includes(type) ? 'and no duration set' : `and ${time} (duration)` : ''} was used in /${interaction.commandName} for ${reason}`); + (client.channels.cache.get(client.config.mainServer.channels.punishment_log) as Discord.TextChannel).send({embeds:[new client.embed().setColor(client.config.embedColor).setAuthor({name: interaction?.user?.username, iconURL: interaction?.user?.displayAvatarURL({size:2048})}).setTitle('Punishment Log').setDescription(`${GuildMember?.user?.username ?? User?.username ?? 'No user data'} ${time ? ['warn', 'kick'].includes(client.punishments.type) ? 'and no duration set' : `and ${time} (duration)` : ''} was used in \`/${interaction.commandName}\` for \`${reason}\``).setTimestamp()]}); + if (interaction.user.id === User.id) return interaction.reply(`You cannot ${type} yourself.`); + if (!GuildMember && type != 'unban') return interaction.reply(`You cannot ${type} someone who is not in the server.`); + if (User.bot) return interaction.reply(`You cannot ${type} a bot!`); + + await interaction.deferReply(); + await client.punishments.addPunishment(type, {time, interaction}, interaction.user.id, reason, User, GuildMember); +} diff --git a/src/funcs/YTLoop.ts b/src/funcs/YTLoop.ts new file mode 100644 index 0000000..764b6de --- /dev/null +++ b/src/funcs/YTLoop.ts @@ -0,0 +1,18 @@ +import {TextChannel} from 'discord.js'; +import TClient from '../client.js'; + +export default async(client: TClient, YTChannelID: string, YTChannelName: string, DiscordChannelID: string, DiscordRoleID: string)=>{ + let Data: any; + try { + await fetch(`https://www.youtube.com/feeds/videos.xml?channel_id=${YTChannelID}`, {signal: AbortSignal.timeout(8000), headers: {'User-Agent': 'Daggerbot - Notification/undici'}}).then(async xml=>Data = client.xjs.xml2js(await xml.text(), {compact: true})) + } catch(err){ + console.log(client.logTime(), `Failed to fetch "${YTChannelName}" from YouTube`) + } + + if (!Data) return; + if (!client.YTCache[YTChannelID]) return client.YTCache[YTChannelID] = Data.feed.entry[0]['yt:videoId']._text; + if (Data.feed.entry[1]['yt:videoId']._text === client.YTCache[YTChannelID]){ + client.YTCache[YTChannelID] = Data.feed.entry[0]['yt:videoId']._text; + (client.channels.resolve(DiscordChannelID) as TextChannel).send({content: `<@&${DiscordRoleID}> (Ping notification are currently WIP, no eta when complete, the mentioned role is a placeholder for now)\n**${YTChannelName}** just uploaded a video!\n${Data.feed.entry[0].link._attributes.href}`, allowedMentions: {parse: ['roles']}}) + } +} diff --git a/src/helpers/FormatBytes.ts b/src/helpers/FormatBytes.ts new file mode 100644 index 0000000..16e1624 --- /dev/null +++ b/src/helpers/FormatBytes.ts @@ -0,0 +1,5 @@ +export default (bytes:number, decimals:number = 2)=>{ + if (bytes === 0) return '0 Bytes'; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return parseFloat((bytes / Math.pow(1024, i)).toFixed(decimals < 0 ? 0 : decimals))+ ' ' +['Bytes', 'KB', 'MB', 'GB', 'TB'][i] +} \ No newline at end of file diff --git a/src/helpers/FormatPlayer.ts b/src/helpers/FormatPlayer.ts new file mode 100644 index 0000000..4a5633a --- /dev/null +++ b/src/helpers/FormatPlayer.ts @@ -0,0 +1,22 @@ +import {FSPlayer} from '../typings/interfaces'; + +export default class FormatPlayer { + static uptimeFormat(playTime: number){ + var Hours = 0; + playTime = Math.floor(Number(playTime)); + if(playTime >= 60){ + var Hours = Math.floor(Number(playTime)/60); + var Minutes = (Number(playTime)-(Hours*60)); + } else Minutes = Number(playTime) + if(Hours >= 24){ + var Days = Math.floor(Number(Hours)/24); + var Hours = (Hours-(Days*24)); + } return (Days > 0 ? Days+' d ':'')+(Hours > 0 ? Hours+' h ':'')+(Minutes > 0 ? Minutes+' m':'') + } + static decoratePlayerIcons(player:FSPlayer){ + let decorator = player.isAdmin ? ':detective:' : ''; + decorator += player.name.includes('Toast') ? '<:toast:1132681026662056079>' : ''; + decorator += player.name.includes('Daggerwin') ? '<:Daggerwin:549283056079339520>' : ''; // Probably useless lol, but we'll see. + return decorator + } +} diff --git a/src/helpers/FormatTime.ts b/src/helpers/FormatTime.ts new file mode 100644 index 0000000..609f5cb --- /dev/null +++ b/src/helpers/FormatTime.ts @@ -0,0 +1,35 @@ +interface formatTimeOpt { + longNames: boolean, + commas: boolean +} + +export default (integer:number, accuracy:number = 1, options?:formatTimeOpt)=>{ + let achievedAccuracy = 0; + let text:any = ''; + for (const timeName of [ + {name: 'year', length: 31536000000}, + {name: 'month', length: 2592000000}, + {name: 'week', length: 604800000}, + {name: 'day', length: 86400000}, + {name: 'hour', length: 3600000}, + {name: 'minute', length: 60000}, + {name: 'second', length: 1000} + ]){ + if (achievedAccuracy < accuracy){ + const fullTimelengths = Math.floor(integer/timeName.length); + if (fullTimelengths === 0) continue; + achievedAccuracy++; + text += fullTimelengths + (options?.longNames ? (' '+timeName.name+(fullTimelengths === 1 ? '' : 's')) : timeName.name.slice(0, timeName.name === 'month' ? 2 : 1)) + (options?.commas ? ', ' : ' '); + integer -= fullTimelengths*timeName.length; + } else break; + } + if (text.length === 0) text = integer + (options?.longNames ? ' milliseconds' : 'ms') + (options?.commas ? ', ' : ''); + if (options?.commas){ + text = text.slice(0, -2); + if (options?.longNames){ + text = text.split(''); + text[text.lastIndexOf(',')] = ' and'; + text = text.join(''); + } + } return text.trim(); +} diff --git a/src/helpers/UsernameHelper.ts b/src/helpers/UsernameHelper.ts new file mode 100644 index 0000000..9628384 --- /dev/null +++ b/src/helpers/UsernameHelper.ts @@ -0,0 +1,18 @@ +export default class UsernameHelper { + static stripName(text: string){ + let matchesLeft = true; + const dirSlash = process.platform === 'linux' ? '\/' : '\\'; + const array = text.split(dirSlash); + while (matchesLeft) { + let usersIndex = array.indexOf(process.platform === 'linux' ? 'media' : 'Users'); + if (usersIndex < 1) matchesLeft = false; + else { + let usernameIndex = usersIndex + 1; + if (array[usernameIndex].length === 0) usernameIndex += 1; + array[usernameIndex] = '・'.repeat(array[usernameIndex].length); + array[usersIndex] = process.platform === 'linux' ? 'med\u200bia' : 'Us\u200bers'; + } + return array.join(dirSlash); + } + } +} diff --git a/src/index.ts b/src/index.ts index b0b96ba..6ac59d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import Discord from 'discord.js'; import TClient from './client.js'; const client = new TClient; client.init(); +import YTLoop from './funcs/YTLoop.js'; import MPLoop from './funcs/MPLoop.js'; import {Player} from 'discord-player'; const player = Player.singleton(client); @@ -10,11 +11,7 @@ import {writeFileSync, readFileSync} from 'node:fs'; // Error handler function DZ(error:Error, type:string){// Yes, I may have shiternet but I don't need to wake up to like a hundred messages or so. - if ([ - 'ConnectTimeoutError: Connect Timeout Error', 'getaddrinfo EAI_AGAIN discord.com', - '[Error: 30130000:error:0A000410:SSL', '[Error: F8200000:error:0A000410:SSL', - 'HTTPError: Internal Server Error' - ].includes(error.message)) return; + if (JSON.parse(readFileSync('src/errorBlocklist.json', 'utf8')).includes(error.message)) return;// I wonder if my idea works, if not then please run me over with a bulldozer. console.error(error); (client.channels.resolve(client.config.mainServer.channels.errors) as Discord.TextChannel | null)?.send({embeds: [new client.embed().setColor('#560000').setTitle('Error caught!').setFooter({text: 'Error type: ' + type}).setDescription(`**Error:**\n\`\`\`${error.message}\`\`\`**Stack:**\n\`\`\`${`${error.stack}`.slice(0, 2500)}\`\`\``)]}) } @@ -41,9 +38,9 @@ if (client.config.botSwitches.mpstats) setInterval(async()=>{ const serverlake = (await client.MPServer._content.findById(client.config.mainServer.id)); for await (const [locName, locArea] of Object.entries(client.config.MPStatsLocation)) await MPLoop(client, locArea.channel, locArea.message, serverlake[locName], locName) }, 35000); -setInterval(async()=>{ - client.YTLoop('UCQ8k8yTDLITldfWYKDs3xFg', 'Daggerwin', '528967918772551702'); // 528967918772551702 = #videos-and-streams - client.YTLoop('UCguI73--UraJpso4NizXNzA', 'Machinery Restorer', '767444045520961567') // 767444045520961567 = #machinery-restorer +setInterval(async()=>{// Ping notification is currently WIP, it might be active in production but I want to see how it goes with role mentions first so I can make any further changes. + YTLoop(client, 'UCQ8k8yTDLITldfWYKDs3xFg', 'Daggerwin', '528967918772551702', '1011341005389307925'); // 528967918772551702 = #videos-and-streams; 1011341005389307925 = Bot Tech; + YTLoop(client, 'UCguI73--UraJpso4NizXNzA', 'Machinery Restorer', '767444045520961567', '989591094524276796') // 767444045520961567 = #machinery-restorer; 989591094524276796 = Temp; }, 300000) // Event loop for punishments and daily msgs diff --git a/src/models/punishments.ts b/src/models/punishments.ts index 541b66c..6fb7afe 100644 --- a/src/models/punishments.ts +++ b/src/models/punishments.ts @@ -2,6 +2,7 @@ import Discord from 'discord.js'; import TClient from '../client.js'; import mongoose from 'mongoose'; import ms from 'ms'; +import FormatTime from '../helpers/FormatTime.js'; import {Punishment} from '../typings/interfaces.js'; const Schema = mongoose.model('punishments', new mongoose.Schema({ @@ -36,7 +37,7 @@ export default class punishments extends Schema { {name: '\u200b', value: '\u200b', inline: true}, {name: '🔹 Reason', value: `\`${punishment.reason}\``, inline: true}) .setColor(this.client.config.embedColor).setTimestamp(punishment.time) - if (punishment.duration) embed.addFields({name: '🔹 Duration', value: this.client.formatTime(punishment.duration, 100), inline: true}, {name: '\u200b', value: '\u200b', inline: true}) + if (punishment.duration) embed.addFields({name: '🔹 Duration', value: `${FormatTime(punishment.duration, 100)}`, inline: true}, {name: '\u200b', value: '\u200b', inline: true}) if (punishment.cancels) { const cancels = await this._content.findById(punishment.cancels); embed.addFields({name: '🔹 Overwrites', value: `This case overwrites Case #${cancels.id}\n\`${cancels.reason}\``}) @@ -72,7 +73,7 @@ export default class punishments extends Schema { if (type == 'mute') timeInMillis = time ? ms(time) : 2419140000; // Timeouts have a limit of 4 weeks else timeInMillis = time ? ms(time) : null; - const durationText = timeInMillis ? ` for ${this.client.formatTime(timeInMillis, 4, {longNames:true,commas:true})}` : ''; + const durationText = timeInMillis ? ` for ${FormatTime(timeInMillis, 4, {longNames:true,commas:true})}` : ''; if (time) embed.addFields({name: 'Duration', value: durationText}); if (GuildMember){ diff --git a/src/typings/interfaces.d.ts b/src/typings/interfaces.d.ts index 8e83c24..8a1ff81 100644 --- a/src/typings/interfaces.d.ts +++ b/src/typings/interfaces.d.ts @@ -4,10 +4,6 @@ export interface UserLevels { messages: number, level: number } -export interface formatTimeOpt { - longNames: boolean, - commas: boolean -} export interface punOpt { time?: string, reason?: string,