From c73e75955342f2ae34d9082c6e07d19961bd7b27 Mon Sep 17 00:00:00 2001 From: toast-ts <96593068+toast-ts@users.noreply.github.com> Date: Sat, 25 Feb 2023 11:55:11 +1100 Subject: [PATCH] Major changes to the bot. --- package.json | 5 +- src/client.ts | 45 +++-- src/commands/bannedWords.ts | 27 +-- src/commands/bonk.ts | 48 ++--- src/commands/case.ts | 86 ++++----- src/commands/dev.ts | 329 ++++++++++++++++---------------- src/commands/faq.ts | 52 +++-- src/commands/mp.ts | 2 +- src/commands/rank.ts | 33 ++-- src/commands/unpunish.ts | 31 +-- src/events/guildMemberRemove.ts | 12 +- src/events/interactionCreate.ts | 2 +- src/events/messageCreate.ts | 2 +- src/events/messageUpdate.ts | 2 +- src/events/roleUpdate.ts | 31 --- src/index.ts | 22 ++- src/models/bannedWords.ts | 17 ++ src/models/bonkCount.ts | 23 +++ src/models/punishments.ts | 151 +++++++++++++++ src/models/userLevels.ts | 37 ++++ src/schoolClassroom.ts | 223 ---------------------- src/typings/interfaces.d.ts | 12 +- 22 files changed, 587 insertions(+), 605 deletions(-) delete mode 100644 src/events/roleUpdate.ts create mode 100644 src/models/bannedWords.ts create mode 100644 src/models/bonkCount.ts create mode 100644 src/models/punishments.ts create mode 100644 src/models/userLevels.ts delete mode 100644 src/schoolClassroom.ts diff --git a/package.json b/package.json index 6e6d0d8..ea25d99 100644 --- a/package.json +++ b/package.json @@ -30,13 +30,14 @@ "ms": "2.1.3", "sequelize": "6.28.2", "sqlite3": "5.1.4", - "systeminformation": "5.17.9", + "mongoose": "6.10.0", + "systeminformation": "5.17.10", "@octokit/rest": "19.0.7", "typescript": "4.9.5", "xml-js": "1.6.11" }, "devDependencies": { - "@types/node": "18.14.0", + "@types/node": "18.14.1", "ts-node": "10.9.1" } } diff --git a/src/client.ts b/src/client.ts index b8aa657..80540a5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,8 +1,13 @@ import Discord, { Client, WebhookClient, GatewayIntentBits, Partials } from 'discord.js'; import fs from 'node:fs'; +import { exec } from 'node:child_process'; import timeNames from './timeNames'; -import { Punishment, formatTimeOpt, Tokens, Config, repeatedMessages } from './typings/interfaces'; -import { bannedWords, bonkCount, userLevels, punishments } from './schoolClassroom'; +import mongoose from 'mongoose'; +import { formatTimeOpt, Tokens, Config, repeatedMessages } from './typings/interfaces'; +import bannedWords from './models/bannedWords'; +import userLevels from './models/userLevels'; +import punishments from './models/punishments'; +import bonkCount from './models/bonkCount'; import MPDB from './models/MPServer'; import axios from 'axios'; import moment from 'moment'; @@ -82,11 +87,16 @@ export default class TClient extends Client { } async init(){ MPDB.sync(); - this.login(this.tokens.token_main); - this.punishments.initLoad(); - this.bannedWords.initLoad(); - this.bonkCount.initLoad(); - this.userLevels.initLoad().intervalSave(30000).disableSaveNotifs(); + mongoose.set('strictQuery', true); + await mongoose.connect(this.tokens.mongodb_uri, { + autoIndex: true, + serverSelectionTimeoutMS: 5000, + socketTimeoutMS: 30000, + family: 4, + keepAlive: true, + waitQueueTimeoutMS: 50000 + }).then(()=>console.log(this.logTime(), 'Successfully connected to MongoDB')).catch(()=>{console.log(this.logTime(), 'Failed to connect to MongoDB'); exec('pm2 stop Daggerbot')}) + await this.login(this.tokens.main); const commandFiles = fs.readdirSync('src/commands').filter(file=>file.endsWith('.ts')); for (const file of commandFiles){ const command = require(`./commands/${file}`); @@ -98,12 +108,6 @@ export default class TClient extends Client { this.on(file.replace('.ts', ''), async(...args)=>eventFile.default.run(this,...args)); }); } - formatPunishmentType(punishment: Punishment, client: TClient, cancels?: Punishment){ - if (punishment.type == 'removeOtherPunishment'){ - cancels ||= this.punishments._content.find((x: Punishment)=>x.id === punishment.cancels) - return cancels.type[0].toUpperCase()+cancels.type.slice(1)+' Removed'; - } else return punishment.type[0].toUpperCase()+punishment.type.slice(1); - } formatTime(integer: number, accuracy = 1, options?: formatTimeOpt){ let achievedAccuracy = 0; let text:any = ''; @@ -134,6 +138,9 @@ export default class TClient extends Client { youNeedRole(interaction: Discord.CommandInteraction, role:string){ return interaction.reply(`This command is restricted to <@&${this.config.mainServer.roles[role]}>`) } + logTime(){ + return `[${this.moment().format('DD/MM/YY HH:mm:ss')}]` + } alignText(text: string, length: number, alignment: string, emptyChar = ' '){ if (alignment == 'right'){ text = emptyChar.repeat(length - text.length)+text; @@ -158,14 +165,6 @@ export default class TClient extends Client { await interaction.deferReply(); await client.punishments.addPunishment(type, { time, interaction }, interaction.user.id, reason, User, GuildMember); - }; - async unPunish(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ - if (!client.isStaff(interaction.member as Discord.GuildMember)) return this.youNeedRole(interaction, 'dcmod'); - const punishment = this.punishments._content.find((x:Punishment)=>x.id === interaction.options.getInteger('case_id')); - if (!punishment) return interaction.reply({content: 'Invalid Case #', ephemeral: true}); - const reason = interaction.options.getString('reason') ?? 'Reason unspecified'; - const unpunishResult = await this.punishments.removePunishment(punishment.id, interaction.user.id, reason); - interaction.reply(unpunishResult) } async YTLoop(YTChannelID: string, YTChannelName: string, DCChannelID: string){ let Data:any; @@ -177,7 +176,7 @@ export default class TClient extends Client { }) } catch(err){ error = true; - console.log(`[${this.moment().format('DD/MM/YY HH:mm:ss')}]`, `${YTChannelName} YT fail`) + console.log(this.logTime(), `${YTChannelName} YT fail`) } if (!Data) return; @@ -192,5 +191,5 @@ export default class TClient extends Client { } } -export class WClient extends WebhookClient {tokens: Tokens; constructor(){super({url: tokens.webhook_url})}} +export class WClient extends WebhookClient {tokens: Tokens; constructor(){super({url: tokens.webhook_url_test})}} // hi tae, ik you went to look for secret hello msgs in here too. \ No newline at end of file diff --git a/src/commands/bannedWords.ts b/src/commands/bannedWords.ts index 1cb4d59..2833528 100644 --- a/src/commands/bannedWords.ts +++ b/src/commands/bannedWords.ts @@ -3,33 +3,34 @@ import TClient from 'src/client'; export default { async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ if (!client.isStaff(interaction.member) && !client.config.eval.whitelist.includes(interaction.member.id)) return client.youNeedRole(interaction, 'admin') - const word = interaction.options.getString('word'); + const word = interaction.options.getString('word', true); + const wordExists = await client.bannedWords._content.findById(word); ({ - add: ()=>{ - if (client.bannedWords._content.includes(word)) return interaction.reply({content: `\`${word}\` is already added.`, ephemeral: true}); - client.bannedWords.addData(word).forceSave(); - interaction.reply(`Successfully added \`${word}\` to the list.`) + add: async()=>{ + if (wordExists) return interaction.reply({content: `\`${word}\` is already added.`, ephemeral: true}); + await client.bannedWords._content.create({_id:word}).then(a=>a.save()); + interaction.reply(`Successfully added \`${word}\` to the database.`) }, - remove: ()=>{ - if (client.bannedWords._content.includes(word) == false) return interaction.reply({content: `\`${word}\` doesn't exist on the list.`, ephemeral: true}); - client.bannedWords.removeData(word, 0, 0).forceSave(); - interaction.reply(`Successfully removed \`${word}\` from the list.`) + remove: async()=>{ + if (!wordExists) return interaction.reply({content: `\`${word}\` doesn't exist on the list.`, ephemeral: true}); + await client.bannedWords._content.findOneAndDelete({_id:word}); + interaction.reply(`Successfully removed \`${word}\` from the database.`) }, - view: ()=>interaction.reply({content: 'Here is a complete list of banned words!\n*You can open it with a web browser, e.g Chrome/Firefox/Safari, or you can use Visual Studio Code/Notepad++*', files: ['src/database/bannedWords.json'], ephemeral: true}) + //view: ()=>interaction.reply({content: 'Here is a complete list of banned words!\n*You can open it with a web browser, e.g Chrome/Firefox/Safari, or you can use Visual Studio Code/Notepad++*', files: ['src/database/bannedWords.json'], ephemeral: true}) } as any)[interaction.options.getSubcommand()](); }, data: new SlashCommandBuilder() .setName('bannedwords') .setDescription('description placeholder') - .addSubcommand((opt)=>opt + /*.addSubcommand((opt)=>opt .setName('view') .setDescription('View the list of currently banned words.')) - .addSubcommand((opt)=>opt + */.addSubcommand((opt)=>opt .setName('add') .setDescription('What word do you want to add?') .addStringOption((optt)=>optt .setName('word') - .setDescription('Add the specific word to automod\'s bannedWords list.') + .setDescription('Add the specific word to automod\'s bannedWords database.') .setRequired(true))) .addSubcommand((opt)=>opt .setName('remove') diff --git a/src/commands/bonk.ts b/src/commands/bonk.ts index 2e6cba0..6f091d4 100644 --- a/src/commands/bonk.ts +++ b/src/commands/bonk.ts @@ -1,28 +1,28 @@ import Discord,{SlashCommandBuilder} from 'discord.js'; import TClient from 'src/client'; export default { - async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ - //if (!client.isStaff(interaction.member) && interaction.channelId == '468835415093411863') return interaction.reply('This command is restricted to staff only in this channel due to high usage.') - const member = interaction.options.getMember('member') as Discord.GuildMember; - const reason = interaction.options.getString('reason'); - const adminPerm = member.permissions.has('Administrator'); - if (adminPerm) return interaction.reply('You cannot bonk an admin!'); - - client.bonkCount._incrementUser(member.id).forceSave(); - interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor) - .setDescription(`> <@${member.id}> has been bonked!\n${reason?.length == null ? '' : `> Reason: **${reason}**`}`) - .setImage('https://media.tenor.com/7tRddlNUNNcAAAAd/hammer-on-head-minions.gif') - .setFooter({text: `Bonk count for ${member.user.tag}: ${client.bonkCount.getUser(member.id).toLocaleString('en-US')}`}) - ]}) - }, - data: new SlashCommandBuilder() - .setName('bonk') - .setDescription('Bonk a member') - .addUserOption((opt)=>opt - .setName('member') - .setDescription('Which member to bonk?') - .setRequired(true)) - .addStringOption((opt)=>opt - .setName('reason') - .setDescription('Reason for the bonk')) + async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ + //if (!client.isStaff(interaction.member) && interaction.channelId == '468835415093411863') return interaction.reply('This command is restricted to staff only in this channel due to high usage.') + const member = interaction.options.getMember('member') as Discord.GuildMember; + const reason = interaction.options.getString('reason'); + const adminPerm = member.permissions.has('Administrator'); + if (adminPerm) return interaction.reply('You cannot bonk an admin!'); + + await client.bonkCount._incrementUser(member.id); + interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor) + .setDescription(`> <@${member.id}> has been bonked!\n${reason?.length == null ? '' : `> Reason: **${reason}**`}`) + .setImage('https://media.tenor.com/7tRddlNUNNcAAAAd/hammer-on-head-minions.gif') + .setFooter({text: `Bonk count for ${member.user.tag}: ${await client.bonkCount._content.findById(member.id).then(b=>b.value.toLocaleString('en-US'))}`}) + ]}) + }, + data: new SlashCommandBuilder() + .setName('bonk') + .setDescription('Bonk a member') + .addUserOption((opt)=>opt + .setName('member') + .setDescription('Which member to bonk?') + .setRequired(true)) + .addStringOption((opt)=>opt + .setName('reason') + .setDescription('Reason for the bonk')) } diff --git a/src/commands/case.ts b/src/commands/case.ts index 9145897..29af153 100644 --- a/src/commands/case.ts +++ b/src/commands/case.ts @@ -1,51 +1,49 @@ import Discord,{SlashCommandBuilder} from "discord.js"; import TClient from 'src/client'; -import { Punishment } from "src/typings/interfaces"; export default { async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ - if (!client.isStaff(interaction.member)) return client.youNeedRole(interaction, 'dcmod'); - const caseId = interaction.options.getInteger('id'); - ({ - update: async()=>{ - const reason = interaction.options.getString('reason'); - client.punishments._content.find((x:Punishment)=>x.id==caseId).reason = reason; - client.punishments.forceSave(); - await interaction.reply({embeds: [new client.embed().setColor(client.config.embedColorGreen).setTitle('Case updated').setDescription(`Case #${caseId} has been successfully updated with new reason:\n\`${reason}\``)]}) - }, - view: ()=>{ - const punishment = client.punishments._content.find((x:Punishment)=>x.id==caseId); - if (!punishment) return interaction.reply('Invalid case #'); - const cancelledBy = punishment.expired ? client.punishments._content.find((x:Punishment)=>x.cancels==punishment.id) : null; - const cancels = punishment.cancels ? client.punishments._content.find((x:Punishment)=>x.id==punishment.cancels) : null; - const embed = new client.embed().setColor(client.config.embedColor).setTimestamp(punishment.time).setTitle(`${client.formatPunishmentType(punishment, client, cancels)} | Case #${punishment.id}`).addFields( - {name: '🔹 User', value: `<@${punishment.member}> \`${punishment.member}\``, inline: true}, - {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.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]}); - }, - member: ()=>{ - // if caseid is user id, show their punishment history sorted by most recent. - const user = (interaction.options.getUser('user') as Discord.User); - if (user.bot) return interaction.reply(`<@${user.id}>'s punishment history cannot be viewed.`) - const punishment = client.punishments._content.find((x:Punishment)=>x.member===user.id); - if (!punishment) return interaction.reply(`<@${user.id}> has a clean record.`) - const cancels = punishment.cancels ? client.punishments._content.find((x:Punishment)=>x.id==punishment.cancels) : null; - const userPunishment = client.punishments._content.filter((x:Punishment)=>x.member==user.id).sort((a:Punishment,b:Punishment)=>a.time-b.time).map((punishment:Punishment)=>{ - return { - name: `${client.formatPunishmentType(punishment, client, cancels)} | 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 #${client.punishments._content.find((x:Punishment)=>x.cancels==punishment.id).id}` : ''}${punishment.cancels ? `\nOverwrites case #${punishment.cancels}` : ''}` - } - }); - // if caseid is not a punishment nor a user, failed - if (!userPunishment || userPunishment.length == 0) return interaction.reply('No punishments found for that case # or User ID'); - const pageNum = interaction.options.getInteger('page') ?? 1; - return interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle(`${user.username}'s punishment history`).setDescription(`**ID:** \`${user.id}\``).setFooter({text: `${userPunishment.length} total punishments. Viewing page ${pageNum} out of ${Math.ceil(userPunishment.length/6)}.`}).addFields(userPunishment.slice((pageNum - 1) * 6, pageNum * 6))]}); - } - } as any)[interaction.options.getSubcommand()](); + if (!client.isStaff(interaction.member)) return client.youNeedRole(interaction, 'dcmod'); + const caseId = interaction.options.getInteger('id'); + ({ + update: async()=>{ + const reason = interaction.options.getString('reason'); + await client.punishments._content.findByIdAndUpdate(caseId, {reason}); + await interaction.reply({embeds: [new client.embed().setColor(client.config.embedColorGreen).setTitle('Case updated').setDescription(`Case #${caseId} has been successfully updated with new reason:\n\`${reason}\``)]}) + }, + view: async()=>{ + const punishment = await client.punishments._content.findById(caseId); + if (!punishment) return interaction.reply('Invalid Case #'); + const cancelledBy = punishment.expired ? await client.punishments._content.findOne({cancels:punishment.id}) : null; + const cancels = punishment.cancels ? await client.punishments._content.findOne({_id:punishment.cancels}) : null; + const embed = new client.embed().setColor(client.config.embedColor).setTimestamp(punishment.time).setTitle(`${punishment.type[0].toUpperCase()+punishment.type.slice(1)} | Case #${punishment.id}`).addFields( + {name: '🔹 User', value: `<@${punishment.member}> \`${punishment.member}\``, inline: true}, + {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.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]}); + }, + member: async()=>{ + // if caseid is user id, show their punishment history sorted by most recent. + const user = (interaction.options.getUser('user') as Discord.User); + if (user.bot) return interaction.reply(`<@${user.id}>'s punishment history cannot be viewed.`) + const punishments = await client.punishments._content.find({}); + if (!punishments) return interaction.reply(`<@${user.id}> has a clean record.`) + const userPunishmentData = await client.punishments._content.find({'member':user.id}); + 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}` : ''}` + } + }); + // if caseid is not a punishment nor a user, failed + if (!userPunishment || userPunishment.length == 0) return interaction.reply('No punishments found for that case # or User ID'); + const pageNum = interaction.options.getInteger('page') ?? 1; + return interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle(`${user.username}'s punishment history`).setDescription(`**ID:** \`${user.id}\``).setFooter({text: `${userPunishment.length} total punishments. Viewing page ${pageNum} out of ${Math.ceil(userPunishment.length/6)}.`}).addFields(userPunishment.slice((pageNum - 1) * 6, pageNum * 6))]}); + } + } as any)[interaction.options.getSubcommand()](); }, data: new SlashCommandBuilder() .setName('case') diff --git a/src/commands/dev.ts b/src/commands/dev.ts index 839bd7a..edf7e00 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -2,176 +2,173 @@ import Discord,{SlashCommandBuilder} from 'discord.js'; import {Octokit} from '@octokit/rest'; import {exec} from 'node:child_process'; import {readFileSync} from 'node:fs'; -import * as util from 'node:util'; +import util from 'node:util'; import TClient from '../client'; import path from 'node:path'; const removeUsername = (text: string)=>{ - let matchesLeft = true; - const array = text.split('\\'); - while (matchesLeft){ - let usersIndex = array.indexOf('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] = 'Us\u200bers'; - } - } return array.join('\\'); + let matchesLeft = true; + const array = text.split('\\'); + while (matchesLeft){ + let usersIndex = array.indexOf('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] = 'Us\u200bers'; + } + } return array.join('\\'); }; export default { - async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>) { - if (!client.config.eval.whitelist.includes(interaction.user.id)) return client.youNeedRole(interaction, 'bottech'); - ({ - eval: async()=>{ - if (!client.config.eval.allowed) return interaction.reply({content: 'Eval is disabled.', ephemeral: true}); - const code = interaction.options.getString('code') as string; - let output = 'error'; - let error = false; - try { - output = await eval(code); - } catch (err: any) { - error = true - const embed = new client.embed().setColor('#ff0000').setTitle('__Eval__').addFields( - {name: 'Input', value: `\`\`\`js\n${code.slice(0, 1010)}\n\`\`\``}, - {name: 'Output', value: `\`\`\`\n${err}\`\`\``} - ) - interaction.reply({embeds: [embed]}).catch(()=>(interaction.channel as Discord.TextChannel).send({embeds: [embed]})).then(errorEmbedMessage=>{ - 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${removeUsername(err.stack)}\n\`\`\``, allowedMentions: {repliedUser: false}}); - }); - }); - } - if (error) return; - if (typeof output == 'object') { - output = 'js\n'+util.formatWithOptions({depth: 1}, '%O', output) - } else { - output = '\n' + String(output); - } - [client.tokens.token_main,client.tokens.token_beta,client.tokens.token_toast,client.tokens.token_tae,client.tokens.webhook_url].forEach((x)=>{ - const regexp = new RegExp(x as string,'g'); - output = output.replace(regexp, ':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: `\`\`\`${removeUsername(output).slice(0,1016)}\n\`\`\``} - ); - interaction.reply({embeds: [embed]}).catch(()=>(interaction.channel as Discord.TextChannel).send({embeds: [embed]})); - }, - update: async()=>{ - var githubRepo = {owner: 'AnxietyisReal', repo: 'Daggerbot-TS', ref: 'HEAD'} - const octokit = new Octokit({timeZone: 'Australia/NSW', userAgent: 'Daggerbot'}) - const fetchCommitMsg = await octokit.repos.getCommit(githubRepo).then(x=>x.data.commit.message); - const fetchCommitAuthor = await octokit.repos.getCommit(githubRepo).then(x=>x.data.commit.author.name); - const clarkson = await interaction.reply({content: 'Pulling from repository...', fetchReply: true}); - exec('git pull',(err:Error,stdout)=>{ - if (err){ - clarkson.edit(`\`\`\`${removeUsername(err.message)}\`\`\``) - } else if (stdout.includes('Already up to date')){ - clarkson.edit('Bot is already up to date with the repository, did you forgor to push the changes? :skull:') - } else { - setTimeout(()=>{clarkson.edit(`Commit: **${fetchCommitMsg}**\nCommit author: **${fetchCommitAuthor}**\n\nUptime before restarting: **${client.formatTime(client.uptime as number, 3, {commas: true, longNames: true})}**`).then(()=>exec('pm2 restart Daggerbot'))},650) - } - }); - }, - presence: ()=>{ - function convertType(Type?: number){ - switch (Type) { - case 0: return 'Playing'; - case 1: return 'Streaming'; - case 2: return 'Listening to'; - case 3: return 'Watching'; - case 5: return 'Competing in'; - } - }; - const status = interaction.options.getString('status') as Discord.PresenceStatusData | null; - const type = interaction.options.getInteger('type'); - const name = interaction.options.getString('name'); - const url = interaction.options.getString('url'); - const currentActivities = client.config.botPresence.activities as Discord.ActivitiesOptions[]; - - if (status) client.config.botPresence.status = status; - if (type) currentActivities[0].type = type; - if (name) currentActivities[0].name = name; - if (url) currentActivities[0].url = url; - - client.user.setPresence(client.config.botPresence); - interaction.reply([ - 'Presence updated:', - `Status: **${client.config.botPresence.status}**`, - `Type: **${convertType(currentActivities[0].type)}**`, - `Name: **${currentActivities[0].name}**`, - `URL: \`${currentActivities[0].url}\`` - ].join('\n')) - }, - statsgraph: ()=>{ - client.statsGraph = -(interaction.options.getInteger('number', true)); - interaction.reply(`Successfully set to \`${client.statsGraph}\`\n*Total data points: **${JSON.parse(readFileSync(path.join(__dirname, '../database/MPPlayerData.json'), {encoding: 'utf8'})).length.toLocaleString()}***`) - }, - logs: ()=>{ - interaction.deferReply(); - (client.channels.resolve(client.config.mainServer.channels.console) as Discord.TextChannel).send({content: `Uploaded the current console dump as of `, files: [`${process.env.pm2_home}/logs/Daggerbot-out-0.log`, `${process.env.pm2_home}/logs/Daggerbot-error-0.log`]}).then(()=>interaction.editReply('It has been uploaded to dev server.')).catch((e:Error)=>interaction.editReply(`\`${e.message}\``)) - }, - restart: ()=>{ - client.userLevels.forceSave(); - interaction.reply(`Uptime before restarting: **${client.formatTime(client.uptime as number, 3, {commas: true, longNames: true})}**`).then(()=>exec('pm2 restart Daggerbot')) - } - } as any)[interaction.options.getSubcommand()](); - }, + async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>) { + if (!client.config.eval.whitelist.includes(interaction.user.id)) return client.youNeedRole(interaction, 'bottech'); + ({ + eval: async()=>{ + if (!client.config.eval.allowed) return interaction.reply({content: 'Eval is disabled.', ephemeral: true}); + const code = interaction.options.getString('code') as string; + let output = 'error'; + let error = false; + try { + output = await eval(code); + } catch (err: any) { + error = true + const embed = new client.embed().setColor('#ff0000').setTitle('__Eval__').addFields( + {name: 'Input', value: `\`\`\`js\n${code.slice(0, 1010)}\n\`\`\``}, + {name: 'Output', value: `\`\`\`\n${err}\`\`\``} + ) + interaction.reply({embeds: [embed]}).catch(()=>(interaction.channel as Discord.TextChannel).send({embeds: [embed]})).then(errorEmbedMessage=>{ + 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${removeUsername(err.stack)}\n\`\`\``, allowedMentions: {repliedUser: false}}); + }); + }); + } + if (error) return; + if (typeof output == 'object') { + output = 'js\n'+util.formatWithOptions({depth: 1, colors: true}, '%O', output) + } else { + output = '\n' + String(output); + } + [client.tokens.main,client.tokens.beta,client.tokens.toast,client.tokens.tae,client.tokens.webhook_url,client.tokens.webhook_url_test,client.tokens.mongodb_uri,client.tokens.mongodb_uri_dev].forEach((x)=>{ + const regexp = new RegExp(x as string,'g'); + output = output.replace(regexp, ':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: `\`\`\`${removeUsername(output).slice(0,1016)}\n\`\`\``} + ); + interaction.reply({embeds: [embed]}).catch(()=>(interaction.channel as Discord.TextChannel).send({embeds: [embed]})); + }, + update: async()=>{ + var githubRepo = {owner: 'AnxietyisReal', repo: 'Daggerbot-TS', ref: 'HEAD'} + const octokit = new Octokit({timeZone: 'Australia/NSW', userAgent: 'Daggerbot'}) + const fetchCommitMsg = await octokit.repos.getCommit(githubRepo).then(x=>x.data.commit.message); + const fetchCommitAuthor = await octokit.repos.getCommit(githubRepo).then(x=>x.data.commit.author.name); + const clarkson = await interaction.reply({content: 'Pulling from repository...', fetchReply: true}); + exec('git pull',(err:Error,stdout)=>{ + if (err){ + clarkson.edit(`\`\`\`${removeUsername(err.message)}\`\`\``) + } else if (stdout.includes('Already up to date')){ + clarkson.edit('Bot is already up to date with the repository, did you forgor to push the changes? :skull:') + } else { + setTimeout(()=>{clarkson.edit(`Commit: **${fetchCommitMsg}**\nCommit author: **${fetchCommitAuthor}**\n\nUptime before restarting: **${client.formatTime(client.uptime as number, 3, {commas: true, longNames: true})}**`).then(()=>exec('pm2 restart Daggerbot'))},650) + } + }); + }, + presence: ()=>{ + function convertType(Type?: number){ + switch (Type) { + case 0: return 'Playing'; + case 1: return 'Streaming'; + case 2: return 'Listening to'; + case 3: return 'Watching'; + case 5: return 'Competing in'; + } + }; + const status = interaction.options.getString('status') as Discord.PresenceStatusData | null; + const type = interaction.options.getInteger('type'); + const name = interaction.options.getString('name'); + const url = interaction.options.getString('url'); + const currentActivities = client.config.botPresence.activities as Discord.ActivitiesOptions[]; + if (status) client.config.botPresence.status = status; + if (type) currentActivities[0].type = type; + if (name) currentActivities[0].name = name; + if (url) currentActivities[0].url = url; + client.user.setPresence(client.config.botPresence); + interaction.reply([ + 'Presence updated:', + `Status: **${client.config.botPresence.status}**`, + `Type: **${convertType(currentActivities[0].type)}**`, + `Name: **${currentActivities[0].name}**`, + `URL: \`${currentActivities[0].url}\`` + ].join('\n')) + }, + statsgraph: ()=>{ + client.statsGraph = -(interaction.options.getInteger('number', true)); + interaction.reply(`Successfully set to \`${client.statsGraph}\`\n*Total data points: **${JSON.parse(readFileSync(path.join(__dirname, '../database/MPPlayerData.json'), {encoding: 'utf8'})).length.toLocaleString()}***`) + }, + logs: ()=>{ + interaction.deferReply(); + (client.channels.resolve(client.config.mainServer.channels.console) as Discord.TextChannel).send({content: `Uploaded the current console dump as of `, files: [`${process.env.pm2_home}/logs/Daggerbot-out-0.log`, `${process.env.pm2_home}/logs/Daggerbot-error-0.log`]}).then(()=>interaction.editReply('It has been uploaded to dev server.')).catch((e:Error)=>interaction.editReply(`\`${e.message}\``)) + }, + restart: ()=>{ + interaction.reply(`Uptime before restarting: **${client.formatTime(client.uptime as number, 3, {commas: true, longNames: true})}**`).then(()=>exec('pm2 restart Daggerbot')) + } + } as any)[interaction.options.getSubcommand()](); + }, data: new SlashCommandBuilder() - .setName('dev') - .setDescription('Developer commands') - .addSubcommand((optt)=>optt - .setName('eval') - .setDescription('Execute the code to the bot') - .addStringOption((opt)=>opt - .setName('code') - .setDescription('Execute your code') - .setRequired(true))) - .addSubcommand((optt)=>optt - .setName('logs') - .setDescription('Retrieve the logs from host and sends it to dev server')) - .addSubcommand((optt)=>optt - .setName('restart') - .setDescription('Restart the bot for technical reasons')) - .addSubcommand((optt)=>optt - .setName('update') - .setDescription('Pull from repository and restart')) - .addSubcommand((optt)=>optt - .setName('statsgraph') - .setDescription('Edit the number of data points to pull') - .addIntegerOption((hiTae)=>hiTae - .setName('number') - .setDescription('Number of data points to pull') - .setRequired(true))) - .addSubcommand((optt)=>optt - .setName('presence') - .setDescription('Update the bot\'s presence') - .addIntegerOption((hiTae)=>hiTae - .setName('type') - .setDescription('Set an activity type') - .addChoices( - {name: 'Playing', value: Discord.ActivityType.Playing}, - {name: 'Streaming', value: Discord.ActivityType.Streaming}, - {name: 'Listening to', value: Discord.ActivityType.Listening}, - {name: 'Watching', value: Discord.ActivityType.Watching}, - {name: 'Competing in', value: Discord.ActivityType.Competing} - )) - .addStringOption((hiAgain)=>hiAgain - .setName('name') - .setDescription('Set a message for the activity status')) - .addStringOption((hiAgainx2)=>hiAgainx2 - .setName('url') - .setDescription('Set an url for streaming status')) - .addStringOption((hiAgainx3)=>hiAgainx3 - .setName('status') - .setDescription('Set a status indicator for the bot') - .setChoices( - {name: 'Online', value: Discord.PresenceUpdateStatus.Online}, - {name: 'Idle', value: Discord.PresenceUpdateStatus.Idle}, - {name: 'Do Not Distrub', value: Discord.PresenceUpdateStatus.DoNotDisturb}, - {name: 'Invisible', value: Discord.PresenceUpdateStatus.Offline} - ))) + .setName('dev') + .setDescription('Developer commands') + .addSubcommand((optt)=>optt + .setName('eval') + .setDescription('Execute the code to the bot') + .addStringOption((opt)=>opt + .setName('code') + .setDescription('Execute your code') + .setRequired(true))) + .addSubcommand((optt)=>optt + .setName('logs') + .setDescription('Retrieve the logs from host and sends it to dev server')) + .addSubcommand((optt)=>optt + .setName('restart') + .setDescription('Restart the bot for technical reasons')) + .addSubcommand((optt)=>optt + .setName('update') + .setDescription('Pull from repository and restart')) + .addSubcommand((optt)=>optt + .setName('statsgraph') + .setDescription('Edit the number of data points to pull') + .addIntegerOption((hiTae)=>hiTae + .setName('number') + .setDescription('Number of data points to pull') + .setRequired(true))) + .addSubcommand((optt)=>optt + .setName('presence') + .setDescription('Update the bot\'s presence') + .addIntegerOption((hiTae)=>hiTae + .setName('type') + .setDescription('Set an activity type') + .addChoices( + {name: 'Playing', value: Discord.ActivityType.Playing}, + {name: 'Streaming', value: Discord.ActivityType.Streaming}, + {name: 'Listening to', value: Discord.ActivityType.Listening}, + {name: 'Watching', value: Discord.ActivityType.Watching}, + {name: 'Competing in', value: Discord.ActivityType.Competing} + )) + .addStringOption((hiAgain)=>hiAgain + .setName('name') + .setDescription('Set a message for the activity status')) + .addStringOption((hiAgainx2)=>hiAgainx2 + .setName('url') + .setDescription('Set an url for streaming status')) + .addStringOption((hiAgainx3)=>hiAgainx3 + .setName('status') + .setDescription('Set a status indicator for the bot') + .setChoices( + {name: 'Online', value: Discord.PresenceUpdateStatus.Online}, + {name: 'Idle', value: Discord.PresenceUpdateStatus.Idle}, + {name: 'Do Not Distrub', value: Discord.PresenceUpdateStatus.DoNotDisturb}, + {name: 'Invisible', value: Discord.PresenceUpdateStatus.Offline} + ))) } diff --git a/src/commands/faq.ts b/src/commands/faq.ts index 298a58a..db30ea0 100644 --- a/src/commands/faq.ts +++ b/src/commands/faq.ts @@ -1,31 +1,29 @@ import Discord,{SlashCommandBuilder} from 'discord.js'; import TClient from 'src/client'; export default { - async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ - ({ - srp: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('When will SRP (Survival Roleplay) return?').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1074260301756780544/image.png')]}), - dlskin: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Daggerwin Logistics hex code').setDescription('The main color will be Onyx (`#353839`) with red bumpers.').setImage('https://cdn.discordapp.com/attachments/801965516947324969/806871878736019456/image0.png')]}), - vtcR: ()=>interaction.reply(`You can get the <@&${client.config.mainServer.roles.vtcmember}> role from <#802283932430106624> by reacting <@282859044593598464>'s message with :truck:\n*VTC skin can also be found in <#801975222609641472> as well.*`), - mpR: ()=>interaction.reply(`You can get the <@&${client.config.mainServer.roles.mpplayer}> role from <#802283932430106624> by reacting <@282859044593598464>'s message with :tractor:`), - ytscam: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Scammers in YouTube comments section').setDescription('If you ever see a comment mentioning a giveaway or anything else, **it\'s a scam!**\nYou should report it to YouTube and move on or ignore it.\nP.S: They\'re on every channel and not just Daggerwin.').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1068078284996345916/image.png')]}), - fsShader: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Clearing your shader cache folder').setDescription('If your game kees crashing shortly after opening your game, then the shaders might be an issue.\nTo resolve this, you can go to `Documents/My Games/FarmingSimulator2022` and delete the folder called `shader_cache`').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1015195687970943016/unknown.png')]}), - fsLogfile: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Uploading your log file').setDescription('You can find `log.txt` in `Documents/My Games/FarmingSimulator2022` and upload it into <#596989522395398144> along with your issue, so people can assist you further and help you resolve.').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1015195643528101958/unknown.png')]}) - } as any)[interaction.options.getString('question', true)](); - }, - data: new SlashCommandBuilder() - .setName('faq') - .setDescription('List of questions, e.g; log file for FS, YT Scams and etc.') - .addStringOption((opt)=>opt - .setName('question') - .setDescription('What question do you want answered?') - .setRequired(true) - .addChoices( - { name: 'Survival Roleplay', value: 'srp' }, - { name: 'Daggerwin Logistics hex code', value: 'dlskin' }, - { name: 'Scams in YT comments', value: 'ytscam' }, - { name: 'VTC Role', value: 'vtcR' }, - { name: 'MP Role', value: 'mpR' }, - { name: '[FS22] Resolve shader_cache issue', value: 'fsShader' }, - { name: '[FS22] Log file location', value: 'fsLogfile' } - )) + async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ + ({ + dlskin: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Daggerwin Logistics hex code').setDescription('The main color will be Onyx (`#353839`) with red bumpers.').setImage('https://cdn.discordapp.com/attachments/801965516947324969/806871878736019456/image0.png')]}), + vtcR: ()=>interaction.reply(`You can get the <@&${client.config.mainServer.roles.vtcmember}> role from <#802283932430106624> by reacting <@282859044593598464>'s message with :truck:\n*VTC skin can also be found in <#801975222609641472> as well.*`), + mpR: ()=>interaction.reply(`You can get the <@&${client.config.mainServer.roles.mpplayer}> role from <#802283932430106624> by reacting <@282859044593598464>'s message with :tractor:`), + ytscam: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Scammers in YouTube comments section').setDescription('If you ever see a comment mentioning a giveaway or anything else, **it\'s a scam!**\nYou should report it to YouTube and move on or ignore it.\nP.S: They\'re on every channel and not just Daggerwin.').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1068078284996345916/image.png')]}), + fsShader: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Clearing your shader cache folder').setDescription('If your game kees crashing shortly after opening your game, then the shaders might be an issue.\nTo resolve this, you can go to `Documents/My Games/FarmingSimulator2022` and delete the folder called `shader_cache`').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1015195687970943016/unknown.png')]}), + fsLogfile: ()=>interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Uploading your log file').setDescription('You can find `log.txt` in `Documents/My Games/FarmingSimulator2022` and upload it into <#596989522395398144> along with your issue, so people can assist you further and help you resolve.').setImage('https://cdn.discordapp.com/attachments/1015195575693627442/1015195643528101958/unknown.png')]}) + } as any)[interaction.options.getString('question', true)](); + }, + data: new SlashCommandBuilder() + .setName('faq') + .setDescription('List of questions, e.g; log file for FS, YT Scams and etc.') + .addStringOption((opt)=>opt + .setName('question') + .setDescription('What question do you want answered?') + .setRequired(true) + .addChoices( + { name: 'Daggerwin Logistics hex code', value: 'dlskin' }, + { name: 'Scams in YT comments', value: 'ytscam' }, + { name: 'VTC Role', value: 'vtcR' }, + { name: 'MP Role', value: 'mpR' }, + { name: '[FS22] Resolve shader_cache issue', value: 'fsShader' }, + { name: '[FS22] Log file location', value: 'fsLogfile' } + )) } diff --git a/src/commands/mp.ts b/src/commands/mp.ts index 562109f..a53884d 100644 --- a/src/commands/mp.ts +++ b/src/commands/mp.ts @@ -24,7 +24,7 @@ async function MPdata(client:TClient, interaction:Discord.ChatInputCommandIntera // Blame Nawdic & RedRover92 embed.setTitle('Host is not responding.'); embed.setColor(client.config.embedColorRed); - console.log(`[${client.moment().format('DD/MM/YY HH:mm:ss')}] dag mp fail to fetch, host is not responding.`); + console.log(client.logTime, 'dag mp fail to fetch, host is not responding.'); return interaction.reply('Server didn\'t respond in time.'); } return FSserver diff --git a/src/commands/rank.ts b/src/commands/rank.ts index b4e7ae3..953b2d9 100644 --- a/src/commands/rank.ts +++ b/src/commands/rank.ts @@ -1,5 +1,4 @@ import Discord,{SlashCommandBuilder} from 'discord.js'; -import {UserLevels} from 'src/typings/interfaces'; import TClient from 'src/client'; import path from 'node:path'; import fs from 'node:fs'; @@ -7,31 +6,32 @@ import canvas from 'canvas'; export default { async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ if (interaction.guildId !== client.config.mainServer.id) return interaction.reply({content: 'This command doesn\'t work in this server.', ephemeral: true}); + const allData = await client.userLevels._content.find({}); ({ - view: ()=>{ + view: async()=>{ // fetch user or user interaction sender const member = interaction.options.getMember("member") ?? interaction.member as Discord.GuildMember; if (member.user.bot) return interaction.reply('Bots don\'t level up, try viewing non-bots instead.') // information about users progress on level roles - const information = client.userLevels._content[member.user.id]; + const userData = await client.userLevels._content.findById(member.user.id); const pronounBool = (you: string, they: string) => { // takes 2 words and chooses which to use based on if user did this command on themself if (interaction.user.id === member.user.id) return you || true; else return they || false; }; - if (!information) return interaction.reply(`${pronounBool('You', 'They')} currently don't have a level, send some messages to level up.`) + if (!userData) return interaction.reply(`${pronounBool('You', 'They')} currently don't have a level, send some messages to level up.`) - const index = Object.entries(client.userLevels._content).sort((a, b) => b[1].messages - a[1].messages).map(x => x[0]).indexOf(member.id) + 1; - const memberDifference = information.messages - client.userLevels.algorithm(information.level); - const levelDifference = client.userLevels.algorithm(information.level+1) - client.userLevels.algorithm(information.level); - interaction.reply({embeds: [new client.embed().setColor(member.displayColor).setTitle(`Level: **${information.level}**\nRank: **${index ? '#' + index : 'last'}**\nProgress: **${information.messages - client.userLevels.algorithm(information.level)}/${client.userLevels.algorithm(information.level+1) - client.userLevels.algorithm(information.level)} (${(memberDifference/levelDifference*100).toFixed(2)}%)**\nTotal: **${information.messages}**`).setThumbnail(member.user.avatarURL({ extension: 'png', size: 256}) || member.user.defaultAvatarURL)]}) + const index = allData.sort((a, b) => b.messages - a.messages).map(x => x._id).indexOf(member.id) + 1; + const memberDifference = userData.messages - client.userLevels.algorithm(userData.level); + const levelDifference = client.userLevels.algorithm(userData.level+1) - client.userLevels.algorithm(userData.level); + interaction.reply({embeds: [new client.embed().setColor(member.displayColor).setTitle(`Level: **${userData.level}**\nRank: **${index ? '#' + index : 'last'}**\nProgress: **${memberDifference}/${levelDifference} (${(memberDifference/levelDifference*100).toFixed(2)}%)**\nTotal: **${userData.messages}**`).setThumbnail(member.user.avatarURL({ extension: 'png', size: 256}) || member.user.defaultAvatarURL)]}) }, leaderboard: ()=>{ - const messageCountsTotal = Object.values(client.userLevels._content).reduce((a, b) => a + b.messages, 0); + const messageCountsTotal = allData.reduce((a, b) => a + b.messages, 0); const timeActive = Math.floor((Date.now() - client.config.LRSstart)/1000/60/60/24); const dailyMsgsPath = path.join(__dirname, '../database/dailyMsgs.json'); - const data = JSON.parse(fs.readFileSync(dailyMsgsPath, {encoding: 'utf8'})).map((x: Array, i: number, a: any) => { + const data = JSON.parse(fs.readFileSync(dailyMsgsPath, 'utf8')).map((x: Array, i: number, a: any) => { const yesterday = a[i - 1] || []; return x[1] - (yesterday[1] || x[1]); }).slice(1).slice(-60); @@ -147,12 +147,15 @@ export default { const ty = graphOrigin[1] + graphSize[1] + (textSize); ctx.fillText('time ->', tx, ty); - const yeahok = new client.attachmentBuilder(img.toBuffer(), {name: 'dailymsgs.png'}) + const topUsers = allData.sort((a,b)=>b.messages - a.messages).slice(0,10).map((x,i)=>`\`${i+1}.\` <@${x._id}>: ${x.messages.toLocaleString('en-US')}`).join('\n'); + + const graphImage = new client.attachmentBuilder(img.toBuffer(), {name: 'dailymsgs.png'}) const embed = new client.embed().setTitle('Ranking leaderboard') - .setDescription(`Level System was created **${timeActive}** days ago. Since then, a total of **${messageCountsTotal.toLocaleString('en-US')}** messages have been sent in this server.\nGraph updates daily @ `) - .addFields({name: 'Top users by messages sent:', value: Object.entries(client.userLevels._content).sort((a, b) => b[1].messages - a[1].messages).slice(0, 10).map((x, i) => `\`${i + 1}.\` <@${x[0]}>: ${x[1].messages.toLocaleString('en-US')}`).join('\n')}) - .setImage('attachment://dailymsgs.png').setColor(client.config.embedColor) - interaction.reply({embeds: [embed], files: [yeahok]}) + .setDescription(`Level System was created **${timeActive}** days ago. Since then, a total of **${messageCountsTotal.toLocaleString('en-US')}** messages have been sent in this server.`) + .addFields({name: 'Top users by messages sent:', value: topUsers}) + .setImage('attachment://dailymsgs.png').setColor(client.config.embedColor) + .setFooter({text: 'Graph updates daily.'}) + interaction.reply({embeds: [embed], files: [graphImage]}) } } as any)[interaction.options.getSubcommand()](); }, diff --git a/src/commands/unpunish.ts b/src/commands/unpunish.ts index cd7e4a7..cd26cbb 100644 --- a/src/commands/unpunish.ts +++ b/src/commands/unpunish.ts @@ -1,17 +1,22 @@ import Discord,{SlashCommandBuilder} from 'discord.js'; import TClient from 'src/client'; export default { - async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ - client.unPunish(client, interaction) - }, - data: new SlashCommandBuilder() - .setName('unpunish') - .setDescription('Remove the active punishment from a member') - .addIntegerOption((opt)=>opt - .setName('case_id') - .setDescription('Case # of the punishment to be overwritten') - .setRequired(true)) - .addStringOption((opt)=>opt - .setName('reason') - .setDescription('Reason for removing the punishment')) + async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ + if (!client.isStaff(interaction.member as Discord.GuildMember)) return client.youNeedRole(interaction, 'dcmod'); + const punishment = (await client.punishments._content.find({})).find(x=>x._id === interaction.options.getInteger('case_id', true)); + if (!punishment) return interaction.reply({content: 'Invalid Case ID', ephemeral: true}); + if (punishment.expired) return interaction.reply('This case has been overwritten by another case.'); + const reason = interaction.options.getString('reason') ?? 'Reason unspecified'; + await client.punishments.removePunishment(punishment.id, interaction.user.id, reason, interaction); + }, + data: new SlashCommandBuilder() + .setName('unpunish') + .setDescription('Remove the active punishment from a member') + .addIntegerOption((opt)=>opt + .setName('case_id') + .setDescription('Case # of the punishment to be overwritten') + .setRequired(true)) + .addStringOption((opt)=>opt + .setName('reason') + .setDescription('Reason for removing the punishment')) } \ No newline at end of file diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index d62f1b2..ff0deef 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -4,12 +4,14 @@ export default { async run(client:TClient, member:Discord.GuildMember){ if (!client.config.botSwitches.logs) return; if (!member.joinedTimestamp || member.guild?.id != client.config.mainServer.id) return; - (client.channels.resolve(client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds: [new client.embed().setColor(client.config.embedColorRed).setTimestamp().setThumbnail(member.user.displayAvatarURL({size: 2048}) as string).setTitle(`Member Left: ${member.user.tag}`).setDescription(`<@${member.user.id}>\n\`${member.user.id}\``).addFields( + const levelData = await client.userLevels._content.findById(member.id); + const embed = new client.embed().setColor(client.config.embedColorRed).setTimestamp().setThumbnail(member.user.displayAvatarURL({size: 2048}) as string).setTitle(`Member Left: ${member.user.tag}`).setDescription(`<@${member.user.id}>\n\`${member.user.id}\``).addFields( {name: '🔹 Account Creation Date', value: `\n`}, {name: '🔹 Server Join Date', value: `\n`}, - {name: `🔹 Roles: ${member.roles.cache.size - 1}`, value: `${member.roles.cache.size > 1 ? member.roles.cache.filter((x)=>x.id !== member.guild.roles.everyone.id).sort((a,b)=>b.position - a.position).map(x=>x).join(member.roles.cache.size > 4 ? ' ' : '\n').slice(0,1024) : 'No roles'}`, inline: true}, - {name: '🔹 Level messages', value: `${client.userLevels._content[member.user.id]?.messages.toLocaleString('en-US') || 0}`, inline: true} - )]}); - delete client.userLevels._content[member.user.id]; + {name: `🔹 Roles: ${member.roles.cache.size - 1}`, value: `${member.roles.cache.size > 1 ? member.roles.cache.filter((x)=>x.id !== member.guild.roles.everyone.id).sort((a,b)=>b.position - a.position).map(x=>x).join(member.roles.cache.size > 4 ? ' ' : '\n').slice(0,1024) : 'No roles'}`, inline: true} + ); + if (levelData && levelData.messages > 1) embed.addFields({name: '🔹 Total messages', value: levelData.messages.toLocaleString('en-US'), inline: true}); + (client.channels.resolve(client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds:[embed]}); + await client.userLevels._content.findByIdAndDelete(member.id) } } diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 5600194..dbe0611 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -5,7 +5,7 @@ export default { if (!interaction.inGuild() || !interaction.inCachedGuild()) return; if (interaction.isChatInputCommand()){ const commandFile = client.commands.get(interaction.commandName); - console.log(`[${client.moment().format('DD/MM/YY HH:mm:ss')}] ${interaction.user.tag} used /${interaction.commandName} ${interaction.options.getSubcommand(false) ?? ''} in #${interaction.channel.name}`); + console.log(client.logTime(), `${interaction.user.tag} used /${interaction.commandName} ${interaction.options.getSubcommand(false) ?? ''} in #${interaction.channel.name}`); if (!client.config.botSwitches.commands && !client.config.eval.whitelist.includes(interaction.user.id)) return interaction.reply({content: 'Bot is currently being run in development mode.', ephemeral: true}); if (commandFile){ try{ diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index c2fa1a6..aba7132 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -14,7 +14,7 @@ export default { // Arrary of channel ids for automod to be disabled in ] - if (client.bannedWords._content.some((x)=>msgarr.includes(x)) && !message.member.roles.cache.has(client.config.mainServer.roles.dcmod) && message.guildId == client.config.mainServer.id && !Whitelist.includes(message.channelId) && client.config.botSwitches.automod){ + if (await client.bannedWords._content.findOne({_id:msgarr}) && !message.member.roles.cache.has(client.config.mainServer.roles.dcmod) && message.guildId == client.config.mainServer.id && !Whitelist.includes(message.channelId) && client.config.botSwitches.automod){ automodded = true; const threshold = 30000; message.delete().catch(err=>console.log('bannedWords automod; msg got possibly deleted by another bot.')) diff --git a/src/events/messageUpdate.ts b/src/events/messageUpdate.ts index 14d4fdf..8593d70 100644 --- a/src/events/messageUpdate.ts +++ b/src/events/messageUpdate.ts @@ -6,7 +6,7 @@ export default { const disabledChannels = ['548032776830582794', '541677709487505408'] if (oldMsg.guild?.id != client.config.mainServer.id || oldMsg.author == null || oldMsg?.author.bot || oldMsg.partial || newMsg.partial || !newMsg.member || disabledChannels.includes(newMsg.channelId)) return; const msgarr = newMsg.content.toLowerCase().split(' '); - if (client.bannedWords._content.some((word:string)=>msgarr.includes(word)) && (!client.isStaff(newMsg.member))) newMsg.delete(); + if (await client.bannedWords._content.findOne({_id:msgarr}) && (!client.isStaff(newMsg.member))) newMsg.delete(); if (newMsg.content === oldMsg.content) return; (client.channels.resolve(client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds: [new client.embed().setColor(client.config.embedColor).setTimestamp().setAuthor({name: `Author: ${oldMsg.author.tag} (${oldMsg.author.id})`, iconURL: `${oldMsg.author.displayAvatarURL()}`}).setTitle('Message edited').setDescription(`<@${oldMsg.author.id}>\nOld content:\n\`\`\`\n${oldMsg.content}\n\`\`\`\nNew content:\n\`\`\`\n${newMsg.content}\`\`\`\nChannel: <#${oldMsg.channelId}>`)], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setStyle(5).setURL(`${oldMsg.url}`).setLabel('Jump to message'))]}); } diff --git a/src/events/roleUpdate.ts b/src/events/roleUpdate.ts deleted file mode 100644 index 26091b4..0000000 --- a/src/events/roleUpdate.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Discord, { AuditLogEvent } from 'discord.js'; -import TClient from '../client'; -export default { - async run(client:TClient, oldRole:Discord.Role, newRole:Discord.Role){ - const fetchRoleUpdoot = await client.guilds.cache.get(oldRole.guild.id).fetchAuditLogs({ - limit: 1, - type: AuditLogEvent.RoleUpdate - }) - if (oldRole.guild?.id != client.config.mainServer.id) return; - const roleLog = fetchRoleUpdoot.entries.first(); - if (!roleLog) return - const {executor, target} = roleLog; - if (target) { - const embed = new client.embed().setColor(newRole.hexColor).setThumbnail(newRole?.iconURL()).setTimestamp().setTitle(`Role modified: ${newRole.name}`).setDescription(`🔹 **Role**\n${target}\n\`${target.id}\``).addFields( - {name: `${executor.bot ? '🔹 Bot' : '🔹 Admin'}`, value: `<@${executor.id}>\n\`${executor.id}\``} - ); - (client.channels.resolve(client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds: [embed]}) - // Moved addFields to these below cuz yes for each role changes, it seems inefficent to me but it will do. :) - // Permissions field seems to trigger when role is hoisted/unhoisted atleast to me. - if (oldRole.hexColor !== newRole.hexColor) { - embed.addFields({name: '🔹 Role changes', value: `**Old color:** ${oldRole.hexColor}\n**New color:** ${newRole.hexColor}`}) - } else if (oldRole.name !== newRole.name) { - embed.addFields({name: '🔹 Role changes', value: `**Old name:** ${oldRole.name}\n**New name:** ${newRole.name}`}) - } else if (!oldRole.permissions.equals(newRole.permissions)) { - embed.addFields({name: '🔹 Role changes', value: `**Old permission(s):** ${newRole.permissions.missing(oldRole.permissions).join(', ')}\n**New permission(s):** ${oldRole.permissions.missing(newRole.permissions).join(', ')}`}) - } - } else { - console.log(`${target.id} was modified from ${client.guilds.cache.get(oldRole.guild.name)} but no audit log could be fetched.`) - } - } -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 24eb3b9..b5ce366 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ const client = new TClient; client.init(); import fs from 'node:fs'; import MPDB from './models/MPServer'; -import {Punishment, UserLevels, FSData, FSCareerSavegame} from './typings/interfaces'; +import {FSData, FSCareerSavegame} from './typings/interfaces'; client.on('ready', async()=>{ setInterval(()=>client.user.setPresence(client.config.botPresence), 300000); @@ -27,7 +27,8 @@ client.on('ready', async()=>{ function DZ(error:Error, location:string){// Yes, I may have shiternet but I don't need to wake up to like a hundred messages or so. if (['getaddrinfo ENOTFOUND discord.com'].includes(error.message)) return; console.log(error); - (client.channels.resolve(client.config.mainServer.channels.errors) as Discord.TextChannel).send({embeds: [new client.embed().setColor('#420420').setTitle('Error caught!').setFooter({text: location}).setDescription(`**Error:** \`${error.message}\`\n\n**Stack:** \`${`${error.stack}`.slice(0, 2500)}\``)]}) + const channel = client.channels.resolve(client.config.mainServer.channels.errors) as Discord.TextChannel | null + channel?.send({embeds: [new client.embed().setColor('#420420').setTitle('Error caught!').setFooter({text: location}).setDescription(`**Error:** \`${error.message}\`\n\n**Stack:** \`${`${error.stack}`.slice(0, 2500)}\``)]}) } process.on('unhandledRejection', async(error: Error)=>DZ(error, 'unhandledRejection')); process.on('uncaughtException', async(error: Error)=>DZ(error, 'uncaughtException')); @@ -85,11 +86,11 @@ setInterval(async()=>{ }).catch((error)=>console.log(error)) if (FSdss.fetchResult.length != 0){ error = true; - console.log(`[${client.moment().format('DD/MM/YY HH:mm:ss')}]`, FSdss.fetchResult); + console.log(client.logTime(), FSdss.fetchResult); } if (FScsg.fetchResult.length != 0){ error = true; - console.log(`[${client.moment().format('DD/MM/YY HH:mm:ss')}]`, FScsg.fetchResult); + console.log(client.logTime(), FScsg.fetchResult); } if (error) { // Blame RedRover and Nawdic embed.setTitle('Host is not responding').setColor(client.config.embedColorRed); @@ -140,23 +141,24 @@ setInterval(async()=>{ const now = Date.now(); const lrsStart = client.config.LRSstart; - client.punishments._content.filter((x:Punishment)=>x.endTime<= now && !x.expired).forEach(async (punishment:Punishment)=>{ - console.log(`[${client.moment().format('DD/MM/YY HH:mm:ss')}] ` + `${punishment.member}\'s ${punishment.type} should expire now`); - const unpunishResult = await client.punishments.removePunishment(punishment.id, client.user.id, 'Time\'s up!'); - console.log(`[${client.moment().format('DD/MM/YY HH:mm:ss')}] ` + unpunishResult); + const punishments = await client.punishments._content.find({}); + punishments.filter(x=>x.endTime && x.endTime<= now && !x.expired).forEach(async punishment=>{ + console.log(client.logTime(), `${punishment.member}\'s ${punishment.type} should expire now`); + const unpunishResult = await client.punishments.removePunishment(punishment._id, client.user.id, 'Time\'s up!'); + console.log(client.logTime(), unpunishResult); }); const formattedDate = Math.floor((now - lrsStart)/1000/60/60/24); const dailyMsgs = JSON.parse(fs.readFileSync(__dirname + '/database/dailyMsgs.json', {encoding: 'utf8'})) if (!dailyMsgs.some((x:Array)=>x[0] === formattedDate)){ - let total = Object.values(client.userLevels._content).reduce((a,b)=>a + b.messages, 0); // sum of all users + let total = (await client.userLevels._content.find({})).reduce((a,b)=>a + b.messages, 0); // sum of all users const yesterday = dailyMsgs.find((x:Array)=>x[0] === formattedDate - 1); if (total < yesterday){ // messages went down. total = yesterday } dailyMsgs.push([formattedDate, total]); fs.writeFileSync(__dirname + '/database/dailyMsgs.json', JSON.stringify(dailyMsgs)) - console.log(`[${client.moment().format('DD/MM/YY HH:mm:ss')}]`, `Pushed [${formattedDate}, ${total}] to dailyMsgs`); + console.log(client.logTime(), `Pushed [${formattedDate}, ${total}] to dailyMsgs`); client.guilds.cache.get(client.config.mainServer.id).commands.fetch().then((commands)=>(client.channels.resolve(client.config.mainServer.channels.logs) as Discord.TextChannel).send(`:pencil: Pushed \`[${formattedDate}, ${total}]\` to x.name == 'rank').id}>`)) } }, 5000) diff --git a/src/models/bannedWords.ts b/src/models/bannedWords.ts new file mode 100644 index 0000000..8513dae --- /dev/null +++ b/src/models/bannedWords.ts @@ -0,0 +1,17 @@ +import Discord from 'discord.js'; +import TClient from 'src/client'; +import mongoose from 'mongoose'; + +const Schema = mongoose.model('bannedWords', new mongoose.Schema({ + _id: {type: String, required:true} +})); + +export default class bannedWords extends Schema { + client: TClient; + _content: typeof Schema; + constructor(client:TClient){ + super(); + this.client = client; + this._content = Schema; + } +} diff --git a/src/models/bonkCount.ts b/src/models/bonkCount.ts new file mode 100644 index 0000000..bb987b4 --- /dev/null +++ b/src/models/bonkCount.ts @@ -0,0 +1,23 @@ +import TClient from 'src/client'; +import mongoose from 'mongoose'; + +const Schema = mongoose.model('bonkCount', new mongoose.Schema({ + _id: {type: String, required:true}, + value: {type: Number, required:true} +})); + +export default class bonkCount extends Schema { + client: TClient; + _content: typeof Schema; + constructor(client:TClient){ + super(); + this.client = client; + this._content = Schema; + } + async _incrementUser(userid: string){ + const amount = await this._content.findById(userid) + if (amount) await this._content.findByIdAndUpdate(userid, {value: amount.value + 1}) + else await this._content.create({_id: userid, value: 1}) + return this; + } +} diff --git a/src/models/punishments.ts b/src/models/punishments.ts new file mode 100644 index 0000000..302bc7a --- /dev/null +++ b/src/models/punishments.ts @@ -0,0 +1,151 @@ +import Discord from 'discord.js'; +import TClient from 'src/client'; +import mongoose from 'mongoose'; +import ms from 'ms'; +import {Punishment} from 'src/typings/interfaces'; + +const Schema = mongoose.model('punishments', new mongoose.Schema({ + _id: {type: Number, required: true}, + type: {type: String, required: true}, + member: {type: String, required: true}, + moderator: {type: String, required: true}, + expired: {type: Boolean}, + time: {type: Number, required: true}, + reason: {type: String, required: true}, + endTime: {type: Number}, + cancels: {type: Number}, + duration: {type: Number} +})); + +export default class punishments extends Schema { + client: TClient; + _content: typeof Schema; + constructor(client:TClient){ + super(); + this.client = client; + this._content = Schema; + } + createId = async()=>Math.max(...(await this._content.find({})).map(x=>x.id), 0) + 1; + async makeModlogEntry(punishment:Punishment){ + // Format data into an embed + const embed = new this.client.embed() + .setTitle(`${punishment.type[0].toUpperCase() + punishment.type.slice(1)} | Case #${punishment._id}`) + .addFields( + {name: '🔹 User', value: `<@${punishment.member}>\n\`${punishment.member}\``, inline: true}, + {name: '🔹 Moderator', value: `<@${punishment.moderator}>\n\`${punishment.moderator}\``, inline: true}, + {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.cancels) { + const cancels = await this._content.findById(punishment.cancels); + embed.addFields({name: '🔹 Overwrites', value: `This case overwrites Case #${cancels.id}\n\`${cancels.reason}\``}) + } + // Send it off to specific Discord channel. + (this.client.channels.cache.get(this.client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds:[embed]}); + }// hi tae + getTense(type:string){// Get past tense form of punishment type, grammar yes + return { + ban: 'banned', + softban: 'softbanned', + kick: 'kicked', + mute: 'muted', + warn: 'warned' + }[type] + } + async addPunishment(type:string, options:{time?:string,interaction?:Discord.ChatInputCommandInteraction<'cached'>},moderator:string,reason:string,User:Discord.User,GuildMember?:Discord.GuildMember){ + const {time,interaction} = options; + const now = Date.now(); + const guild = this.client.guilds.cache.get(this.client.config.mainServer.id) as Discord.Guild; + const punData:Punishment={type, _id: await this.createId(), member:User.id, reason, moderator, time:now} + const inOrFromBoolean = ['warn', 'mute'].includes(type) ? 'in' : 'from'; + const auditLogReason = `${reason || 'Reason unspecified'} | Case #${punData._id}`; + const embed = new this.client.embed() + .setColor(this.client.config.embedColor) + .setTitle(`Case #${punData._id}: ${type[0].toUpperCase()+type.slice(1)}`) + .setDescription(`${User.tag}\n<@${User.id}>\n(\`${User.id}\`)`) + .addFields({name: 'Reason', value: reason}) + let punResult; + let timeInMillis; + let DM; + + 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})}` : ''; + if (time) embed.addFields({name: 'Duration', value: durationText}); + + if (GuildMember){ + try{ + DM=await GuildMember.send(`You've been ${this.getTense(type)} ${inOrFromBoolean} ${guild.name}${durationText} for \`${reason}\` (Case #${punData._id})`); + }catch(err){ + embed.setFooter({text: 'Failed to DM a member.'}) + } + } + + if (['ban', 'softban'].includes(type)){ + const banned = await guild.bans.fetch(User.id).catch(()=>undefined); + if (!banned) punResult = await guild.bans.create(User.id, {reason: auditLogReason, deleteMessageSeconds: 172800}).catch((err:Error)=>err.message) + else punResult = 'User is already banned.'; + } + else if (type == 'kick') punResult = await GuildMember?.kick(auditLogReason).catch((err:Error)=>err.message); + else if (type == 'mute') punResult = await GuildMember?.timeout(timeInMillis, auditLogReason).catch((err:Error)=>err.message); + if (type == 'softban' && typeof punResult != 'string') punResult = await guild.bans.remove(User.id, auditLogReason).catch((err:Error)=>err.message); + + if (timeInMillis && ['mute','ban'].includes(type)){ + punData.endTime = now + timeInMillis; + punData.duration = timeInMillis; + } + + if (typeof punResult == 'string'){// Unsuccessful punishment + if (DM) DM.delete(); + if (interaction) return interaction.editReply(punResult); + else return punResult; + } else { + await this.makeModlogEntry(punData); + await this._content.create(punData); + + if (interaction) return interaction.editReply({embeds:[embed]}); + else return punResult; + } + } + async removePunishment(caseId:number,moderator:string,reason:string,interaction?:Discord.ChatInputCommandInteraction<'cached'>){ + const now = Date.now(); + const _id = await this.createId(); + const punishment = await this._content.findById(caseId); + if (!punishment) return 'Punishment not found.'; + const guild = this.client.guilds.cache.get(this.client.config.mainServer.id) as Discord.Guild; + const auditLogReason = `${reason || 'Reason unspecified'} | Case #${punishment.id}`; + const User = await this.client.users.fetch(punishment.member); + const GuildMember = await guild.members.fetch(punishment.member); + + let removePunishmentData:Punishment={type:`un${punishment.type}`, _id, cancels:punishment.id, member:punishment.member, reason, moderator, time:now}; + let removePunishmentResult; + + if (punishment.type == 'ban') removePunishmentResult = guild.bans.remove(punishment.member, auditLogReason).catch((err:Error)=>err.message); + else if (punishment.type == 'mute'){ + if (GuildMember){ + removePunishmentResult = GuildMember.timeout(null, auditLogReason).catch((err:Error)=>err.message); + GuildMember.send(`You've been unmuted in ${guild.name}.`).catch((err:Error)=>console.log(err.message)); + } else await this._content.findByIdAndUpdate(caseId,{expired:true},{new:true}); + } else removePunishmentData.type = 'removeOtherPunishment'; + + if (typeof removePunishmentResult == 'string'){//Unsuccessful punishment + if (interaction) return interaction.reply(removePunishmentResult); + else return removePunishmentResult; + } else { + await this._content.findByIdAndUpdate(caseId,{expired:true},{new:true}); + await this._content.create(removePunishmentData); + await this.makeModlogEntry(removePunishmentData); + + if (interaction) { + return interaction.reply({embeds:[new this.client.embed().setColor(this.client.config.embedColor) + .setTitle(`Case #${removePunishmentData._id}: ${removePunishmentData.type[0].toUpperCase()+removePunishmentData.type.slice(1)}`) + .setDescription(`${User.tag}\n<@${User.id}>\n(\`${User.id}\`)`) + .addFields({name: 'Reason', value: reason},{name: 'Overwrites', value: `Case #${punishment.id}`}) + ]}) + } else return `Successfully ${this.getTense(removePunishmentData.type)} ${User.tag} (\`${User.id}\`) for ${reason}` + } + } +} diff --git a/src/models/userLevels.ts b/src/models/userLevels.ts new file mode 100644 index 0000000..73318de --- /dev/null +++ b/src/models/userLevels.ts @@ -0,0 +1,37 @@ +import Discord from 'discord.js'; +import TClient from 'src/client'; +import mongoose from 'mongoose'; + +const Schema = mongoose.model('userLevels', new mongoose.Schema({ + _id: {type: String}, + messages: {type: Number, required: true}, + level: {type: Number, required: true} +})); + +export default class userLevels extends Schema { + client: TClient; + _content: typeof Schema; + constructor(client:TClient){ + super(); + this.client = client; + this._content = Schema; + } + async incrementUser(userid:string){ + const userData = await this._content.findById(userid) + + if (userData){ + await this._content.findByIdAndUpdate(userid, {messages: userData.messages + 1}); + if (userData.messages >= this.algorithm(userData.level+2)){ + while (userData.messages > this.algorithm(userData.level+1)){ + const newData = await this._content.findByIdAndUpdate(userid, {level:userData.level+1}, {new: true}); + console.log(`${userid} EXTENDED LEVELUP ${newData?.level}`) + } + } else if (userData.messages >= this.algorithm(userData.level+1)) { + const newData = await this._content.findByIdAndUpdate(userid, {level:userData.level+1}, {new: true}); + (this.client.channels.resolve(this.client.config.mainServer.channels.botcommands) as Discord.TextChannel).send({content: `<@${userid}> has reached level **${newData.level}**. GG!`, allowedMentions: {parse: ['users']}}) + } + } else await this._content.create({_id: userid, messages: 1, level: 0}) + } + algorithm = (level:number)=>level*level*15; +// Algorithm for determining levels. If adjusting, recommended to only change the integer at the end of equation. +} diff --git a/src/schoolClassroom.ts b/src/schoolClassroom.ts deleted file mode 100644 index c9723b0..0000000 --- a/src/schoolClassroom.ts +++ /dev/null @@ -1,223 +0,0 @@ -import TClient from './client'; -import Discord from 'discord.js'; -import { Database } from './database'; -import { Punishment, punOpt } from './typings/interfaces'; - -export class bannedWords extends Database { - client: TClient; - constructor(client: TClient){ - super('src/database/bannedWords.json', 'array'); - this.client = client; - } -} -export class bonkCount extends Database { - client: TClient; - constructor(client: TClient){ - super('src/database/bonkCount.json', 'object') - this.client = client - } - _incrementUser(userid: string){ - const amount = this._content[userid]; - if(amount) this._content[userid]++; - else this._content[userid] = 1; - return this; - } - getUser(userid: string){ - return this._content[userid] as number || 0; - } -} -export class userLevels extends Database { - client: TClient; - constructor(client: TClient){ - super('src/database/userLevels.json', 'object'); - this.client = client - } - incrementUser(userid: string){ - const data = this._content[userid];// User's data. Integer for old format, object for new format. - - if (typeof data == 'number'){// If user's data is an integer, convert it into object for new format. - this._content[userid] = {messages: data, level: 0}; - } - - if (data) {// If user exists on file... - this._content[userid].messages++;// Increment their message count - if (data.messages >= this.algorithm(data.level+2)){// Quietly level up users who can surpass more than 2 levels at once, usually due to manually updating their message count - while (data.messages > this.algorithm(data.level+1)){ - this._content[userid].level++; - console.log(`${userid} EXTENDED LEVELUP ${this._content[userid].level}`) - } - } else if (data.messages >= this.algorithm(data.level+1)){// If user's message count meets/exceeds message requirement for next level... - this._content[userid].level++;// Level them up. - (this.client.channels.resolve(this.client.config.mainServer.channels.botcommands) as Discord.TextChannel).send({content: `<@${userid}> has reached level **${data.level}**. GG!`, allowedMentions: {parse: ['users']}}) - } - } else {// If user doesn't exist on file, create an object for it. - this._content[userid] = {messages: 1, level: 0}; - } - } - algorithm(level: number){// Algorithm for determining levels. If adjusting, recommended to only change the integer at the end of equation. - return level*level*15; - } -} -export class punishments extends Database { - client: TClient; - constructor(client: TClient){ - super('src/database/punishments.json', 'array'); - this.client = client; - } - createId(){ - return Math.max(...this.client.punishments._content.map((x:Punishment)=>x.id), 0)+1; - } - makeModlogEntry(data: Punishment) { - const cancels = data.cancels ? this.client.punishments._content.find((x: Punishment) => x.id === data.cancels) : null; - const channelId = ['kick', 'ban'].includes(data.type) ? '1048341961901363352' : this.client.config.mainServer.channels.logs; - - // format data into embed - const embed = new this.client.embed() - .setTitle(`${this.client.formatPunishmentType(data, this.client, cancels)} | Case #${data.id}`) - .addFields( - {name: '🔹 User', value: `<@${data.member}> \`${data.member}\``, inline: true}, - {name: '🔹 Moderator', value: `<@${data.moderator}> \`${data.moderator}\``, inline: true}, - {name: '\u200b', value: '\u200b', inline: true}, - {name: '🔹 Reason', value: `\`${data.reason}\``, inline: true}) - .setColor(this.client.config.embedColor) - .setTimestamp(data.time) - if (data.duration) { - embed.addFields( - {name: '🔹 Duration', value: this.client.formatTime(data.duration, 100), inline: true}, - {name: '\u200b', value: '\u200b', inline: true} - ) - } - if (data.cancels) embed.addFields({name: '🔹 Overwrites', value: `This case overwrites Case #${cancels.id}\n\`${cancels.reason}\``}); - - // send embed in modlog channel - (this.client.channels.resolve(channelId) as Discord.TextChannel).send({embeds: [embed]}); - }; - getTense(type: string) { // Get past tense form of punishment type, grammar yes - switch (type) { - case 'ban': return 'banned'; - case 'softban': return 'softbanned'; - case 'kick': return 'kicked'; - case 'mute': return 'muted'; - case 'warn': return 'warned'; - } - } - async addPunishment(type: string, options: punOpt, moderator: string, reason: string, User: Discord.User, GuildMember?: Discord.GuildMember) { - const { time, interaction } = options; - const ms = require('ms'); - const now = Date.now(); - const guild = this.client.guilds.cache.get(this.client.config.mainServer.id) as Discord.Guild; - const punData: Punishment = { type, id: this.createId(), member: User.id, reason, moderator, time: now } - const embed = new this.client.embed() - .setColor(this.client.config.embedColor) - .setTitle(`Case #${punData.id}: ${type[0].toUpperCase() + type.slice(1)}`) - .setDescription(`${User.tag}\n<@${User.id}>\n(\`${User.id}\`)`) - .addFields({name: 'Reason', value: reason}) - let punResult: any; - let timeInMillis: number; - let DM: Discord.Message | undefined; - - if (type == "mute") { - timeInMillis = time ? ms(time) : 2419140000; // Timeouts have a limit of 4 weeks - } else { - timeInMillis = time ? ms(time) : null; - } - - // Add field for duration if time is specified - if (time) embed.addFields({name: 'Duration', value: `${timeInMillis ? `for ${this.client.formatTime(timeInMillis, 4, { longNames: true, commas: true })}` : "forever"}`}) - - if (GuildMember) { - try { - DM = await GuildMember.send(`You've been ${this.getTense(type)} ${['warn', 'mute'].includes(type) ? 'in' : 'from'} ${guild.name}${time ? (timeInMillis ? ` for ${this.client.formatTime(timeInMillis, 4, { longNames: true, commas: true })}` : 'forever') : ''} for reason \`${reason}\` (Case #${punData.id})`); - } catch (err: any) { - embed.setFooter({text: 'Failed to DM member of punishment'}); - } - } - - if (['ban', 'softban'].includes(type)) { - const banned = await guild.bans.fetch(User.id).catch(() => undefined); - if (!banned) { - punResult = await guild.bans.create(User.id, {reason: `${reason} | Case #${punData.id}`, deleteMessageSeconds: 172800}).catch((err: Error) => err.message); - } else { - punResult = 'User is already banned.'; - } - } else if (type == 'kick') { - punResult = await GuildMember?.kick(`${reason} | Case #${punData.id}`).catch((err: Error) => err.message); - } else if (type == 'mute') { - punResult = await GuildMember?.timeout(timeInMillis, `${reason} | Case #${punData.id}`).catch((err: Error) => err.message); - } - - if (type == 'softban' && typeof punResult != 'string') { // If type was softban and it was successful, continue with softban (unban) - punResult = await guild.bans.remove(User.id, `${reason} | Case #${punData.id}`).catch((err: Error) => err.message); - } - - if (timeInMillis && ['mute', 'ban'].includes(type)) { // If type is mute or ban, specify duration and endTime - punData.endTime = now + timeInMillis; - punData.duration = timeInMillis; - } - - if (typeof punResult == 'string') { // Punishment was unsuccessful - if (DM) DM.delete(); - if (interaction) { - return interaction.editReply(punResult); - } else { - return punResult; - } - } else { // Punishment was successful - this.makeModlogEntry(punData); - this.client.punishments.addData(punData).forceSave(); - - if (interaction) { - return interaction.editReply({embeds: [embed]}); - } else { - return punResult; - } - } - - } - async removePunishment(caseId:number, moderator:any, reason:string):Promise{ - const now = Date.now() - const punishment = this._content.find((x:Punishment)=>x.id === caseId); - const id = this.createId(); - if (!punishment) return 'Punishment not found'; - if (['ban','mute'].includes(punishment.type)) { - const guild = this.client.guilds.cache.get(this.client.config.mainServer.id) as Discord.Guild; - let removePunishmentResult; - if (punishment.type === 'ban'){ - removePunishmentResult = await guild.members.unban(punishment.member, `${reason || 'Reason unspecified'} | Case #${id}`).catch((err:TypeError)=>err.message); - } else if (punishment.type === 'mute'){ - const member = await guild.members.fetch(punishment.member).catch(err=>undefined); - if (member){ - removePunishmentResult = await member - if (typeof removePunishmentResult !== 'string'){ - member.timeout(null, `${reason || 'Reason unspecified'} | Case #${id}`) - removePunishmentResult.send(`You've been unmuted in ${removePunishmentResult.guild.name}.`); - removePunishmentResult = removePunishmentResult.user; - } - } else { - // user probably left, quietly remove punishment - const removePunishmentData = {type: `un${punishment.type}`, id, cancels: punishment.id, member: punishment.member, reason, moderator, time: now}; - this._content[this._content.findIndex((x:Punishment)=>x.id === punishment.id)].expired = true - this.addData(removePunishmentData).forceSave(); - } - } - if (typeof removePunishmentResult === 'string') return `Un${punishment.type} was unsuccessful: ${removePunishmentResult}`; - else { - const removePunishmentData = {type: `un${punishment.type}`, id, cancels: punishment.id, member: punishment.member, reason, moderator, time: now}; - this.makeModlogEntry(removePunishmentData); - this._content[this._content.findIndex((x:Punishment)=>x.id === punishment.id)].expired = true; - this.addData(removePunishmentData).forceSave(); - return `Successfully ${punishment.type === 'ban' ? 'unbanned' : 'unmuted'} **${removePunishmentResult?.tag}** (${removePunishmentResult?.id}) for reason \`${reason || 'Reason unspecified'}\`` - } - } else { - try { - const removePunishmentData = {type: 'removeOtherPunishment', id, cancels: punishment.id, member: punishment.member, reason, moderator, time: now}; - this.makeModlogEntry(removePunishmentData); - this._content[this._content.findIndex((x:Punishment)=>x.id === punishment.id)].expired = true; - this.addData(removePunishmentData).forceSave(); - return `Successfully removed Case #${punishment.id} (type: ${punishment.type}, user: ${punishment.member}).`; - } catch (error:any){ - return `${punishment.type[0].toUpperCase() + punishment.type.slice(1)} removal was unsuccessful: ${error.message}`; - } - } - } -} \ No newline at end of file diff --git a/src/typings/interfaces.d.ts b/src/typings/interfaces.d.ts index c4ece0e..6ea1f73 100644 --- a/src/typings/interfaces.d.ts +++ b/src/typings/interfaces.d.ts @@ -17,7 +17,7 @@ export interface repeatedMessages { [key:string]: {data: Discord.Collection, timeout: NodeJS.Timeout} } export interface Punishment { - id: number; + _id: number; type: string; member: string; moderator: string; @@ -122,12 +122,14 @@ interface XMLText { _text: string } export interface Tokens { - token_main: string - token_beta: string - token_toast: string - token_tae: string + main: string + beta: string + toast: string + tae: string webhook_url: string webhook_url_test: string + mongodb_uri: string + mongodb_uri_dev: string } export interface Config { embedColor: Discord.ColorResolvable,