1
0
mirror of https://github.com/toast-ts/Daggerbot-TS.git synced 2024-12-25 23:15:38 -05:00

Merry Christmas! Here's the V3

This commit is contained in:
toast-ts 2023-12-25 02:21:40 +11:00
parent cefcbdb1ac
commit bfec8ec431
106 changed files with 10027 additions and 13015 deletions

10
.dockerignore Normal file
View File

@ -0,0 +1,10 @@
botStartup.bat
.ncurc.json
.gitignore
README.md
.github
.vscode
.yarn/sdks
dist/
docker-compose.yml
startWithYarn.cjs

13
.gitignore vendored
View File

@ -1,12 +1,5 @@
.vscode
# Yarn stuff
.yarn/cache/
.yarn/unplugged/
.yarn/sdks/
.yarn/install-state.gz
# NodeJS stuff
.ncurc.json
# Bot stuff
dist/
src/database/
.yarn
# TypeScript stuff
dist
src/*.json

4
.ncurc.json Normal file
View File

@ -0,0 +1,4 @@
{
"upgrade": true,
"reject": []
}

14658
.pnp.cjs generated

File diff suppressed because one or more lines are too long

909
.pnp.loader.mjs generated

File diff suppressed because it is too large Load Diff

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"arcanis.vscode-zipfs"
]
}

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"search.exclude": {
"**/.yarn": true,
"**/.pnp.*": true
},
"typescript.tsdk": ".yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

12
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "dev",
"problemMatcher": [],
"label": "npm: yarn dev",
"detail": "yarn tsc && yarn node . src/DB-Beta.config.json"
}
]
}

File diff suppressed because one or more lines are too long

View File

@ -1 +1,9 @@
yarnPath: .yarn/releases/yarn-3.6.3.cjs
compressionLevel: mixed
enableGlobalCache: false
progressBarStyle: "simba"
npmScopes:
toast:
npmRegistryServer: https://git.toast-server.net/api/packages/toast/npm/

View File

@ -1,7 +1,25 @@
<p align="center">
<img width="650" height="240" src="https://cdn.discordapp.com/attachments/1118960531135541318/1151036641717260348/Daggerbot-TS-repo.gif">
<h1 align="center">Daggerbot-TS Description</h1>
<p align="center">
This is a 1st generation bot that is a TypeScript-based bot converted from JavaScript at <s><a href="https://github.com/SpaceManBuzz/DaggerBot-">SpaceManBuzz/DaggerBot-</a></s> (now archived and privated)
</p>
<img width="630" height="250" src="https://cdn.toast-server.net/daggerwin/DaggerbotV3-Repo.gif">
<h1 align="center">Daggerbot V3 Description</h1>
</p>
This is a repository for V3 revision that has been transitioned and rewritten from V2 bot to be more robust and reliable with today's standards.
This revision took **4 months** (Late September to Mid December) working on and off to do literally everything that needed a rewrite so badly that it cannot be done in V2.
**Q:** So what are the changes if it almost looks the same as V2?
**A:** Here's the bullet points of the changes so far;
- Reworked some of the files
- Commands and events are now classes
- Bot no longer stores short-term and long-term data locally
- Transitioned MongoDB schemas to PostgreSQL models
- MPModule got a facelift and rewritten from scratch
- Moved the module files to another directory called `modules`
- Renamed `funcs` to `components` as I don't think `funcs` directory makes sense anymore at this point.
If you're looking for V2 revision, it has been moved to a [branch called `old`](https://github.com/AnxietyisReal/Daggerbot-TS/tree/old).
This is a revision history of how far we come in development cycle;
| Revision | Language | Library | Commands |
|---------|----------|-----------|----------|
| V1 | JavaScript | Discord.JS v13 | Message commands |
| V2-V3 | TypeScript | Discord.JS v14 | Slash/message commands |

View File

@ -1,7 +1,19 @@
services:
cache:
container_name: redis-cache
image: redis/redis-stack-server:7.2.0-v2
image: redis/redis-stack-server:7.2.0-v6
restart: unless-stopped
ports:
- 6379:6379/tcp
- 6379:6379/tcp
db:
container_name: postgres-db
image: postgres:16.1-alpine3.19
restart: unless-stopped
ports:
- 5432:5432/tcp
volumes:
- /var/lib/docker/volumes/daggerbot-db:/var/lib/postgresql/data:rw
environment:
POSTGRES_USER: daggerbot
POSTGRES_PASSWORD: dagbot
POSTGRES_DB: daggerbot

View File

@ -5,7 +5,11 @@
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/AnxietyisReal/Daggerbot-TS/tree/old"
"url": "git+https://github.com/AnxietyisReal/Daggerbot-TS.git"
},
"scripts": {
"dev": "yarn tsc && yarn node . src/DB-Beta.config.json",
"sdk": "yarn dlx @yarnpkg/sdks vscode"
},
"author": "Toast",
"license": "ISC",
@ -21,30 +25,37 @@
"!ia32"
],
"engines": {
"node": ">=18.17.0",
"yarn": ">=3.6.1",
"npm": "please use yarn instead of npm"
"node": ">=18.18.0, <19 || >=20",
"yarn": ">=4.0.0",
"npm": "yarn is required, dont use npm"
},
"engineStrict": true,
"packageManager": "yarn@3.6.3",
"packageManager": "yarn@4.0.2+sha256.825003a0f561ad09a3b1ac4a3b3ea6207af2796d54f62a9420520915721f5186",
"dependencies": {
"@octokit/auth-token": "4.0.0",
"@octokit/rest": "20.0.2",
"@toast/tokenservice-client": "1.0.5",
"ansi-colors": "4.1.3",
"canvas": "2.11.2",
"dayjs": "1.11.10",
"discord.js": "14.13.0",
"discord.js": "14.14.1",
"fast-xml-parser": "4.3.2",
"mongoose": "7.6.3",
"ms": "2.1.3",
"node-cron": "3.0.2",
"redis": "4.6.10",
"systeminformation": "5.21.13"
"node-cron": "3.0.3",
"pg": "8.11.3",
"pg-hstore": "2.3.4",
"redis": "4.6.12",
"sequelize": "6.35.2",
"simple-git": "3.21.0",
"systeminformation": "5.21.22",
"undici": "6.2.1"
},
"devDependencies": {
"@types/ms": "0.7.33",
"@types/node": "20.8.8",
"@types/node-cron": "3.0.10",
"typescript": "5.2.2"
"@types/ms": "0.7.34",
"@types/node": "20.10.5",
"@types/node-cron": "3.0.11",
"@types/pg": "8.10.9",
"@types/sequelize": "4.28.19",
"typescript": "5.3.3"
}
}

View File

@ -1,52 +1,49 @@
interface repeatedMessages {
[key: string]: {data: Discord.Collection<number,{type:string,channel:string}>,timeout: NodeJS.Timeout}
interface IRepeatedMessages {
[key:string]: {
type:string;
count:number;
firstTime:number;
timeout:NodeJS.Timeout;
}
}
type MPServerCache = Record<string,{
players: FSPlayer[],
status: 'online' | 'offline' | null,
name: string | null
}>
import Discord from 'discord.js';
import ConfigHelper from './helpers/ConfigHelper.js';
import {readdirSync} from 'node:fs';
import {Config, FSPlayer} from './typings/interfaces';
import bannedWords from './models/bannedWords.js';
import userLevels from './models/userLevels.js';
import suggestion from './models/suggestion.js';
import punishments from './models/punishments.js';
import tags from './models/tagSystem.js';
import bonkCount from './models/bonkCount.js';
import MPServer from './models/MPServer.js';
import DatabaseServer from './funcs/DatabaseServer.js';
import CacheServer from './funcs/CacheServer.js';
import {Config} from './interfaces';
import {
DailyMsgsSvc, UserLevelsSvc, BonkCountSvc,
MPServerSvc, PunishmentsSvc, ProhibitedWordsSvc,
SuggestionsSvc, TagSystemSvc, YouTubeChannelsSvc
} from './models/IMPORTS.js';
import DatabaseServer from './components/DatabaseServer.js';
import CacheServer from './components/CacheServer.js';
import fxp from 'fast-xml-parser';
import dayjs from 'dayjs';
import TSClient from './helpers/TSClient.js';
const importconfig = ConfigHelper.loadConfig();
export default class TClient extends Discord.Client {
invites: Map<any, any>;
commands: Discord.Collection<string, any>;
registry: Array<Discord.ApplicationCommandDataResolvable>;
config: Config;
embed: typeof Discord.EmbedBuilder;
collection: typeof Discord.Collection;
attachmentBuilder: typeof Discord.AttachmentBuilder;
dayjs: typeof dayjs;
fxp: typeof fxp;
userLevels: userLevels;
punishments: punishments;
bonkCount: bonkCount;
bannedWords: bannedWords;
MPServer: MPServer;
MPServerCache: MPServerCache = {};
suggestion: suggestion;
tags: tags;
repeatedMessages: repeatedMessages;
statsGraph: number;
public invites: Map<any, any> = new Map();
public commands: Discord.Collection<string, any> = new Discord.Collection();
public registry: Array<Discord.ApplicationCommandDataResolvable> = [];
public config: Config;
public embed: typeof Discord.EmbedBuilder = Discord.EmbedBuilder;
public collection: typeof Discord.Collection = Discord.Collection;
public attachment: typeof Discord.AttachmentBuilder = Discord.AttachmentBuilder;
public dayjs: typeof dayjs = dayjs;
public fxp: typeof fxp = fxp;
public dailyMsgs: DailyMsgsSvc = new DailyMsgsSvc();
public userLevels: UserLevelsSvc = new UserLevelsSvc(this);
public punishments: PunishmentsSvc = new PunishmentsSvc(this);
public bonkCount: BonkCountSvc = new BonkCountSvc();
public prohibitedWords: ProhibitedWordsSvc = new ProhibitedWordsSvc();
public MPServer: MPServerSvc = new MPServerSvc();
public suggestions: SuggestionsSvc = new SuggestionsSvc();
public tags: TagSystemSvc = new TagSystemSvc();
public ytChannels: YouTubeChannelsSvc = new YouTubeChannelsSvc();
public repeatedMessages: IRepeatedMessages = {};
public statsGraph: number = -120;
constructor(){
constructor() {
super({
intents: [
Discord.GatewayIntentBits.Guilds, Discord.GatewayIntentBits.GuildMembers,
@ -56,60 +53,31 @@ export default class TClient extends Discord.Client {
Discord.GatewayIntentBits.DirectMessages
], partials: [
Discord.Partials.Channel, Discord.Partials.Reaction, Discord.Partials.Message
], allowedMentions: {users:[],roles:[]}
], allowedMentions: {users:[], roles:[]}
})
this.invites = new Map();
this.commands = new Discord.Collection();
this.registry = [];
this.config = importconfig as Config;
this.embed = Discord.EmbedBuilder;
this.collection = Discord.Collection;
this.attachmentBuilder = Discord.AttachmentBuilder;
this.dayjs = dayjs;
this.fxp = fxp;
this.userLevels = new userLevels(this);
this.bonkCount = new bonkCount(this);
this.punishments = new punishments(this);
this.bannedWords = new bannedWords(this);
this.MPServer = new MPServer(this);
this.MPServerCache = {} as MPServerCache;
this.suggestion = new suggestion(this);
this.tags = new tags(this);
this.repeatedMessages = {};
this.setMaxListeners(62);
this.statsGraph = -120;
this.config = ConfigHelper.loadConfig() as Config;
this.setMaxListeners(50);
}
async init(){
async init() {
console.time('Startup');
await Promise.all([
CacheServer.init(),
DatabaseServer.init(),
this.login((await TSClient.Token()).main)
]);
const eventFiles = await Promise.all(
readdirSync('dist/events').map(file=>import(`./events/${file}`))
);
const eventFiles = await Promise.all(readdirSync('dist/events').map(file=>import(`./events/${file}`)));
eventFiles.forEach((eventFile, index)=>{
const eventName = readdirSync('dist/events')[index].replace('.js', '');
this.on(eventName, async(...args)=>eventFile.default.run(this, ...args));
});
const commandFiles = await Promise.all(
readdirSync('dist/commands').map(file=>import(`./commands/${file}`))
);
const commandFiles = await Promise.all(readdirSync('dist/commands').map(file=>import(`./commands/${file}`)));
commandFiles.forEach(commandFile=>{
const {default: command} = commandFile;
this.commands.set(command.data.name, {command, uses: 0});
this.registry.push(command.data.toJSON());
});
Object.keys(this.config.MPStatsLocation).forEach(naming=>{
this.MPServerCache[naming] = {
players: [],
status: null,
name: null
}
});
await Promise.all([
CacheServer.init(),
DatabaseServer.init(),
this.login((await TSClient()).main)
]);
}
}

View File

@ -1,11 +1,11 @@
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'>){
import Punish from '../components/Punish.js';
export default class Ban {
static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
Punish(client, interaction, 'ban');
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('ban')
.setDescription('Ban a member from the server')
.addUserOption(x=>x
@ -18,4 +18,4 @@ export default {
.addStringOption(x=>x
.setName('reason')
.setDescription('Reason for the ban'))
}
}

View File

@ -1,48 +0,0 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import {writeFileSync} from 'node:fs';
import MessageTool from '../helpers/MessageTool.js';
export default {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
if (!MessageTool.isStaff(interaction.member) && !client.config.whitelist.includes(interaction.member.id)) return MessageTool.youNeedRole(interaction, 'admin');
const word = interaction.options.getString('word');
const wordExists = await client.bannedWords._content.findById(word);
({
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()).catch(e=>{if (e.name == 'No document found for query') return});
interaction.reply(`Successfully added \`${word}\` to the database.`)
},
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: async()=>{
const findAll = await client.bannedWords.findInCache();
writeFileSync('src/database/bw_dump.json', JSON.stringify(findAll.map(i=>i._id), null, 2), {encoding: 'utf8', flag: 'w+'});
interaction.reply({content: 'Here\'s the dump file from the database.', files: ['src/database/bw_dump.json'], ephemeral: true}).catch(err=>interaction.reply({content: `Ran into an error, notify <@&${client.config.mainServer.roles.bottech}> if it happens again:\n\`${err.message}\``, ephemeral: true}))
}
} as any)[interaction.options.getSubcommand()]();
},
data: new Discord.SlashCommandBuilder()
.setName('bannedwords')
.setDescription('description placeholder')
.addSubcommand(x=>x
.setName('view')
.setDescription('View the list of currently banned words'))
.addSubcommand(x=>x
.setName('add')
.setDescription('Add the word to the list')
.addStringOption(x=>x
.setName('word')
.setDescription('Add the specific word to automod\'s bannedWords database')
.setRequired(true)))
.addSubcommand(x=>x
.setName('remove')
.setDescription('Remove the word from the list')
.addStringOption(x=>x
.setName('word')
.setDescription('Remove the specific word from automod\'s bannedWords list')
.setRequired(true)))
}

View File

@ -1,20 +1,20 @@
import Discord from 'discord.js';
import TClient from '../client.js';
export default {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
export default class Bonk {
static 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');
if (member.permissions.has('Administrator')) return interaction.reply('You cannot bonk an admin!');
await client.bonkCount._incrementUser(member.id);
await client.bonkCount.hitCountIncremental(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.displayName}: ${await client.bonkCount._content.findById(member.id).then(b=>b.value.toLocaleString('en-US'))}`})
.setFooter({text: `Bonk count for ${member.displayName}: ${await client.bonkCount.fetchUser(member.id).then(x=>x.count)}`})
]})
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('bonk')
.setDescription('Bonk a member')
.addUserOption(x=>x

View File

@ -1,10 +1,10 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
import FormatTime from '../helpers/FormatTime.js';
import Formatters from '../helpers/Formatters.js';
export default {
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
export default class Calculator {
static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
const now = Date.now();
const exp = interaction.options.getString('expression', true).replace(/[^-()\d/*+.]/g, '');
try {
@ -22,12 +22,12 @@ export default {
'-- Siri, 2015'
));
};
interaction.reply({embeds:[new client.embed().setColor(client.config.embedColor).addFields({name: 'Expression', value: `\`\`\`js\n${exp}\n\`\`\``},{name: 'Answer', value: `\`\`\`js\n${result}\n\`\`\``}).setFooter({text: `Time taken: ${FormatTime(Date.now() - now, 3)}`})]})
interaction.reply({embeds:[new client.embed().setColor(client.config.embedColor).addFields({name: 'Expression', value: `\`\`\`js\n${exp}\n\`\`\``},{name: 'Answer', value: `\`\`\`js\n${result}\n\`\`\``}).setFooter({text: `Time taken: ${Formatters.timeFormat(Date.now() - now, 3)}`})]})
} catch {
interaction.reply('The given expression is invalid.');
}
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('calculator')
.setDescription('Calculate a math expression or simple 2+2')
.addStringOption(x=>x

View File

@ -1,42 +1,59 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import FormatTime from '../helpers/FormatTime.js';
import Formatters from '../helpers/Formatters.js';
import MessageTool from '../helpers/MessageTool.js';
export default {
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
export default class Case {
private static async updateEntry(client:TClient, caseId:number, reason:string) {
const logsArray = [client.config.dcServer.channels.logs, client.config.dcServer.channels.bankick_log];
for (const channelID of logsArray) {
const channel = await client.channels.fetch(channelID) as Discord.TextChannel;
if (channel && channel.type === Discord.ChannelType.GuildText) {
const messages = await channel.messages.fetch({limit: 3});
messages.forEach(async message=>{
if (message?.embeds[0]?.title.match(new RegExp(`Case #${caseId}`))) {
const findIndex = message?.embeds[0].fields.findIndex(x=>x.name === 'Reason');
await message.edit({embeds: [new client.embed(message.embeds[0]).spliceFields(findIndex, 1, {name: 'Reason', value: `\`${reason}\``})]});
}
})
}
}
}
static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
if (!MessageTool.isStaff(interaction.member)) return MessageTool.youNeedRole(interaction, 'dcmod');
const caseId = interaction.options.getInteger('id');
({
update: async()=>{
const reason = interaction.options.getString('reason');
await client.punishments._content.findByIdAndUpdate(caseId, {reason});
if (await client.punishments._content.findById(caseId)) 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}\``)]});
else interaction.reply({embeds: [new client.embed().setColor(client.config.embedColorRed).setTitle('Case not updated').setDescription(`Case #${caseId} is not stored on database, not updating the reason.`)]});
await client.punishments.updateReason(caseId, reason);
if (client.punishments.findCase(caseId)) {
await this.updateEntry(client, 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}\``)]});
} else interaction.reply({embeds: [new client.embed().setColor(client.config.embedColorRed).setTitle('Case not updated').setDescription(`Case #${caseId} is not found in database, not updating the reason.`)]});
},
view: async()=>{
const punishment = await client.punishments._content.findById(caseId);
if (!punishment) return interaction.reply('Invalid Case ID');
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: `${MessageTool.formatMention(punishment.member, 'user')} \`${punishment.member}\``, inline: true},
{name: '🔹 Moderator', value: `${MessageTool.formatMention(punishment.moderator, 'user')} \`${punishment.moderator}\``, inline: true},
const punishment = await client.punishments.findCase(caseId);
if (!punishment) return interaction.reply('Case ID is not found in database.');
const cancelledBy = punishment.dataValues.expired ? await client.punishments.findByCancels(punishment.dataValues.case_id) : null;
const cancels = punishment.dataValues.cancels ? await client.punishments.findCase(punishment.dataValues.cancels) : null;
const embed = new client.embed().setColor(client.config.embedColor).setTimestamp(Number(punishment.dataValues.time)).setTitle(`${punishment.dataValues.type[0].toUpperCase()+punishment.dataValues.type.slice(1)} | Case #${punishment.dataValues.case_id}`).addFields(
{name: 'User', value: `${MessageTool.formatMention(punishment.dataValues.member, 'user')} \`${punishment.dataValues.member}\``, inline: true},
{name: 'Moderator', value: `${MessageTool.formatMention(punishment.dataValues.moderator, 'user')} \`${punishment.dataValues.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: `${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}\``})
{name: 'Reason', value: `\`${punishment.reason || 'Reason unspecified'}\``, inline: true})
if (punishment.dataValues.duration) embed.addFields({name: 'Duration', value: `${Formatters.timeFormat(punishment.dataValues.duration, 100)}`})
if (punishment.dataValues.expired) embed.addFields({name: 'Expired', value: `This case has been overwritten by Case #${cancelledBy.dataValues.case_id} for reason \`${cancelledBy.dataValues.reason}\``})
if (punishment.dataValues.cancels) embed.addFields({name: 'Overwrites', value: `This case overwrites Case #${cancels.dataValues.case_id} with reason \`${cancels.dataValues.reason}\``})
interaction.reply({embeds: [embed]});
},
member: async()=>{
const user = (interaction.options.getUser('user') as Discord.User);
if (user.bot) return interaction.reply(`**${user.username}**'s punishment history cannot be viewed as they are a bot.`)
const punishments = await client.punishments._content.find({});
const userPunishmentData = await client.punishments._content.find({'member':user.id});
const userPunishment = userPunishmentData.sort((a,b)=>a.time-b.time).map((punishment)=>{
const punishments = await client.punishments.getAllCases();
const userPunishmentData = punishments.filter(x=>x.dataValues.member === user.id);
const userPunishment = userPunishmentData.sort((a,b)=>a.dataValues.time-b.dataValues.time).map(punishment=>{
return {
name: `${punishment.type[0].toUpperCase()+punishment.type.slice(1)} | Case #${punishment.id}`,
value: `Reason: \`${punishment.reason}\`\n${punishment.duration ? `Duration: ${FormatTime(punishment.duration, 3)}\n` : ''}Moderator: ${MessageTool.formatMention(punishment.moderator, 'user')}${punishment.expired ? `\nOverwritten by Case #${punishments.find(x=>x.cancels===punishment._id)?._id}` : ''}${punishment.cancels ? `\nOverwrites Case #${punishment.cancels}` : ''}`
name: `${punishment.dataValues.type[0].toUpperCase()+punishment.dataValues.type.slice(1)} | Case #${punishment.dataValues.case_id}`,
value: `Reason: \`${punishment.dataValues.reason}\`\n${punishment.dataValues.duration ? `Duration: ${Formatters.timeFormat(punishment.dataValues.duration, 3)}\n` : ''}Moderator: ${MessageTool.formatMention(punishment.dataValues.moderator, 'user')}${punishment.dataValues.expired ? `\nOverwritten by Case #${punishments.find(x=>x.dataValues.cancels===punishment.dataValues.case_id)?.case_id}` : ''}${punishment.dataValues.cancels ? `\nOverwrites Case #${punishment.dataValues.cancels}` : ''}`
}
});
if (!punishments || !userPunishment) return interaction.reply(`**${user.username}** has a clean record.`)
@ -44,13 +61,13 @@ export default {
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 Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('case')
.setDescription('Retrieve case information or user\'s punishment history')
.addSubcommand(x=>x
.setName('view')
.setDescription('View a multiple or single case')
.setDescription('View information of the case ID')
.addIntegerOption(x=>x
.setName('id')
.setDescription('Case ID')
@ -76,4 +93,4 @@ export default {
.setName('reason')
.setDescription('New reason for the case')
.setRequired(true)))
};
}

View File

@ -1,16 +1,13 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
export default {
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
export default class Contributors {
static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Daggerbot contributors').setDescription(MessageTool.concatMessage(
'**Thanks to those below that contributed to/developed the bot!**',
client.config.contribList.map(id=>{
const member = interaction.guild.members.cache.get(id);
return `${member?.user?.username ?? 'N/A'} <@${id}>`}
).join('\n')))]})
},
data: new Discord.SlashCommandBuilder()
'**Thanks to those below that made their contributions to the bot!**',
client.config.contribList.map(id=>`${interaction.guild.members.cache.get(id)?.user?.username ?? 'N/A'} <@${id}>`).join('\n')))]})
}
static data = new Discord.SlashCommandBuilder()
.setName('contributors')
.setDescription('List of people who contributed to the bot')
}
}

View File

@ -1,28 +1,26 @@
import Discord from 'discord.js';
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 TSClient from '../helpers/TSClient.js';
import Formatters from '../helpers/Formatters.js';
import GitHub from '../helpers/GitHub.js';
import TClient from '../client.js';
import fs from 'node:fs';
import util from 'node:util';
export default {
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>) {
import fs from 'node:fs';
export default class Developer {
static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>) {
if (!client.config.whitelist.includes(interaction.user.id)) return MessageTool.youNeedRole(interaction, 'bottech');
({
eval: async()=>{
if (!client.config.eval) return interaction.reply({content: 'Eval is currently disabled.', ephemeral: true});
fs;
const code = interaction.options.getString('code') as string;
let consoleOutput:string = '';
const deleteEmbedBtn = new Discord.ButtonBuilder().setCustomId('deleteEmbed').setLabel('Delete').setStyle(Discord.ButtonStyle.Danger).setEmoji('🗑️');
const deleteEmbedBtn = new Discord.ButtonBuilder().setCustomId('deleteEvalEmbed').setLabel('Delete').setStyle(Discord.ButtonStyle.Danger).setEmoji('🗑️');
const deleteEmbedRow = new Discord.ActionRowBuilder<Discord.ButtonBuilder>().addComponents(deleteEmbedBtn);
const deleteEmbedCollector = interaction.channel.createMessageComponentCollector({componentType: Discord.ComponentType.Button});
deleteEmbedCollector.on('collect', async i=>{
if (i.customId === 'deleteEmbed') deleteEmbedCollector.stop();
if (i.customId === 'deleteEvalEmbed') deleteEmbedCollector.stop();
});
try {
@ -33,13 +31,13 @@ export default {
}
const output = await eval(interaction.options.getBoolean('async') ? `(async()=>{${code}})()` : code);
let outVal = output !== undefined ? output : 'No output';
let outVal = output;
if (outVal && outVal.includes && outVal.includes(client.token)) outVal = outVal.replace(client.token, '*'.repeat(8));
const embedFields:Discord.APIEmbedField[] = [
{name: 'Input', value: `\`\`\`js\n${code.slice(0,1020)}\n\`\`\``},
{name: 'Output', value: `**\`\`\`${UsernameHelper.stripName(outVal === 'string' ? String(outVal) : 'ansi\n'+util.formatWithOptions({depth: 3, colors: true}, '%O', outVal)).slice(0,1012)}\n\`\`\`**`}
{name: 'Output', value: `**\`\`\`${UsernameHelper(outVal === 'string' ? String(outVal) : 'ansi\n'+util.formatWithOptions({depth: 3, colors: true}, '%O', outVal)).slice(0,1012)}\n\`\`\`**`}
];
if (consoleOutput) embedFields.push({name: 'Console', value: `**\`\`\`ansi\n${UsernameHelper.stripName(consoleOutput).slice(0,1008)}\n\`\`\`**`});
if (consoleOutput) embedFields.push({name: 'Console', value: `**\`\`\`ansi\n${UsernameHelper(consoleOutput).slice(0,1008)}\n\`\`\`**`});
if (typeof output === 'object') {
const embed = new client.embed().setColor(client.config.embedColor).addFields(embedFields);
interaction.reply({embeds: [embed], components: [deleteEmbedRow]}).catch(()=>(interaction.channel as Discord.TextChannel).send({embeds: [embed], components: [deleteEmbedRow]}));
@ -57,7 +55,7 @@ export default {
const messagecollector = (interaction.channel as Discord.TextChannel).createMessageCollector({filter, max: 1, time: 60000});
messagecollector.on('collect', collected=>{
console.log(err)
collected.reply(`\`\`\`\n${UsernameHelper.stripName(err.stack)}\n\`\`\``);
collected.reply(`\`\`\`\n${UsernameHelper(err.stack)}\n\`\`\``);
});
});
} finally {
@ -65,32 +63,31 @@ export default {
}
},
update: async()=>{
const SummonAuthentication = createTokenAuth((await TSClient.Token()).octokit);
const {token} = await SummonAuthentication();
var githubRepo = {owner: 'AnxietyisReal', repo: 'Daggerbot-TS', ref: 'HEAD'};
const hammond = await interaction.reply({content: 'Pulling from repository...', fetchReply: true});
const octokit = new Octokit({auth: token, timeZone: 'Australia/NSW', userAgent: 'Daggerbot-TS'});
const github = {
fetchCommit: {
msg: await octokit.repos.getCommit(githubRepo).then(x=>x.data.commit.message).catch((err:Error)=>err.message),
author: await octokit.repos.getCommit(githubRepo).then(x=>x.data.commit.author.name).catch((err:Error)=>err.message),
url: await octokit.repos.getCommit(githubRepo).then(x=>x.data.html_url).catch((err:Error)=>err.message)
},
fetchChanges: {
total: await octokit.repos.getCommit(githubRepo).then(x=>x.data.stats.total.toLocaleString('en-US')).catch((err:Error)=>err.message),
addition: await octokit.repos.getCommit(githubRepo).then(x=>x.data.stats.additions.toLocaleString('en-US')).catch((err:Error)=>err.message),
deletion: await octokit.repos.getCommit(githubRepo).then(x=>x.data.stats.deletions.toLocaleString('en-US')).catch((err:Error)=>err.message)
}
const hammondYouIdiot = await interaction.reply({content: 'Pulling...', fetchReply: true});
const repoData = await GitHub.RemoteRepository();
const commitStats = {
total: repoData.stats.total.toLocaleString('en-US'),
addition: repoData.stats.additions.toLocaleString('en-US'),
deletion: repoData.stats.deletions.toLocaleString('en-US')
};
exec('git pull',{windowsHide:true},(err:Error,stdout)=>{
if (err) hammond.edit(`\`\`\`${UsernameHelper.stripName(err.message)}\`\`\``)
else if (stdout.includes('Already up to date')) hammond.edit('I am already up to date with the upstream repository.')
else hammond.edit('Compiling TypeScript files...').then(()=>exec('yarn tsc', {windowsHide:true}, (err:Error)=>{
if (err) hammond.edit(`\`\`\`${UsernameHelper.stripName(err.message)}\`\`\``)
if (interaction.options.getBoolean('restart')) hammond.edit(`[Commit:](<${github.fetchCommit.url}>) **${github.fetchCommit.msg.length === 0 ? 'No commit message' : 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 hammond.edit(`[Commit:](<${github.fetchCommit.url}>) **${github.fetchCommit.msg.length === 0 ? 'No commit message' : 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!`)
}))
})
const msgBody = MessageTool.concatMessage(
`[Commit pulled:](<${repoData.commit.url}>)`,
`Message: **${repoData.commit.message.length === 0 ? '*No commit message*' : repoData.commit.message}**`,
`Author: **${repoData.commit.author.name}**`,
'Changes',
`${commitStats.addition.length > 1 ? `Additions:` : 'Addition:'} **${commitStats.addition}**`,
`${commitStats.deletion.length > 1 ? `Deletions:` : 'Deletion:'} **${commitStats.deletion}**`,
`╰ Total: **${commitStats.total}**`
);
exec('git pull', {windowsHide:true}, (err:Error, stdout)=>{
if (err) hammondYouIdiot.edit(`\`\`\`${UsernameHelper(err.message)}\`\`\``);
else if (stdout.includes('Already up to date')) hammondYouIdiot.edit('Repository is currently up to date.');
else hammondYouIdiot.edit('Running `yarn tsc`...').then(()=>exec('yarn tsc', {windowsHide:true}, (err:Error)=>{
if (err) hammondYouIdiot.edit(`\`\`\`${UsernameHelper(err.message)}\`\`\``);
else if (interaction.options.getBoolean('restart')) hammondYouIdiot.edit(msgBody + `\nUptime: ${Formatters.timeFormat(process.uptime()*1000, 4, {longNames:true, commas:true})}`).then(()=>process.exit(0));
else hammondYouIdiot.edit(msgBody);
}));
});
},
presence: ()=>{
function convertType(Type?: number){
@ -121,24 +118,11 @@ export default {
`URL: \`${currentActivities[0].url}\``
))
},
statsgraph: ()=>{
client.statsGraph = -(interaction.options.getInteger('number', true));
interaction.reply(`Successfully set to \`${client.statsGraph}\`\n*Total data points: **${JSON.parse(fs.readFileSync(`src/database/${interaction.options.getString('server')}PlayerData.json`, {encoding: 'utf8'})).length.toLocaleString()}***`)
},
logs: ()=>(client.channels.resolve(client.config.mainServer.channels.console) as Discord.TextChannel).send({content: `Uploaded the current console dump as of <t:${Math.round(Date.now()/1000)}:R>`, files: [`${process.env.pm2_home}/logs/Daggerbot-out.log`, `${process.env.pm2_home}/logs/Daggerbot-error.log`]}).then(()=>interaction.reply('It has been uploaded to dev server.')).catch((e:Error)=>interaction.reply(`\`${e.message}\``)),
restart: async()=>{
const i = await interaction.reply({content: 'Compiling TypeScript files...', fetchReply: true});
exec('yarn tsc',{windowsHide:true},(err:Error)=>{
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(UsernameHelper.stripName(err.message))
else i.edit('Your device should be awake by now!\n||Don\'t blame me if it isn\'t on.||')
const int = await interaction.reply({content: 'Running `yarn tsc`...', fetchReply: true});
exec('yarn tsc', {windowsHide:true}, (err:Error)=>{
if (err) int.edit(`\`\`\`${UsernameHelper(err.message)}\`\`\``);
else int.edit(`Restarting...\nUptime: **${Formatters.timeFormat(process.uptime()*1000, 4, {longNames:true, commas:true})}**`).then(()=>process.exit(0));
})
},
dm: async()=>{
@ -148,8 +132,8 @@ export default {
member.send(message).then(()=>int.edit(`Successfully sent a DM to **${member.user.username}** with the following message:\n\`\`\`${message}\`\`\``)).catch((e:Error)=>int.edit(`\`${e.message}\``))
}
} as any)[interaction.options.getSubcommand()]();
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('dev')
.setDescription('Developer commands')
.addSubcommand(x=>x
@ -163,9 +147,6 @@ export default {
.setName('async')
.setDescription('Asynchronously execute your code')
.setRequired(false)))
.addSubcommand(x=>x
.setName('logs')
.setDescription('Retrieve the logs from host and sends it to dev server'))
.addSubcommand(x=>x
.setName('restart')
.setDescription('Restart the bot for technical reasons'))
@ -177,24 +158,6 @@ export default {
.setDescription('Restart the bot after pulling from repository')
.setRequired(true)
))
.addSubcommand(x=>x
.setName('wake_device')
.setDescription('Remotely wake up a device in the same network as the bot')
.addStringOption(x=>x
.setName('name')
.setDescription('Device name')
.setRequired(true)))
.addSubcommand(x=>x
.setName('statsgraph')
.setDescription('Edit the number of data points to pull')
.addStringOption(x=>x
.setName('server')
.setDescription('Server name')
.setRequired(true))
.addIntegerOption(x=>x
.setName('number')
.setDescription('Number of data points to pull')
.setRequired(true)))
.addSubcommand(x=>x
.setName('presence')
.setDescription('Update the bot\'s presence')
@ -224,13 +187,6 @@ export default {
{name: 'Do Not Distrub', value: Discord.PresenceUpdateStatus.DoNotDisturb},
{name: 'Invisible', value: Discord.PresenceUpdateStatus.Offline}
)))
.addSubcommand(x=>x
.setName('file')
.setDescription('Send a JSON file from database directory on the host')
.addStringOption(x=>x
.setName('name')
.setDescription('JSON filename, don\'t include an extension')
.setRequired(true)))
.addSubcommand(x=>x
.setName('dm')
.setDescription('Reply or send a DM to a member')

View File

@ -1,45 +1,28 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
import FAQStore from '../helpers/FAQStore.js';
export default {
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
const CDN =(filename:string)=>'https://cdn.toast-server.net/daggerwin/'+filename+'.png';
const verifyFaq = MessageTool.concatMessage(
'```ansi',
'Steam (Top panel)',
'1. Go to your game library and right click on Farming Simulator 22',
'2. Click on Properties and navigate to "Installed Files"',
'3. Click on "Verify integrity of game files"',
'4. Steam will scan your game installation directory and will re-download anything that is corrupted or tampered with.',
'',
'Epic Games (Bottom panel)',
'1. Go to your game library and click on 3 dots (...)',
'2. Click on Manage and click on "Verify"',
'3. Epic Launcher will scan your game installation directory and will re-download anything that is corrupted or tampered with.',
'```'
);
const youCanGetRole = (role:string, roleEmoji:string)=>`You can get the ${MessageTool.formatMention(client.config.mainServer.roles[role], 'role')} role from <#802283932430106624> by clicking :${roleEmoji}: button on a webhook's message.`;
import FAQHelper from '../helpers/FAQHelper.js';
export default class FAQ {
static run(_client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
({
srp: ()=>FAQStore.reply(interaction, null, '[Ballyspring](<https://www.farming-simulator.com/mod.php?mod_id=270745>) is the map that is used in Survival Roleplay S4.\n\n> __Note__\n> The map won\'t look closely like the one in SRP as it is privately edited version of the public map.', null, false),
vtcR: ()=>interaction.reply(youCanGetRole('vtcmember', 'truck')+'\n*VTC skin can also be found in <#801975222609641472> as well.*'),
mpR: ()=>interaction.reply(youCanGetRole('mpplayer', 'tractor')),
ytscam: ()=>FAQStore.reply(interaction, 'Scammers in YouTube comments section', '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.', CDN('YTScam'), true),
steamscam: ()=>FAQStore.reply(interaction, 'Steam account report scam', 'If you received a DM about this, please report it to Discord Moderators or open a [ticket](https://discord.com/channels/468835415093411861/942173932339986472/1054128182468546631)', CDN('SteamScam'), true),
fsVerifyGame: ()=>FAQStore.reply(interaction, 'Verifying your game files', `You can verify your game files if you experience any issues with your game.\n${verifyFaq}`, CDN('Steam-Epic-VerifyGamesLocation'), true),
fsShader: ()=>FAQStore.reply(interaction, 'Clearing your shader cache folder', 'If your game keeps 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`', CDN('shader_cache-Location'), true),
fsLogfile: ()=>FAQStore.reply(interaction, 'Uploading your log file', '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.', CDN('log_txt-Location'), true),
fsDevConsole: ()=>FAQStore.reply(interaction, 'Enabling the development console', 'Head over to `game.xml` in `Documents/My Games/FarmingSimulator2022` and find the section that mentions `<controls>false</controls>` inside development section, change it to `true` then you are good to go!\nFYI: The keybind to open console is \``\u200b\` (backtick).', CDN('enableDevConsole'), true)
srp: ()=>FAQHelper.reply(interaction, null, `[Ballyspring](<${FAQHelper.linkMapping.ballyspring}>) is the map that is used in Survival Roleplay S4.\n\n> __Note__\n> The map won't look closely like the one in SRP as it is privately edited version of the public map.`, null, false),
vtcR: ()=>interaction.reply(FAQHelper.youCanGetRole('vtcmember', 'truck')+'\n*VTC skin can also be found in <#801975222609641472> as well.*'),
mpR: ()=>interaction.reply(FAQHelper.youCanGetRole('mpplayer', 'tractor')),
ytscam: ()=>FAQHelper.reply(interaction, 'Scammers in YouTube comments section', '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.', FAQHelper.CDN('YTScam'), true),
steamscam: ()=>FAQHelper.reply(interaction, 'Steam account report scam', `If you received a DM about this, please report it to Discord Moderators or open a [ticket](${FAQHelper.linkMapping.staffTicket})`, FAQHelper.CDN('SteamScam'), true),
fsVerifyGame: ()=>FAQHelper.reply(interaction, 'Verifying your game files', `You can verify your game files if you experience any issues with your game.\n${FAQHelper.verifyGameFiles}`, FAQHelper.CDN('Steam-Epic-VerifyGamesLocation'), true),
fsShader: ()=>FAQHelper.reply(interaction, 'Clearing your shader cache folder', 'If your game keeps 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`', FAQHelper.CDN('shader_cache-Location'), true),
fsLogfile: ()=>FAQHelper.reply(interaction, 'Uploading your log file', '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.', FAQHelper.CDN('log_txt-Location'), true),
fsDevConsole: ()=>FAQHelper.reply(interaction, 'Enabling the development console', 'Head over to `game.xml` in `Documents/My Games/FarmingSimulator2022` and find the section that mentions `<controls>false</controls>` inside development section, change it to `true` then you are good to go!\nFYI: The keybind to open console is \``\u200b\` (backtick).', FAQHelper.CDN('enableDevConsole'), true)
} as any)[interaction.options.getString('question', true)]();
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('faq')
.setDescription('List of questions, e.g; log file for FS, YT Scams and etc.')
.addStringOption(x=>x
.setName('question')
.setDescription('What question do you want answered?')
.setRequired(true)
.addChoices(
.setChoices(
{ name: 'Survival Roleplay Map', value: 'srp'},
{ name: 'Scams in YT comments', value: 'ytscam' },
{ name: 'Steam account report scam', value: 'steamscam' },

View File

@ -1,8 +1,8 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
export default {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
export default class InviteInfo {
static async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
await client.fetchInvite(interaction.options.getString('code',true).replace(/(https:\/\/|discord.gg\/)/g,'')).then(async inviteData=>
await interaction.reply({embeds:[new client.embed()
.setColor(client.config.embedColor).setURL(`https://discord.gg/${inviteData.code}`).setTitle(inviteData.guild.name).setDescription(MessageTool.concatMessage(
@ -13,12 +13,12 @@ export default {
`Channel: \`#${inviteData.channel.name}\``,
)).setThumbnail(inviteData.guild.iconURL({size:1024,extension:'webp'})).setImage(inviteData.guild.bannerURL({size:2048,extension:'webp'}))
]})).catch((err:Discord.DiscordAPIError)=>interaction.reply(err.message));
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('inviteinfo')
.setDescription('View the server data from invite link')
.addStringOption(x=>x
.setName('code')
.setDescription('Discord invite code')
.setRequired(true))
}
}

View File

@ -1,13 +1,13 @@
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'>){
import Punish from '../components/Punish.js';
export default class Kick {
static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
Punish(client, interaction, 'kick');
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('kick')
.setDescription('Boot a member from the server')
.setDescription('Kick a member from the server')
.addUserOption(x=>x
.setName('member')
.setDescription('Which member to kick?')
@ -15,4 +15,4 @@ export default {
.addStringOption(x=>x
.setName('reason')
.setDescription('Reason for the kick'))
}
}

View File

@ -1,31 +0,0 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
export default {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
if (client.config.mainServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.mainServer.roles.mpmanager) && !interaction.member.roles.cache.has(client.config.mainServer.roles.bottech) && !interaction.member.roles.cache.has(client.config.mainServer.roles.admin)) return MessageTool.youNeedRole(interaction, 'mpmanager');
}
const maintenanceMessage = interaction.options.getString('message');
const activePlayersChannel = '739084625862852715';
const channel = (client.channels.cache.get(activePlayersChannel) as Discord.TextChannel);
const embed = new client.embed().setColor(client.config.embedColor).setAuthor({name: interaction.member.displayName, iconURL: interaction.member.displayAvatarURL({size:1024})}).setTimestamp();
if (channel.permissionsFor(interaction.guildId).has('SendMessages')) {
channel.permissionOverwrites.edit(interaction.guildId, {SendMessages: false}, {type: 0, reason: `Locked by ${interaction.member.displayName}`});
channel.send({embeds: [embed.setTitle('🔒 Channel locked').setDescription(`**Reason:**\n${maintenanceMessage}`)]});
interaction.reply({content: `<#${activePlayersChannel}> has been locked!`, ephemeral: true});
} else if (!channel.permissionsFor(interaction.guildId).has('SendMessages')) {
channel.permissionOverwrites.edit(interaction.guildId, {SendMessages: true}, {type: 0, reason: `Unlocked by ${interaction.member.displayName}`});
channel.send({embeds: [embed.setTitle('🔓 Channel unlocked').setDescription(`**Reason:**\n${maintenanceMessage}`)]});
interaction.reply({content: `<#${activePlayersChannel}> has been unlocked!`, ephemeral: true});
}
},
data: new Discord.SlashCommandBuilder()
.setName('mp-maintenance') // Just a workaround because I am too fucking tired of issues with it, so it gets to be in dedicated file for now. (Also sorry for the swear word, I am just stressed right now.)
.setDescription('Toggle maintenance mode for #mp-active-players')
.addStringOption(x=>x
.setName('message')
.setDescription('The message to display in the channel')
.setRequired(true))
}

View File

@ -1,274 +1,326 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import path from 'node:path';
import canvas from 'canvas';
import PalletLibrary from '../helpers/PalletLibrary.js';
import FormatPlayer from '../helpers/FormatPlayer.js';
import MessageTool from '../helpers/MessageTool.js';
import Logger from '../helpers/Logger.js';
import {readFileSync} from 'node:fs';
import {FSData} from '../typings/interfaces';
import CanvasBuilder from '../components/CanvasGraph.js';
import RanIntoHumor from '../helpers/RanIntoHumor.js';
import MessageTool from '../helpers/MessageTool.js';
import PalletLibrary from '../helpers/PalletLibrary.js';
import {FSData} from '../interfaces';
import {requestServerData, mpModuleDisabled, refreshTimerSecs, playtimeStat} from '../modules/MPModule.js';
const serverChoices = [
{name: 'Main Server', value: 'mainServer'},
{name: 'Second Server', value: 'secondServer'}
]
async function fetchData(client:TClient, interaction:Discord.ChatInputCommandInteraction, serverName:string):Promise<FSData|Discord.InteractionResponse> {
const db = await client.MPServer.findInCache();
const {dss} = await requestServerData(client, db.find(x=>x.serverName === serverName));
if (!dss) return interaction.reply('Ran into a '+RanIntoHumor+' while trying to retrieve server data, please try again later.');
return dss as FSData;
}
export default {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
if (client.uptime < 35000) return interaction.reply('I have just restarted, please wait for MPLoop to finish initializing.');
const serverSelector = interaction.options.getString('server');
if (['468835769092669461', '1149238561934151690'].includes(interaction.channelId) && !MessageTool.isStaff(interaction.member) && ['status', 'players'].includes(interaction.options.getSubcommand())) return interaction.reply('Please use <#739084625862852715> for `/mp status/players` commands to prevent clutter in this channel.').then(()=>setTimeout(()=>interaction.deleteReply(), 6000));
const database = await client.MPServer.findInCache(interaction.guildId);
const endpoint = await fetch(database[serverSelector].ip+'/feed/dedicated-server-stats.json?code='+database[serverSelector].code, {signal: AbortSignal.timeout(7500),headers:{'User-Agent':'Daggerbot - MPdata/undici'}}).then(r=>r.json() as Promise<FSData>);
const embed = new client.embed();
const logPrefix = 'MPDB';
const channels = {
activePlayers: '739084625862852715',
announcements: '1084864116776251463',
mainMpChat: '468835769092669461',
mfMpChat: '1149238561934151690',
serverInfo: '543494084363288637',
}
export default class MP {
static async autocomplete(client: TClient, interaction: Discord.AutocompleteInteraction<'cached'>) {
const serversInCache = await client.MPServer?.findInCache();
const filterByActive = serversInCache?.filter(x=>x.isActive)?.map(x=>x.serverName);
await interaction?.respond(filterByActive?.map(server=>({name: server, value: server})));
}
static async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>) {
if (client.config.botSwitches.mpSys === false) return interaction.reply({embeds: [mpModuleDisabled(client)]});
if (client.uptime < refreshTimerSecs) return interaction.reply('MPModule isn\'t initialized yet, please wait a moment and try again.');
if ([channels.mainMpChat, channels.mfMpChat].includes(interaction.channelId) && !MessageTool.isStaff(interaction.member) && ['status', 'players'].includes(interaction.options.getSubcommand())) return interaction.reply(`Please use <#${channels.activePlayers}> for \`/mp status/players\` commands to prevent clutter in this channel.`).then(()=>setTimeout(()=>interaction.deleteReply(), 6000));
const choiceSelector = interaction.options.getString('server');
({
players: async()=>{
const data = JSON.parse(readFileSync(path.join(`src/database/${client.MPServerCache[serverSelector].name}PlayerData.json`), {encoding: 'utf8'})).slice(client.statsGraph);
// handle negative days
for (const [i, change] of data.entries()) if (change < 0) data[i] = data[i - 1] || data[i + 1] || 0;
const DSS = await fetchData(client, interaction, choiceSelector) as FSData;
if (!DSS) return console.log('Endpoint failed - players');
const first_graph_top = 16;
const second_graph_top = 16;
const textSize = 40;
const img = canvas.createCanvas(1500, 750);
const ctx = img.getContext('2d');
const graphOrigin = [15, 65];
const graphSize = [1300, 630];
const nodeWidth = graphSize[0] / (data.length - 1);
ctx.fillStyle = '#36393f';
ctx.fillRect(0, 0, img.width, img.height);
// grey horizontal lines
ctx.lineWidth = 5;
const interval_candidates: [number, number, number][] = [];
for (let i = 4; i < 10; i++) {
const interval = first_graph_top / i;
if (Number.isInteger(interval)) {
let intervalString = interval.toString();
const reference_number = i * Math.max(intervalString.split('').filter(x => x === '0').length / intervalString.length, 0.3) * (['1', '2', '4', '5', '6', '8'].includes(intervalString[0]) ? 1.5 : 0.67)
interval_candidates.push([interval, i, reference_number]);
}
const PDArr = await client.MPServer.fetchPlayerData(choiceSelector);
const canvas = await new CanvasBuilder().generateGraph(PDArr.slice(client.statsGraph), 'players');
const players:string[] = [];
let embedColor:Discord.ColorResolvable;
switch (true){
case DSS?.slots?.used === DSS?.slots.capacity:
embedColor = client.config.embedColorRed;
break;
case DSS?.slots?.used > 8:
embedColor = client.config.embedColorYellow;
break;
default:
embedColor = client.config.embedColorGreen;
}
const chosen_interval = interval_candidates.sort((a, b) => b[2] - a[2])[0];
const previousY: number[] = [];
ctx.strokeStyle = '#202225';
for (const player of DSS.slots.players.filter(x=>x.isUsed)) players.push(playtimeStat(player))
for (let i = 0; i <= chosen_interval[1]; i++) {
const y = graphOrigin[1] + graphSize[1] - (i * (chosen_interval[0] / second_graph_top) * graphSize[1]);
if (y < graphOrigin[1]) continue;
const even = ((i + 1) % 2) === 0;
if (even) ctx.strokeStyle = '#2c2f33';
ctx.beginPath();
ctx.lineTo(graphOrigin[0], y);
ctx.lineTo(graphOrigin[0] + graphSize[0], y);
ctx.stroke();
ctx.closePath();
if (even) ctx.strokeStyle = '#202225';
previousY.push(y, i * chosen_interval[0]);
}
let attachmentName:string = 'MPModule.jpg';
await interaction.reply({embeds:[new client.embed()
.setTitle(DSS.server?.name.length > 0 ? DSS.server.name : 'Offline')
.setColor(embedColor)
.setDescription(DSS?.slots?.used < 1 ? '*Nobody is playing*' : players.join('\n\n'))
.setImage('attachment://'+attachmentName)
.setAuthor({name: `${DSS.slots.used}/${DSS.slots.capacity}`})
.setFooter({text: 'Current time: '+`${('0'+Math.floor((DSS?.server.dayTime/3600/1000))).slice(-2)}:${('0'+Math.floor((DSS?.server.dayTime/60/1000)%60)).slice(-2)}`})
], files: [new client.attachment(canvas.toBuffer(), {name: attachmentName})]})
},
details: async()=>{
const DSS = await fetchData(client, interaction, choiceSelector) as FSData;
if (!DSS) return console.log('Endpoint failed - details');
const db = await client.MPServer.findInCache();
const server = db.find(x=>x.serverName === choiceSelector);
// 30d mark
ctx.setLineDash([8, 16]);
ctx.beginPath();
const lastMonthStart = graphOrigin[0] + (nodeWidth * (data.length - 60));
ctx.lineTo(lastMonthStart, graphOrigin[1]);
ctx.lineTo(lastMonthStart, graphOrigin[1] + graphSize[1]);
ctx.stroke();
ctx.closePath();
ctx.setLineDash([]);
// draw points
ctx.lineWidth = 5;
const gradient = ctx.createLinearGradient(0, graphOrigin[1], 0, graphOrigin[1] + graphSize[1]);
gradient.addColorStop(1 / 16, '#e62c3b'); // Red
gradient.addColorStop(5 / 16, '#ffea00'); // Yellow
gradient.addColorStop(12 / 16, '#57f287'); // Green
let lastCoords: number[] = [];
for (let [i, curPC /* current player count */] of data.entries()) {
if (curPC < 0) curPC = 0;
const x = i * nodeWidth + graphOrigin[0];
const y = ((1 - (curPC / second_graph_top)) * graphSize[1]) + graphOrigin[1];
const nexPC /* next player count */ = data[i + 1];
const prvPC /* previous player count */ = data[i - 1];
ctx.strokeStyle = gradient;
ctx.beginPath();
if (lastCoords.length) ctx.moveTo(lastCoords[0], lastCoords[1]);
// if the line being drawn is horizontal, make it go until it has to go down
if (y === lastCoords[1]) {
let newX = x;
for (let j = i + 1; j <= data.length; j++) {
if (data[j] === curPC) newX += nodeWidth;
else break;
}
ctx.lineTo(newX, y);
} else ctx.lineTo(x, y);
lastCoords = [x, y];
ctx.stroke();
ctx.closePath();
if (curPC !== prvPC || curPC !== nexPC) { // Ball if vertical different to next or prev point
// ball
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(x, y, ctx.lineWidth * 1.3, 0, 2 * Math.PI)
ctx.closePath();
ctx.fill();
};
}
// draw text
ctx.font = '400 ' + textSize + 'px sans-serif';
ctx.fillStyle = 'white';
// highest value
if (!isNaN(previousY.at(-2) as number)) {
const maxx = graphOrigin[0] + graphSize[0] + textSize / 2;
const maxy = (previousY.at(-2) as number) + (textSize / 3);
ctx.fillText((previousY.at(-1) as number).toLocaleString('en-US'), maxx, maxy);
}
// lowest value
const lowx = graphOrigin[0] + graphSize[0] + textSize / 2;
const lowy = graphOrigin[1] + graphSize[1] + (textSize / 3);
ctx.fillText('0 players', lowx, lowy);
// 30d
ctx.fillText('30 min ago', lastMonthStart, graphOrigin[1] - (textSize / 2));
// time ->
const tx = graphOrigin[0] + (textSize / 2);
const ty = graphOrigin[1] + graphSize[1] + (textSize);
ctx.fillText('time ->', tx, ty);
const playerData: string[] = [];
let Color = client.config.embedColor;
if (endpoint.slots.used === endpoint.slots.capacity) Color = client.config.embedColorRed;
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)) 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)}`;
interaction.reply({embeds:[new client.embed().setColor(Color).setTitle(endpoint.server.name.length > 0 ? endpoint.server.name : 'Offline').setDescription(endpoint.slots.used < 1 ? '*No players online*' : playerData.join('\n\n')).setImage('attachment://FSStats.png').setAuthor({name:slot}).setFooter({text: 'Current time: '+ingameTime})], files: [new client.attachmentBuilder(img.toBuffer(),{name:'FSStats.png'})]})
const dEmbed = new client.embed().setColor(client.config.embedColor).setAuthor({name: 'Crossplay server'}).setDescription(MessageTool.concatMessage(
`**Name:** \`${DSS?.server.name.length > 0 ? DSS.server.name : '\u200b'}\``,
`**Password:** \`mf4700\``,
`**Map:** \`${DSS.server.mapName.length > 0 ? DSS.server.mapName : 'No map'}\``,
`**Mods:** [Click here](http://${server.ip}/mods.html) **|** [Direct link](http://${server.ip}/all_mods_download?onlyActive=true)`,
'**Filters:** [Click here](https://discord.com/channels/468835415093411861/468835769092669461/926581585938120724)',
`Please see <#${channels.serverInfo}> for more additional information and rules.`
));
if (DSS.server.name.length < 1) dEmbed.setFooter({text: 'Server is currently offline'});
await interaction.reply({embeds: [dEmbed]})
},
status: async()=>{
if (!endpoint) return console.log('Endpoint failed - status');
try {
if (endpoint.server.name.length > 1){
interaction.reply({embeds: [embed.setTitle('Status/Details').setColor(client.config.embedColor).addFields(
{name: 'Server name', value: `${endpoint?.server.name.length === 0 ? '\u200b' : `\`${endpoint?.server.name}\``}`, inline: true},
{name: 'Players', value: `${endpoint.slots.used} out of ${endpoint.slots.capacity}`, inline: true},
{name: 'Current map', value: `${endpoint?.server.mapName.length === 0 ? '\u200b' : endpoint.server.mapName}`, inline: true},
{name: 'Version', value: `${endpoint?.server.version.length === 0 ? '\u200b' : endpoint.server.version}`, inline: true},
{name: 'In-game Time', value: `${('0' + Math.floor((endpoint.server.dayTime/3600/1000))).slice(-2)}:${('0' + Math.floor((endpoint.server.dayTime/60/1000)%60)).slice(-2)}`, inline: true}
)]})
} else if (endpoint.server.name.length === 0) interaction.reply('Server is currently offline.')
} catch (err){
console.log(err)
interaction.reply('Ah, you caught a rare one... Please notify <@&'+client.config.mainServer.roles.bottech+'>')
}
},
info: async()=>{
if (!endpoint) return console.log('Endpoint failed - info')
if (endpoint.server.name.length < 1) embed.setFooter({text: 'Server is currently offline.'})
interaction.reply({embeds: [embed.setColor(client.config.embedColor).setDescription(MessageTool.concatMessage(
`**Server name**: \`${endpoint?.server.name.length === 0 ? '\u200b' : endpoint.server.name}\``,
'**Password:** `mf4700`',
'**Crossplay server**',
`**Map:** ${endpoint.server.mapName.length < 1 ? 'Null Island' : endpoint.server.mapName}`,
`**Mods:** [Click here](${database[serverSelector].ip}/mods.html) **|** [Direct Download](${database[serverSelector].ip}/all_mods_download?onlyActive=true)`,
'**Filters:** [Click here](https://discord.com/channels/468835415093411861/468835769092669461/926581585938120724)',
'Please see <#543494084363288637> for additional information.'
))]});
},
url: async()=>{
if (client.config.mainServer.id == interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.mainServer.roles.mpmanager) && !interaction.member.roles.cache.has(client.config.mainServer.roles.bottech) && !interaction.member.roles.cache.has(client.config.mainServer.roles.admin)) return MessageTool.youNeedRole(interaction, 'mpmanager');
}
const address = interaction.options.getString('address');
if (!address){
try {
const Url = await client.MPServer.findInCache(interaction.guildId);
if (Url[serverSelector].ip && Url[serverSelector].code) return interaction.reply(Url[serverSelector].ip+'/feed/dedicated-server-stats.json?code='+Url[serverSelector].code)
} catch(err){
Logger.forwardToConsole('error', 'MPDB', err);
interaction.reply(`\`\`\`${err}\`\`\``)
}
} else {
if (!address.match(/dedicated-server-stats/)) return interaction.reply('The URL does not match `dedicated-server-stats.xml`');
const newURL = address.replace('xml','json').split('/feed/dedicated-server-stats.json?code=');
try {
Logger.forwardToConsole('log', 'MPDB', `${serverSelector}\'s URL for ${interaction.guild.name} has been updated by ${interaction.member.displayName} (${interaction.member.id})`);
const affected = await client.MPServer._content.findByIdAndUpdate({_id: interaction.guildId}, {$set: {[serverSelector]: {ip: newURL[0], code: newURL[1]}}})
if (affected) return interaction.reply('URL successfully updated.')
} catch (err) {
Logger.forwardToConsole('log', 'MPDB', `${serverSelector}\'s URL for ${interaction.guild.name} has been created by ${interaction.member.displayName} (${interaction.member.id})`);
await client.MPServer._content.create({_id: interaction.guildId, [serverSelector]: { ip: newURL[0], code: newURL[1] }})
.then(()=>interaction.reply('This server doesn\'t have any data in the database, therefore I have created it for you.'))
.catch((err:Error)=>interaction.reply(`I got hit by a flying brick while trying to populate the server data:\n\`\`\`${err.message}\`\`\``))
}
}
const DSS = await fetchData(client, interaction, choiceSelector) as FSData;
if (!DSS) return console.log('Endpoint failed - status');
if (DSS.server.name.length > 0) {
await interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).addFields(
{name: 'Name', value: `\`${DSS?.server.name}\``, inline: true},
{name: 'Players', value: `${DSS.slots.used}/${DSS.slots.capacity}`, inline: true},
{name: 'Map', value: DSS?.server.mapName, inline: true}
).setFooter({text: `Version: ${DSS?.server.version} | Time: ${`${('0'+Math.floor((DSS?.server.dayTime/3600/1000))).slice(-2)}:${('0'+Math.floor((DSS?.server.dayTime/60/1000)%60)).slice(-2)}`}`})]})
} else return interaction.reply('Server is currently offline.')
},
pallets: async()=>{
if (!endpoint) return console.log('Endpoint failed - pallets');
const filter = endpoint.vehicles.filter(v=>v.type === 'pallet');
if (filter.length < 1) return interaction.reply('There are no pallets on the server.');
else interaction.reply(`There are currently ${filter.length} pallets on the server. Here\'s the breakdown:\`\`\`\n${Object.values(PalletLibrary(endpoint)).map(t=>`${t.name.padEnd(12)}${t.size}`).join('\n')}\`\`\``)
const DSS = await fetchData(client, interaction, choiceSelector) as FSData;
if (!DSS) return console.log('Endpoint failed - pallets');
const filter = DSS?.vehicles.filter(x=>x.category === 'PALLETS');
if (filter.length < 1) return interaction.reply('No pallets found on the server.');
else {
const getLongestName = Object.entries(PalletLibrary(DSS)).map(([name, _])=>name.length).sort((a,b)=>b-a)[0];
await interaction.reply(MessageTool.concatMessage(
`There are currently **${filter.length}** pallets on the server. Here\'s the breakdown:\`\`\``,
Object.entries(PalletLibrary(DSS)).map(([name, count])=>`${name.padEnd(getLongestName+3)}${count}`).join('\n'),
'```'
))
}
},
maintenance: async()=>{
if (client.config.dcServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.dcServer.roles.mpmod) && !interaction.member.roles.cache.has(client.config.dcServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'mpmod');
}
const reason = interaction.options.getString('reason');
const channel = interaction.guild.channels.cache.get(channels.activePlayers) as Discord.TextChannel;
const embed = new client.embed().setColor(client.config.embedColor).setAuthor({name: interaction.member.displayName, iconURL: interaction.member.displayAvatarURL({size:1024})}).setTimestamp();
if (channel.permissionsFor(interaction.guildId).has('SendMessages')) {
channel.permissionOverwrites.edit(interaction.guildId, {SendMessages: false}, {type: 0, reason: `Locked by ${interaction.member.displayName}`});
channel.send({embeds: [embed.setTitle('🔒 Locked').setDescription(`**Reason:**\n${reason}`)]});
interaction.reply({content: `${MessageTool.formatMention(channels.activePlayers, 'channel')} locked successfully`, ephemeral: true});
} else {
channel.permissionOverwrites.edit(interaction.guildId, {SendMessages: true}, {type: 0, reason: `Unlocked by ${interaction.member.displayName}`});
channel.send({embeds: [embed.setTitle('🔓 Unlocked').setDescription(`**Reason:**\n${reason}`)]});
interaction.reply({content: `${MessageTool.formatMention(channels.activePlayers, 'channel')} unlocked successfully`, ephemeral: true});
}
},
start: async()=>{
if (client.config.dcServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.dcServer.roles.mpmod) && !interaction.member.roles.cache.has(client.config.dcServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'mpmod');
}
const map_names = interaction.options.getString('map_names', true).split('|');
if (map_names.length > 10) return interaction.reply('You can only have up to 10 maps in a poll!');
const msg = await (interaction.guild.channels.cache.get(channels.announcements) as Discord.TextChannel).send({content: MessageTool.formatMention(client.config.dcServer.roles.mpplayer, 'role'), embeds: [
new client.embed()
.setColor(client.config.embedColor)
.setTitle('Vote for next map!')
.setDescription(map_names.map((map,i)=>`${i+1}. **${map}**`).join('\n'))
.setFooter({text: `Poll started by ${interaction.user.tag}`, iconURL: interaction.member.displayAvatarURL({extension: 'webp', size: 1024})})
], allowedMentions: {parse: ['roles']}});
await interaction.reply(`Successfully created a poll in <#${channels.announcements}>`)
this.reactionSystem(msg, map_names.length);
},
end: async()=>{
if (client.config.dcServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.dcServer.roles.mpmod) && !interaction.member.roles.cache.has(client.config.dcServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'mpmod');
}
const msg = await (interaction.guild.channels.cache.get(channels.announcements) as Discord.TextChannel).messages.fetch(interaction.options.getString('message_id', true));
if (!msg) return interaction.reply('Message not found, please make sure you have the correct message ID.');
if (msg.embeds[0].title !== 'Vote for next map!') return interaction.reply('This message is not a poll!');
if (msg.embeds[0].footer?.text?.startsWith('Poll ended by')) return interaction.reply('This poll has already ended!');
const pollResults = Buffer.from(JSON.stringify({
map_names: msg.embeds[0].description.split('\n').map(x=>x.slice(3)),
votes: msg.reactions.cache.map(x=>x.count)
}, null, 2));
(client.channels.cache.get(client.config.dcServer.channels.mpmod_chat) as Discord.TextChannel).send({files: [new client.attachment(pollResults, {name: `pollResults-${msg.id}.json`})]});
msg.edit({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Voting has ended!').setDescription('The next map will be '+msg.embeds[0].description.split('\n')[msg.reactions.cache.map(x=>x.count).indexOf(Math.max(...msg.reactions.cache.map(x=>x.count)))].slice(3)).setFooter({text: `Poll ended by ${interaction.user.tag}`, iconURL: interaction.member.displayAvatarURL({extension: 'webp', size: 1024})})]}).then(()=>msg.reactions.removeAll());
await interaction.reply(`Successfully ended the [poll](<https://discord.com/channels/${interaction.guildId}/${channels.announcements}/${msg.id}>) in <#${channels.announcements}>`)
},
maps: async()=>{
if (client.config.dcServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.dcServer.roles.mpmod) && !interaction.member.roles.cache.has(client.config.dcServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'mpmod');
}
const suggestionPool = await (interaction.guild.channels.cache.get(client.config.dcServer.channels.mpmod_chat) as Discord.TextChannel).messages.fetch('1141293129673232435');
interaction.reply({embeds: [suggestionPool.embeds[0]]});
}, // Server management group
create_server: async()=>{
if (client.config.dcServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.dcServer.roles.mpmanager) && !interaction.member.roles.cache.has(client.config.dcServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'mpmanager');
}
const dedicatedServerStatsURL = interaction.options.getString('dss-url');
if (!dedicatedServerStatsURL) {
const fetchUrls = await client.MPServer.findInCache();
const urlByName = fetchUrls.find(x=>x.serverName === choiceSelector);
if (urlByName) return await interaction.reply(`http://${urlByName.ip}/feed/dedicated-server-stats.json?code=${urlByName.code}`);
} else {
if (!dedicatedServerStatsURL.match(/http.*dedicated-server-stats/)) return interaction.reply(`Improper URL provided, you sent: \`${dedicatedServerStatsURL}\`\nFormat: \`http://<ip>:<port>/feed/dedicated-server-stats.xml?code=<MD5-Code>\`\nI can accept either XML or JSON variants, no need to panic.`);
const stripURL = dedicatedServerStatsURL.replace(/http:\/\//, '').replace(/\.xml|\.json/g, '.json').split('/feed/dedicated-server-stats.json?code=')
const stripped = {
ip: stripURL[0],
code: stripURL[1]
};
Logger.console('log', logPrefix, `Updating the IP for "${choiceSelector}" to ${stripped.ip}`)
await client.MPServer.addServer(choiceSelector, stripped.ip, stripped.code);
await interaction.reply(`**${choiceSelector}**'s entry has been successfully created!`);
}
},
remove_server: async()=>{
if (client.config.dcServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.dcServer.roles.mpmanager) && !interaction.member.roles.cache.has(client.config.dcServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'mpmanager');
}
try {
Logger.console('log', logPrefix, `Removing "${choiceSelector}" from database`)
await client.MPServer.removeServer(choiceSelector);
await interaction.reply(`**${choiceSelector}**'s entry has been successfully removed!`);
} catch {
Logger.console('log', logPrefix, `Failed to remove "${choiceSelector}", it probably does not exist or something went very wrong`)
await interaction.reply(`**${choiceSelector}**'s entry does not exist!`);
}
},
visibility_toggle: async()=>{
if (client.config.dcServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.dcServer.roles.mpmanager) && !interaction.member.roles.cache.has(client.config.dcServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'mpmanager');
}
const toggleFlag = interaction.options.getBoolean('is_active');
const submitFlagUpdate = await client.MPServer.toggleServerUsability(choiceSelector, toggleFlag);
Logger.console('log', logPrefix, `Toggling isActive flag for ${choiceSelector} to ${toggleFlag}`);
let visibilityTxt = `**${choiceSelector}** is now `;
if (toggleFlag) {
submitFlagUpdate
await interaction.reply(visibilityTxt += 'visible to public');
} else if (!toggleFlag) {
submitFlagUpdate
await interaction.reply(visibilityTxt += 'hidden from public');
}
}
})[interaction.options.getSubcommand()]();
},
data: new Discord.SlashCommandBuilder()
.setName('mp')
.setDescription('Display MP status and other things')
.addSubcommand(x=>x
.setName('status')
.setDescription('Display server status')
.addStringOption(x=>x
.setName('server')
.setDescription('The server to update')
.setRequired(true)
.setChoices(...serverChoices)))
.addSubcommand(x=>x
.setName('players')
.setDescription('Display players on server')
.addStringOption(x=>x
.setName('server')
.setDescription('The server to display players for')
.setRequired(true)
.setChoices(...serverChoices)))
.addSubcommand(x=>x
.setName('url')
.setDescription('View or update the server URL')
.addStringOption(x=>x
.setName('server')
.setDescription('The server to update')
.setRequired(true)
.setChoices(...serverChoices))
.addStringOption(x=>x
.setName('address')
.setDescription('The URL to the dedicated-server-stats.json file')
.setRequired(false)))
.addSubcommand(x=>x
.setName('info')
.setDescription('Display server information')
.addStringOption(x=>x
.setName('server')
.setDescription('The server to display information for')
.setRequired(true)
.setChoices(...serverChoices)))
.addSubcommand(x=>x
.setName('pallets')
.setDescription('Check total amount of pallets on the server')
.addStringOption(x=>x
.setName('server')
.setDescription('The server to get amount of pallets from')
.setRequired(true)
.setChoices(...serverChoices)))
})[interaction.options.getSubcommand() ?? interaction.options.getSubcommandGroup()]();
}
private static async reactionSystem(message:Discord.Message, length:number) {
const numbersArr = ['1⃣','2⃣','3⃣','4⃣','5⃣','6⃣','7⃣','8⃣','9⃣','🔟'];
await Promise.all(numbersArr.slice(0, length).map(emote=>message.react(emote)));
}
static data = new Discord.SlashCommandBuilder()
.setName('mp')
.setDescription('Get information from the FSMP server(s)')
.addSubcommand(x=>x
.setName('players')
.setDescription('Fetches the player list from the requested server')
.addStringOption(x=>x
.setName('server')
.setDescription('The server to fetch the player list from')
.setAutocomplete(true)
.setRequired(true)))
.addSubcommand(x=>x
.setName('details')
.setDescription('Fetches the information about the requested server')
.addStringOption(x=>x
.setName('server')
.setDescription('The server to fetch the information from')
.setAutocomplete(true)
.setRequired(true)))
.addSubcommand(x=>x
.setName('status')
.setDescription('Display the status of the requested server')
.addStringOption(x=>x
.setName('server')
.setDescription('The server to fetch the status from')
.setAutocomplete(true)
.setRequired(true)))
.addSubcommand(x=>x
.setName('pallets')
.setDescription('Fetches how many pallets are on the requested server')
.addStringOption(x=>x
.setName('server')
.setDescription('The server to fetch the pallet count from')
.setAutocomplete(true)
.setRequired(true)))
.addSubcommandGroup(x=>x
.setName('server_mgmnt')
.setDescription('Manage the server entries in database, e.g toggling server visiblity, adding/removing, etc.')
.addSubcommand(x=>x
.setName('create_server')
.setDescription('View or update the URL for the requested server')
.addStringOption(x=>x
.setName('server')
.setDescription('The server to create or update')
.setAutocomplete(true)
.setRequired(true))
.addStringOption(x=>x
.setName('dss-url')
.setDescription('The URL to the dedicated-server-stats')
.setRequired(false)))
.addSubcommand(x=>x
.setName('remove_server')
.setDescription('Remove the requested server from database')
.addStringOption(x=>x
.setName('server')
.setDescription('The server to be removed')
.setAutocomplete(true)
.setRequired(true)))
.addSubcommand(x=>x
.setName('visibility_toggle')
.setDescription('Toggle isActive flag for the requested server')
.addStringOption(x=>x
.setName('server')
.setDescription('The server to toggle the flag')
.setAutocomplete(true)
.setRequired(true))
.addBooleanOption(x=>x
.setName('is_active')
.setDescription('Whether to hide or show the server from the public view')
.setRequired(true))))
.addSubcommand(x=>x
.setName('maintenance')
.setDescription('Toggle the maintenance mode for #mp-active-players channel')
.addStringOption(x=>x
.setName('reason')
.setDescription('The message to send to the channel after toggling')
.setRequired(true)))
.addSubcommandGroup(x=>x
.setName('poll')
.setDescription('Create or end a map poll in #mp-announcements channel')
.addSubcommand(x=>x
.setName('start')
.setDescription('Start a map poll')
.addStringOption(x=>x
.setName('map_names')
.setDescription('Map names separated by |\'s, up to 10 maps!')
.setRequired(true)))
.addSubcommand(x=>x
.setName('end')
.setDescription('End a map poll')
.addStringOption(x=>x
.setName('message_id')
.setDescription('Message ID of the poll')
.setRequired(true)))
.addSubcommand(x=>x
.setName('maps')
.setDescription('Fetch the list of maps currently in the suggestion pool')))
}

View File

@ -1,11 +1,11 @@
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'>){
import Punish from '../components/Punish.js';
export default class Mute {
static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
Punish(client, interaction, 'mute');
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('mute')
.setDescription('Mute a member')
.addUserOption(x=>x
@ -18,4 +18,4 @@ export default {
.addStringOption(x=>x
.setName('reason')
.setDescription('Reason for the mute'))
}
}

View File

@ -1,13 +1,20 @@
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: \`${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()
import {fetch} from 'undici';
import Formatters from '../helpers/Formatters.js';
export default class Ping {
static async run(client:TClient, interaction:Discord.ChatInputCommandInteraction<'cached'>) {
const expectedUptime:number = 16300;
if (client.uptime < expectedUptime) return interaction.reply(`I just restarted, try again in <t:${Math.round((Date.now() + expectedUptime - client.uptime) / 1000)}:R>`);
const timeOpt = {longNames: false, commas: true};
const apiResp = (await fetch('https://discordstatus.com/metrics-display/5k2rt9f7pmny/day.json')).json();
const msg = await interaction.reply({content: 'Pinging...', fetchReply: true});
msg.edit({content: null, embeds:[new client.embed().setColor('#7e96fd').addFields(
{name: 'Discord', value: Formatters.timeFormat(await apiResp.then((data:any)=>data.metrics[0].summary.mean.toFixed(0)), 3, timeOpt), inline: true},
{name: 'WebSocket', value: Formatters.timeFormat(client.ws.ping, 3, timeOpt), inline: true}
)]})
}
static data = new Discord.SlashCommandBuilder()
.setName('ping')
.setDescription('Check latency between bot and Discord API')
}

View File

@ -1,65 +0,0 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import {writeFileSync, existsSync, mkdirSync} from 'node:fs';
import MessageTool from '../helpers/MessageTool.js';
export default {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
if (client.config.mainServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.mainServer.roles.mpmod) && !interaction.member.roles.cache.has(client.config.mainServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'mpmod');
}
const channelId = '1084864116776251463'; // #mp-announcements
({
start: async()=>{
const map_names = interaction.options.getString('map_names', true).split('|');
if (map_names.length > 10) return interaction.reply('You can only have up to 10 maps in a poll!');
const msg = await (interaction.guild.channels.cache.get(channelId) as Discord.TextChannel).send({content: MessageTool.formatMention(client.config.mainServer.roles.mpplayer, 'role'), embeds: [
new client.embed()
.setColor(client.config.embedColor)
.setTitle('Vote for next map!')
.setDescription(map_names.map((map,i)=>`${i+1}. **${map}**`).join('\n'))
.setFooter({text: `Poll started by ${interaction.user.tag}`, iconURL: interaction.member.displayAvatarURL({extension: 'webp', size: 1024})})
], allowedMentions: {parse: ['roles']}});
await interaction.reply(`Successfully created a poll in <#${channelId}>`)
const numbers = ['1⃣','2⃣','3⃣','4⃣','5⃣','6⃣','7⃣','8⃣','9⃣','🔟'];
for (let i = 0; i < map_names.length; i++) await msg.react(numbers[i])
},
end: async()=>{
const msg = await (interaction.guild.channels.cache.get(channelId) as Discord.TextChannel).messages.fetch(interaction.options.getString('message_id', true));
if (!msg) return interaction.reply('Message not found, please make sure you have the correct message ID.');
if (msg.embeds[0].title !== 'Vote for next map!') return interaction.reply('This message is not a poll!');
if (msg.embeds[0].footer?.text?.startsWith('Poll ended by')) return interaction.reply('This poll has already ended!');
if (msg.reactions.cache.size < 2) return interaction.reply('This poll has not been voted on yet!');
if (!existsSync('src/database/polls')) mkdirSync('src/database/polls');
writeFileSync(`src/database/polls/pollResults-${msg.id}.json`, JSON.stringify({
map_names: msg.embeds[0].description.split('\n').map(x=>x.slice(3)),
votes: msg.reactions.cache.map(x=>x.count)
}, null, 2));
(client.channels.cache.get('516344221452599306') as Discord.TextChannel).send({files: [`src/database/polls/pollResults-${msg.id}.json`]});
msg.edit({embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Voting has ended!').setDescription('The next map will be '+msg.embeds[0].description.split('\n')[msg.reactions.cache.map(x=>x.count).indexOf(Math.max(...msg.reactions.cache.map(x=>x.count)))].slice(3)).setFooter({text: `Poll ended by ${interaction.user.tag}`, iconURL: interaction.member.displayAvatarURL({extension: 'webp', size: 1024})})]});
await interaction.reply(`Successfully ended the [poll](<https://discord.com/channels/${interaction.guildId}/${channelId}/${msg.id}>) in <#${channelId}>`)
}
})[interaction.options.getSubcommand()]();
},
data: new Discord.SlashCommandBuilder()
.setName('poll')
.setDescription('Poll system for FSMP server')
.addSubcommand(x=>x
.setName('start')
.setDescription('Start a poll')
.addStringOption(x=>x
.setName('map_names')
.setDescription('Map names separated by |\'s, up to 10 maps')
.setRequired(true)))
.addSubcommand(x=>x
.setName('end')
.setDescription('End a poll')
.addStringOption(x=>x
.setName('message_id')
.setDescription('Message ID of the poll')
.setRequired(true)))
}

View File

@ -0,0 +1,70 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
export default class ProhibitedWords {
static async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
if (!MessageTool.isStaff(interaction.member)) return MessageTool.youNeedRole(interaction, 'admin');
const word = interaction.options.getString('word');
const wordExists = await client.prohibitedWords.findWord(word);
({
view: async()=>{
const pwList = await client.prohibitedWords.getAllWords();
interaction.reply({
ephemeral: true,
content: `There are currently **${pwList.length}** words in the list`,
files: [
new client.attachment(Buffer.from(JSON.stringify(pwList.map(x=>x.dataValues.word), null, 2)), {name: 'pwDump.json'})
]
})
},
add: async()=>{
if (wordExists) return interaction.reply({ephemeral: true, content: `\`${word}\` already exists in the list`});
else {
await client.prohibitedWords.insertWord(word);
interaction.reply({ephemeral: true, content: `Successfully added \`${word}\` to the list`});
}
},
remove: async()=>{
if (!wordExists) return interaction.reply({ephemeral: true, content: `\`${word}\` does not exist in the list`});
else {
await client.prohibitedWords.removeWord(word);
interaction.reply({ephemeral: true, content: `Successfully removed \`${word}\` from the list`});
}
},
import: async()=>{
const file = interaction.options.getAttachment('file', true);
if (!file.contentType.match(/application\/json/)) return interaction.reply({ephemeral: true, content: 'This file is not a JSON file!'});
const success = await client.prohibitedWords.importWords(file.url);
if (success) interaction.reply({ephemeral: true, content: `Successfully imported the list from \`${file.name}\` into the database`});
else interaction.reply({ephemeral: true, content: `Failed to import the list from \`${file.name}\` into the database`});
}
} as any)[interaction.options.getSubcommand()]();
}
static data = new Discord.SlashCommandBuilder()
.setName('pw')
.setDescription('Manage the database of prohibited words')
.addSubcommand(x=>x
.setName('view')
.setDescription('View the list of currently banned words'))
.addSubcommand(x=>x
.setName('add')
.setDescription('Add the word to the list')
.addStringOption(x=>x
.setName('word')
.setDescription('Add the specific word to automod\'s prohibitedWords database')
.setRequired(true)))
.addSubcommand(x=>x
.setName('remove')
.setDescription('Remove the word from the list')
.addStringOption(x=>x
.setName('word')
.setDescription('Remove the specific word from automod\'s prohibitedWords database')
.setRequired(true)))
.addSubcommand(x=>x
.setName('import')
.setDescription('Import a JSON file of words into the database')
.addAttachmentOption(x=>x
.setName('file')
.setDescription('The JSON file to import')
.setRequired(true)))
}

View File

@ -1,11 +1,9 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
export default {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
if (client.config.mainServer.id === interaction.guildId) {
if (!MessageTool.isStaff(interaction.member)) return MessageTool.youNeedRole(interaction, 'dcmod');
}
export default class Purge {
static async run(_client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
if (!MessageTool.isStaff(interaction.member)) return MessageTool.youNeedRole(interaction, 'dcmod');
const amount = interaction.options.getInteger('amount') as number;
if (amount > 100) return interaction.reply({content: 'Discord API limits purging up to 100 messages.', ephemeral: true})
const user = interaction.options.getUser('user');
@ -23,8 +21,8 @@ export default {
})
}
await interaction.reply({content: `Successfully purged ${amount} messages.`, ephemeral: true})
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('purge')
.setDescription('Purge the amount of messages in this channel')
.addIntegerOption(x=>x

View File

@ -1,20 +1,20 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
import path from 'node:path';
import {readFileSync} from 'node:fs';
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({});
import CanvasBuilder from '../components/CanvasGraph.js';
export default class Rank {
static async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
if (interaction.guildId !== client.config.dcServer.id) return interaction.reply({content: 'This command doesn\'t work in this server.', ephemeral: true});
// const allData = await client.userLevels._content.find();
const allData = await client.userLevels.fetchEveryone();
({
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 the rank data from the users instead.');
// information about users progress on level roles
const userData = await client.userLevels._content.findById(member.user.id);
// const userData = await client.userLevels._content.findById(member.user.id);
const userData = await client.userLevels.fetchUser(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;
@ -22,150 +22,45 @@ export default {
};
if (!userData) return interaction.reply(`${pronounBool('You', 'They')} currently don't have a level, send some messages to level up.`)
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.toLocaleString('en-US')}**`).setThumbnail(member.avatarURL({extension:'png',size:1024}) || member.user.avatarURL({extension:'png',size:1024}) || member.user.defaultAvatarURL).setFooter({text: userData.notificationPing === true ? 'Ping notification enabled' : 'Ping notification disabled'})]})
const index = allData.sort((a, b) => b.messages - a.messages).map(x=>x.dataValues.id).indexOf(member.id) + 1;
const memberDifference = userData.dataValues.messages - client.userLevels.algorithm(userData.dataValues.level);
const levelDifference = client.userLevels.algorithm(userData.dataValues.level+1) - client.userLevels.algorithm(userData.dataValues.level);
let ptText = 'Ping toggle ';
interaction.reply({embeds: [new client.embed().setColor(member.displayColor).setTitle(`Level: **${userData.dataValues.level}**\nRank: **${index ? '#' + index : 'last'}**\nProgress: **${memberDifference}/${levelDifference} (${(memberDifference/levelDifference*100).toFixed(2)}%)**\nTotal: **${userData.dataValues.messages/* .toLocaleString('en-US') */}**`).setThumbnail(member.avatarURL({extension:'png',size:1024}) || member.user.avatarURL({extension:'png',size:1024}) || member.user.defaultAvatarURL).setFooter({text: userData.pingToggle === true ? ptText += 'enabled' : ptText += 'disabled'})]})
},
leaderboard: ()=>{
const data = JSON.parse(readFileSync(path.join('./src/database/dailyMsgs.json'), 'utf8')).map((x: Array<number>, i: number, a: any) => {
leaderboard: async()=>{
const data = (await client.dailyMsgs.fetchDays()).map(x=>[x.dataValues.day, x.dataValues.total]).sort((a,b)=>a[0]-b[0]).slice(-60).map((x: number[], i: number, a: any)=>{
return x[1] - ((a[i - 1] || [])[1] || x[1])
}).slice(1).slice(-60);
});
if (data.length < 3) return interaction.reply('Not enough data to generate graph.');
// handle negative days
data.forEach((change: number, i: number) => {
if (change < 0) data[i] = data[i - 1] || data[i + 1] || 0;
});
const maxValue = Math.max(...data);
const maxValueArr = maxValue.toString().split('');
const first_graph_top = Math.ceil(maxValue * 10 ** (-maxValueArr.length + 1)) * 10 ** (maxValueArr.length - 1);
const second_graph_top = Math.ceil(maxValue * 10 ** (-maxValueArr.length + 2)) * 10 ** (maxValueArr.length - 2);
const textSize = 32;
const img = canvas.createCanvas(1200, 600);
const ctx = img.getContext('2d');
const graphOrigin = [25, 50];
const graphSize = [1020, 500];
const nodeWidth = graphSize[0] / (data.length - 1);
ctx.fillStyle = '#36393f'; //'#111111';
ctx.fillRect(0, 0, img.width, img.height);
// grey horizontal lines
ctx.lineWidth = 3;
let interval_candidates = [];
for (let i = 4; i < 10; i++) {
const interval = first_graph_top / i;
if (Number.isInteger(interval)) {
let intervalString = interval.toString();
const reference_number = i * Math.max(intervalString.split('').filter(x => x === '0').length / intervalString.length, 0.3) * (['1', '2', '4', '5', '6', '8'].includes(intervalString[0]) ? 1.5 : 0.67)
interval_candidates.push([interval, i, reference_number]);
}
}
const chosen_interval = interval_candidates.sort((a, b) => b[2] - a[2])[0];
let previousY: Array<number> = [];
ctx.strokeStyle = '#202225'; //'#555B63';
if (chosen_interval === undefined) return interaction.reply({content: MessageTool.concatMessage(
'No data to display for now. It is also possible that the following either happened:',
'- No interval was found for the graph. This is likely due to the fact that there is not enough data to generate a graph.',
'- The level system was recently reset.',
'- The graph is currently being generated in the background. Please try again in a few minutes.',
'If you believe this is a mistake, please contact **Toast** or the **Discord Moderation** team.'
), ephemeral: true});
for (let i = 0; i <= chosen_interval[1]; i++) {
const y = graphOrigin[1] + graphSize[1] - (i * (chosen_interval[0] / second_graph_top) * graphSize[1]);
if (y < graphOrigin[1]) continue;
const even = ((i + 1) % 2) === 0;
if (even) ctx.strokeStyle = '#2c2f33'; //'#3E4245';
ctx.beginPath();
ctx.lineTo(graphOrigin[0], y);
ctx.lineTo(graphOrigin[0] + graphSize[0], y);
ctx.stroke();
ctx.closePath();
if (even) ctx.strokeStyle = '#202225'; //'#555B63';
previousY = [y, i * chosen_interval[0]];
}
// 30d mark
ctx.setLineDash([8, 16]);
ctx.beginPath();
const lastMonthStart = graphOrigin[0] + (nodeWidth * (data.length - 30));
ctx.lineTo(lastMonthStart, graphOrigin[1]);
ctx.lineTo(lastMonthStart, graphOrigin[1] + graphSize[1]);
ctx.stroke();
ctx.closePath();
ctx.setLineDash([]);
// draw points
ctx.strokeStyle = client.config.embedColor as string;
ctx.fillStyle = client.config.embedColor as string;
ctx.lineWidth = 4;
function getYCoordinate(value: number) {
return ((1 - (value / second_graph_top)) * graphSize[1]) + graphOrigin[1];
}
let lastCoords: Array<number> = [];
data.forEach((val: number, i: number) => {
ctx.beginPath();
if (lastCoords) ctx.moveTo(lastCoords[0], lastCoords[1]);
if (val < 0) val = 0;
const x = i * nodeWidth + graphOrigin[0];
const y = getYCoordinate(val);
ctx.lineTo(x, y);
lastCoords = [x, y];
ctx.stroke();
ctx.closePath();
// ball
ctx.beginPath();
ctx.arc(x, y, ctx.lineWidth * 1.2, 0, 2 * Math.PI)
ctx.closePath();
ctx.fill();
});
// draw text
ctx.font = '400 ' + textSize + 'px sans-serif';
ctx.fillStyle = 'white';
// highest value
ctx.fillText(previousY[1].toLocaleString('en-US'), graphOrigin[0] + graphSize[0] + textSize, previousY[0] + (textSize / 3));
// lowest value
ctx.fillText('0 msgs', graphOrigin[0] + graphSize[0] + textSize, graphOrigin[1] + graphSize[1] + (textSize / 3));
// 30d
ctx.fillText('30d ago', lastMonthStart, graphOrigin[1] - (textSize / 3));
// time ->
ctx.fillText('time ->', graphOrigin[0] + (textSize / 2), graphOrigin[1] + graphSize[1] + (textSize));
interaction.reply({embeds: [
new client.embed().setTitle('Ranking leaderboard')
.setDescription(`Level System was created **${Math.floor((Date.now()-client.config.LRSstart)/1000/60/60/24)}** days ago. Since then, a total of **${allData.reduce((a, b)=>a+b.messages, 0).toLocaleString('en-US')}** messages have been sent in this server.`)
.addFields({name: 'Top users by messages sent:', value: 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')})
.setImage('attachment://dailymsgs.png').setColor(client.config.embedColor)
.setFooter({text: 'Graph updates daily.'})
], files: [new client.attachmentBuilder(img.toBuffer(),{name: 'dailymsgs.png'})]})
const graph = await new CanvasBuilder().generateGraph(data, 'leaderboard');
interaction.reply({
embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Leaderboard')
.setDescription(MessageTool.concatMessage(
`Level System was created **${Math.floor((Date.now()-client.config.LRSstart)/1000/60/60/24)}** days ago.`,
`Since then, a total of **${allData.reduce((a, b)=>a+b.messages, 0).toLocaleString('en-US')}** messages have been sent in this server.`
)).addFields({
name: 'Top users sorted by messages sent:',
value: allData.sort((a,b)=>b.messages - a.messages).slice(0,15).map((x,i)=>`${i+1}. <@${x.dataValues.id}>: ${x.messages.toLocaleString('en-US')}`).join('\n')
}).setImage('attachment://dailyMessages.jpg').setFooter({text: 'Graph updates daily'})],
files: [new client.attachment(graph.toBuffer(),{name: 'dailyMessages.jpg'})]
})
},
notification: async()=>{
const findUserInMongo = await client.userLevels._content.findById(interaction.user.id);
const findUserInDatabase = await client.userLevels.fetchUser(interaction.user.id);
const textDeco = ' be pinged for level-up notification in the future.'
if (!findUserInMongo.notificationPing) {
await findUserInMongo.updateOne({_id: interaction.user.id, notificationPing: true})
if (!findUserInDatabase.pingToggle) {
await findUserInDatabase.update({pingToggle: true}, {where: {id: interaction.user.id}})
interaction.reply({content: 'You will'+textDeco, ephemeral: true})
} else if (findUserInMongo.notificationPing) {
await findUserInMongo.updateOne({_id: interaction.user.id, notificationPing: false})
} else if (findUserInDatabase.pingToggle) {
await findUserInDatabase.update({pingToggle: false}, {where: {id: interaction.user.id}})
interaction.reply({content: 'You won\'t'+textDeco, ephemeral: true})
}
}
} as any)[interaction.options.getSubcommand()]();
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('rank')
.setDescription('Level system')
.addSubcommand(x=>x

View File

@ -1,7 +1,7 @@
import Discord from 'discord.js';
import TClient from '../client.js';
export default {
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
export default class RoleInfo {
static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
const role = interaction.options.getRole('role') as Discord.Role;
const permissions = role.permissions.toArray();
interaction.reply({embeds: [new client.embed().setColor(role.color || '#fefefe').setThumbnail(role?.iconURL()).setTitle(`Role Info: ${role.name}`).addFields(
@ -10,13 +10,13 @@ export default {
{name: '🔹 Creation Date', value: `<t:${Math.round(role.createdTimestamp/1000)}>\n<t:${Math.round(role.createdTimestamp/1000)}:R>`, inline: true},
{name: '🔹 Misc', value: `Hoist: \`${role.hoist}\`\nMentionable: \`${role.mentionable}\`\nPosition: \`${role.position}\` from bottom\nMembers: \`${role.members.size}\`\n${role.members.size < 21 ? role.members.map((e:Discord.GuildMember)=>`**${e.user.username}**`).join('\n') || '' : ''}`, inline: true},
{name: '🔹 Permissions', value: `${permissions.includes('Administrator') ? ['Administrator'] : permissions.join(', ').replace(/([a-z])([A-Z])/g, '$1 $2') || 'No permissions'}`, inline: true}
)]})// https://stackoverflow.com/a/15343790 - For anonymous programmer, you know who I am talking to. You're welcome...
},
data: new Discord.SlashCommandBuilder()
)]})
}
static data = new Discord.SlashCommandBuilder()
.setName('roleinfo')
.setDescription('View information about the selected role')
.addRoleOption(x=>x
.setName('role')
.setDescription('Role name to view information')
.setRequired(true))
}
}

View File

@ -1,11 +1,11 @@
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'>){
import Punish from '../components/Punish.js';
export default class Softban {
static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
Punish(client, interaction, 'softban');
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('softban')
.setDescription('Softban a member from the server')
.addUserOption(x=>x
@ -15,4 +15,4 @@ export default {
.addStringOption(x=>x
.setName('reason')
.setDescription('Reason for the softban'))
}
}

View File

@ -1,94 +1,83 @@
interface CommitHashes {
localHash: string,
remoteHash: string
}
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 TSClient from '../helpers/TSClient.js';
import Formatters from '../helpers/Formatters.js';
import MessageTool from '../helpers/MessageTool.js';
import GitHub from '../helpers/GitHub.js';
import si from 'systeminformation';
import os from 'node:os';
import {Octokit} from '@octokit/rest';
import {createTokenAuth} from '@octokit/auth-token';
import {readFileSync} from 'node:fs';
import {Worker} from 'node:worker_threads';
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
const workerThread = new Worker(new URL('../helpers/CommitHashes.js', import.meta.url));
const hashData = await new Promise<CommitHashes>(resolve=>workerThread.on('message', (data:CommitHashes)=>resolve(data)));
export default {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
const waitForData = await interaction.reply({content: '<a:sakjdfsajkfhsdjhjfsa:1065342869428252743>', fetchReply:true})
const cpu = await si.cpu();
const ram = await si.mem();
const osInfo = await si.osInfo();
const currentLoad = await si.currentLoad();
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: **${FormatTime(client.uptime, 3, {longNames: true, commas: true})}**`);
const nameLength = Math.max(...includedCommands.map(x=>x.command.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'];
includedCommands.forEach(command=>{
const name = command.command.data.name;
const count = command.uses.toString();
rows.push(`${name + ' '.repeat(nameLength - name.length)}${' '.repeat(amountLength - count.length) + count}\n`);
});
const embed = new client.embed().setColor(client.config.embedColor).setTitle('Statistics: Command Usage')
.setDescription(MessageTool.concatMessage(
'List of commands that have been used in this session, ordered by amount of use. Table contains command name and amount of uses.',
`Total amount of commands used in this session: ${client.commands.filter(x=>x.uses).map(x=>x.uses).reduce((a,b)=>a+b, 0)}`
import ts from 'typescript';
import {readFileSync} from 'fs';
export default class Statistics {
static async run(client:TClient, interaction:Discord.ChatInputCommandInteraction<'cached'>) {
const initialMsg = await interaction.reply({content: '<a:sakjdfsajkfhsdjhjfsa:1065342869428252743>', fetchReply:true});
const repoData = await GitHub.LocalRepository();
const embed = new client.embed().setColor(client.config.embedColor).setTitle('Statistics').setDescription(MessageTool.concatMessage(
'This is a list of commands ordered by their names and how many times they had been used in this session.',
'Underneath is a list of main dependencies and their versions as well as the bot/host statistics.'
));
if (rows.join('').length > 1024){
let fieldValue = '';
rows.forEach(row=>{
if (fieldValue.length + row.length > 1024){
embed.addFields({name: '\u200b', value: `\`\`\`\n${fieldValue}\`\`\``});
fieldValue = row;
} else fieldValue += row
});
embed.addFields({name: '\u200b', value: `\`\`\`\n${fieldValue}\`\`\``});
} else embed.addFields({name: '\u200b', value: `\`\`\`\n${rows.join('')}\`\`\``});
const SummonAuthentication = createTokenAuth((await TSClient.Token()).octokit);
const {token} = await SummonAuthentication();
let githubRepo = {owner: 'AnxietyisReal', repo: 'Daggerbot-TS', ref: 'HEAD'};
const octokit = new Octokit({auth: token, timeZone: 'Australia/NSW', userAgent: 'Daggerbot-TS'});
const github = {
remoteCommit: await octokit.repos.getCommit({...githubRepo, ref: hashData.remoteHash}),
localCommit: await octokit.repos.getCommit({...githubRepo, ref: hashData.localHash}),
}
const systemInfo = {
cpu: await si.cpu(),
mem: await si.mem(),
osInfo: await si.osInfo(),
currLoad: await si.currentLoad()
};
const col = ['Command', 'Uses'];
const cmdUses = client.commands.filter(x=>x.uses).sort((a,b)=>b.uses - a.uses);
const nameLen = Math.max(...cmdUses.map(x=>x.command.data.name.length), col[0].length) + 2;
const usesLen = Math.max(...cmdUses.map(x=>x.uses.toString().length), col[1].length) + 1;
const rows = [`${col[0] + ' '.repeat(nameLen-col[0].length)}|${' '.repeat(usesLen-col[1].length) + col[1]}\n`, '-'.repeat(nameLen) + '-'.repeat(usesLen) + '\n'];
cmdUses.forEach(cmd=>{
const name = cmd.command.data.name;
const uses = cmd.uses.toString();
rows.push(`${name+' '.repeat(nameLen-name.length)}${' '.repeat(usesLen-uses.length)+uses}\n`);
});
if (rows.join('').length > 1024) {
let field = '';
rows.forEach(r=>{
if (field.length+r.length > 1024) {
embed.addFields({name: '\u200b', value: `\`\`\`\n${field}\`\`\``});
field = r;
}
});
embed.addFields({name: '\u200b', value: `\`\`\`\n${field}\`\`\``});
} else embed.addFields({name: '\u200b', value: `\`\`\`\n${rows.join('')}\`\`\``});
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
embed.addFields(
{
name: '> __Repository__', value: MessageTool.concatMessage(
`**Local:** [${hashData.localHash}](${github.localCommit.data.html_url})`,
`**Remote:** [${hashData.remoteHash}](${github.remoteCommit.data.html_url})`
)
},
{name: '> __Dependencies__', value: MessageTool.concatMessage(
`**TypeScript:** ${pkg.version}`,
`**NodeJS:** ${process.version}`,
`**DiscordJS:** ${Discord.version}`,
`**Yarn:** ${packageJson.packageManager.slice(5)}`
{name: '🔹 *Dependencies*', value: MessageTool.concatMessage(
`>>> **Yarn:** ${pkg.packageManager.split('@')[1].split('+')[0]}`,
`**Node.js:** ${process.version.slice(1)}`,
`**Discord.js:** ${pkg.dependencies['discord.js']}`,
`**TypeScript:** ${ts.version}`,
`**Postgres:** ${pkg.dependencies.pg}`,
`**Redis:** ${pkg.dependencies.redis}`
)},
{name: '> __Host__', value: MessageTool.concatMessage(
`**Operating System:** ${osInfo.distro + ' ' + osInfo.release}`,
`**CPU:** ${cpu.manufacturer} ${cpu.brand}`,
`**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: ${FormatTime((os.uptime()*1000), 2, {longNames: true, commas: true})}\nBot: ${FormatTime(client.uptime, 2, {commas: true, longNames: true})}`
{name: '🔹 *Host*', value: MessageTool.concatMessage(
`>>> **OS:** ${systemInfo.osInfo.distro} ${systemInfo.osInfo.release}`,
`**CPU:** ${systemInfo.cpu.manufacturer} ${systemInfo.cpu.brand}${systemInfo.cpu.speed} GHz`,
'**RAM**',
`╰ **Host:** ${this.progressBar(systemInfo.mem.used, systemInfo.mem.total)} (${Formatters.byteFormat(systemInfo.mem.used)}/${Formatters.byteFormat(systemInfo.mem.total)})`,
`╰ **Bot:** ${this.progressBar(process.memoryUsage().heapUsed, process.memoryUsage().heapTotal)} (${Formatters.byteFormat(process.memoryUsage().heapUsed)}/${Formatters.byteFormat(process.memoryUsage().heapTotal)})`,
'**Uptime**',
`╰ **Host:** ${Formatters.timeFormat(os.uptime()*1000, 3, {longNames: true, commas: true})}`,
`╰ **Bot:** ${Formatters.timeFormat(process.uptime()*1000, 3, {longNames: true, commas: true})}`,
'**Load Usage**',
`╰ **User:** ${this.progressBar(systemInfo.currLoad.currentLoadUser, 100)} (${systemInfo.currLoad.currentLoadUser.toFixed(2)}%)`,
`╰ **Sys:** ${this.progressBar(systemInfo.currLoad.currentLoadSystem, 100)} (${systemInfo.currLoad.currentLoadSystem.toFixed(2)}%)`
)}
);
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()
).setFooter({text: `Version: ${repoData.hash.slice(0,7)}${repoData.message}`});
initialMsg.edit({content: null, embeds: [embed]});
}
private static progressBar(used:number, total:number):string {
const length:number = 10;
const percent = used/total;
const bar = '▓'.repeat(Math.round(percent*length)) + '░'.repeat(length-Math.round(percent*length));
return `${bar} ${Math.round(percent*100)}%`;
}
static data = new Discord.SlashCommandBuilder()
.setName('statistics')
.setDescription('See a list of commands ordered by their usage or host stats')
.setDescription('List of commands used in current session and host statistics')
}

View File

@ -1,106 +1,77 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
import HookMgr from '../funcs/HookManager.js';
export default {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
const replyInDM = interaction.options.getString('message');
const suggestionIDReply = interaction.options.getString('id');
const suggestionID = (Math.random() + 1).toString(36).substring(5);
const userid = (await client.suggestion._content.findById(suggestionIDReply))?.user._id;
const theirIdea = (await client.suggestion._content.findById(suggestionIDReply))?.idea;
const timeFormatting = client.dayjs().format('DD/MM/YY h:mm A');
const stateChanged = 'Suggestion state has been successfully updated and DM is sent.';
const dmFail = `Failed to send a DM to ${MessageTool.formatMention(userid, 'user')}, they possibly have it turned off or blocked me.\nSuggestion ID: **${suggestionIDReply}**`;
import HookMgr from '../components/HookManager.js';
export default class Suggest {
static async run(client:TClient, interaction:Discord.ChatInputCommandInteraction<'cached'>) {
const idVal = interaction.options.getInteger('id');
({
your: async()=>{
const suggestionText = interaction.options.getString('suggestion');
const suggestionImage = interaction.options.getAttachment('image');
const notifEmbed = new client.embed()
.setColor(client.config.embedColor)
.setTitle(`Suggestion ID: ${suggestionID}`)
.setAuthor({name: interaction.user.username, iconURL: interaction.user.avatarURL({size: 256})})
.setFooter({text: `Timestamp: ${timeFormatting}`})
.setDescription(MessageTool.concatMessage(
'> **Suggestion:**',
suggestionText
));
if (suggestionImage) notifEmbed.setImage(suggestionImage.url);
HookMgr.send(client, 'bot_suggestions', '1079621523561779272', {embeds:[notifEmbed], username: `${client.user.username} Suggestions`, avatarURL: client.user.avatarURL({size:256})}).catch(e=>{
console.log(e.message);
interaction.reply({content: 'Failed to send suggestion, try again later.', ephemeral: true})
});
await client.suggestion._content.create({_id: suggestionID, idea: suggestionText, user: {_id: interaction.user.id, name: interaction.user.username}, state: 'Pending'});
interaction.reply({content: `Suggestion sent, here is your suggestion ID to take note of it: \`${suggestionID}\``, ephemeral: true})
create: async()=>{
const suggestion = interaction.options.getString('suggestion', true);
const newSugg = await client.suggestions.create(interaction.user.id, suggestion);
this.newWebhookMessage(client, newSugg.dataValues.id, suggestion, interaction.user.username);
return interaction.reply({content: `Your suggestion has been sent to bot developers. \`#${newSugg.dataValues.id}\``, ephemeral: true});
},
approve: async()=>{
if (client.config.mainServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.mainServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'bottech');
delete: async()=>{
if (client.config.dcServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.dcServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'bottech');
}
if ((await client.suggestion._content.findById(suggestionIDReply)).state === 'Rejected') return interaction.reply({content: 'This suggestion\'s state is locked and cannot be modified.', ephemeral: true});
(await client.users.fetch(userid)).send({embeds: [new client.embed()
.setColor(client.config.embedColorGreen)
.setAuthor({name: interaction.user.username, iconURL: interaction.user.avatarURL({size: 256})})
.setTitle('Your suggestion has been approved.')
.setDescription(`> **Your suggestion:**\n${theirIdea}\n> **Their message:**\n${replyInDM}`)
.setFooter({text: `Timestamp: ${timeFormatting} | Suggestion ID: ${suggestionIDReply}`})
]}).catch((err:Discord.DiscordjsErrorCodes)=>{if (err) return (client.channels.resolve('1040018521746325586') as Discord.TextChannel).send(dmFail)});
await client.suggestion._content.findByIdAndUpdate(suggestionIDReply, {state: 'Approved'});
return interaction.reply({embeds:[new client.embed().setColor(client.config.embedColorGreen).setTitle(`Suggestion approved | ${suggestionIDReply}`).setDescription(stateChanged)]});
const sugg = await this.deleteSuggestion(client, idVal);
if (sugg) return interaction.reply(`Suggestion \`#${idVal}\` has been deleted.`);
else return interaction.reply(`Suggestion \`#${idVal}\` does not exist.`);
},
reject: async()=>{
if (client.config.mainServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.mainServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'bottech');
update: async()=>{
if (client.config.dcServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.dcServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'bottech');
}
if ((await client.suggestion._content.findById(suggestionIDReply)).state === 'Approved') return interaction.reply({content: 'This suggestion\'s state is locked and cannot be modified.', ephemeral: true});
(await client.users.fetch(userid)).send({embeds: [new client.embed()
.setColor(client.config.embedColorRed)
.setAuthor({name: interaction.user.username, iconURL: interaction.user.avatarURL({size: 256})})
.setTitle('Your suggestion has been rejected.')
.setDescription(`> **Your suggestion:**\n${theirIdea}\n> **Their message:**\n${replyInDM}`)
.setFooter({text: `Timestamp: ${timeFormatting} | Suggestion ID: ${suggestionIDReply}`})
]}).catch((err:Discord.DiscordjsErrorCodes)=>{if (err) return (client.channels.resolve('1040018521746325586') as Discord.TextChannel).send(dmFail)});
await client.suggestion._content.findByIdAndUpdate(suggestionIDReply, {state: 'Rejected'});
return interaction.reply({embeds:[new client.embed().setColor(client.config.embedColorRed).setTitle(`Suggestion rejected | ${suggestionIDReply}`).setDescription(stateChanged)]});
const status = interaction.options.getString('status', true);
await this.updateSuggestion(client, idVal, status as 'Accepted'|'Rejected');
client.users.fetch((await client.suggestions.fetchById(idVal)).dataValues.userid).then(x=>x.send(`Your suggestion \`#${idVal}\` has been updated to \`${status}\` by **${interaction.user.username}**`)).catch(()=>interaction.channel.send(`Unable to send DM to user of suggestion \`#${idVal}\``))
return await interaction.reply(`Suggestion \`#${idVal}\` has been updated to \`${status}\`.`);
}
} as any)[interaction.options.getSubcommand()]();
},
data: new Discord.SlashCommandBuilder()
}
static async updateSuggestion(client:TClient, id:number, status: 'Accepted'|'Rejected') {
return await client.suggestions.updateStatus(id, status);
}
static async deleteSuggestion(client:TClient, id:number) {
return await client.suggestions.delete(id);
}
static newWebhookMessage(client:TClient, id:number, suggestion:string, username:string) {
const hook = new HookMgr(client, 'bot_suggestions', '1079621523561779272');
if (hook) return hook.send({embeds: [new client.embed().setColor(client.config.embedColor).setTitle(`Suggestion #${id}`).setAuthor({name: username}).setDescription(`\`\`\`${suggestion}\`\`\``)]});
else throw new Error('[SUGGESTION-HOOK] Provided webhook cannot be fetched, not sending message.')
}
static data = new Discord.SlashCommandBuilder()
.setName('suggest')
.setDescription('Want to suggest ideas/thoughts to bot techs? Suggest it here')
.setDescription('Want to suggest something to the bot devs? You can do so!')
.addSubcommand(x=>x
.setName('your')
.setDescription('What do you want to suggest?')
.setName('create')
.setDescription('Create a new suggestion for your idea')
.addStringOption(x=>x
.setName('suggestion')
.setDescription('Suggest something to bot techs. (You will be DM\'d by bot if your idea was approved/rejected)')
.setMaxLength(1024)
.setRequired(true))
.addAttachmentOption(x=>x
.setName('image')
.setDescription('If your idea seems complicated or prefer to show what your idea may look like then attach the image.')))
.setDescription('Your precious idea')
.setRequired(true)))
.addSubcommand(x=>x
.setName('approve')
.setDescription('[Bot Tech] Approve the suggestion sent by the user')
.addStringOption(x=>x
.setName('delete')
.setDescription('Delete a suggestion (Bot Tech only)')
.addIntegerOption(x=>x
.setName('id')
.setDescription('User\'s suggestion ID')
.setRequired(true))
.addStringOption(x=>x
.setName('message')
.setDescription('(Optional) Include a message with your approval')
.setRequired(true)
.setMaxLength(256)))
.setDescription('The ID of the suggestion')
.setRequired(true)))
.addSubcommand(x=>x
.setName('reject')
.setDescription('[Bot Tech] Reject the suggestion sent by the user')
.addStringOption(x=>x
.setName('update')
.setDescription('Update a suggestion (Bot Tech only)')
.addIntegerOption(x=>x
.setName('id')
.setDescription('User\'s suggestion ID')
.setDescription('The ID of the suggestion')
.setRequired(true))
.addStringOption(x=>x
.setName('message')
.setDescription('(Optional) Include a message with your rejection')
.setName('status')
.setDescription('The status of the suggestion (Accepted/Rejected)')
.setRequired(true)
.setMaxLength(256)))
.setChoices(
{name: 'Accept', value: 'Accepted'},
{name: 'Reject', value: 'Rejected'}
)))
}

View File

@ -1,128 +1,85 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
export default {
async autocomplete(client: TClient, interaction: Discord.AutocompleteInteraction){
const array = (await client.tags?.findInCache())?.map(x=>x._id).filter(c=>c.startsWith(interaction.options.getFocused()));
await interaction?.respond(array?.map(c=>({name: c, value: c})));
// If you question all those '?.', let me tell you: Discord.JS is fricking stupid and I am too stressed to find a solution for it.
},
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
if (interaction.options.getSubcommandGroup() === 'management' && !MessageTool.isStaff(interaction.member) && !client.config.whitelist.includes(interaction.member.id)) return MessageTool.youNeedRole(interaction, 'dcmod');
const tagData = async()=>await client.tags._content.findOne({_id: interaction.options.getString('name')});
const tagMeta = {
isEmbedTrue: async()=>(await tagData()).embedBool,
title: async()=>(await tagData())._id,
message: async()=>(await tagData()).message,
creatorName: async()=>(await client.users.fetch((await tagData()).user._id)).displayName
};
export default class Tag {
static async autocomplete(client:TClient, interaction:Discord.AutocompleteInteraction<'cached'>) {
const tagsInCache = await client.tags?.findInCache();
const filterArray = tagsInCache?.map(x=>x.tagname).filter(x=>x.startsWith(interaction.options.getFocused()));
await interaction?.respond(filterArray?.map(tag=>({name: tag, value: tag})));
}
static async run(client:TClient, interaction:Discord.ChatInputCommandInteraction<'cached'>) {
const tagName = interaction.options.getString('tag-name');
const tagMsg = interaction.options.getString('message');
({
send: async()=>{
if (!await tagData()) return interaction.reply({content:'This tag is not available in the database.',ephemeral:true});
let targetField = '';
const targetMember = interaction.options.getMember('target_user');
if (targetMember) targetField = `*This tag is for <@${targetMember.id}>*`;
const embedTemplate = new client.embed().setColor(client.config.embedColor).setTitle(await tagMeta.title()).setDescription(await tagMeta.message()).setFooter({text: `Tag creator: ${await tagMeta.creatorName()}`});
const messageTemplate = MessageTool.concatMessage(
targetField ? targetField : '',
`**${await tagMeta.title()}**`,
await tagMeta.message(),
'',
`Tag creator: **${await tagMeta.creatorName()}**`
);
if (await tagMeta.isEmbedTrue()) return interaction.reply({content: targetField ? targetField : null, embeds: [embedTemplate], allowedMentions:{parse:['users']}});
else return interaction.reply({content: messageTemplate, allowedMentions:{parse:['users']}})
send: async()=>await client.tags.sendTag(interaction, tagName, interaction.options.getMember('target')?.id),
create: async()=>{
const newTag = await client.tags.createTag(interaction.member.id, tagName, tagMsg, interaction.options.getBoolean('toggle-embed'));
await interaction.reply(newTag ? 'Tag successfully created, should be available in the list soon!' : 'Tag already exists, try again with a different name.');
},
create: async()=>await client.tags._content.create({
_id: interaction.options.getString('name'),
message: interaction.options.getString('message').replaceAll(/\\n/g, '\n'),
embedBool: interaction.options.getBoolean('embed'),
user: {
_id: interaction.member.id,
name: interaction.user.username
}
})
.then(()=>{
interaction.reply('Tag successfully created, should be available in a few seconds!')
client.tags.updateCache();
})
.catch(err=>interaction.reply(`There was an error while trying to create your tag:\n\`\`\`${err}\`\`\``)),
delete: async()=>await client.tags._content.findByIdAndDelete(interaction.options.getString('name'))
.then(()=>{
interaction.reply('Tag successfully deleted.')
client.tags.updateCache();
}).catch(err=>interaction.reply(`Failed to delete the tag:\n\`\`\`${err}\`\`\``)),
edit: async()=>await client.tags._content.findByIdAndUpdate(interaction.options.getString('name'), {
$set: {
message: interaction.options.getString('new-message').replaceAll(/\\n/g, '\n'),
embedBool: interaction.options.getBoolean('embed')
}
})
.then(()=>{
interaction.reply('Tag successfully updated, enjoy!')
client.tags.updateCache();
})
.catch(err=>interaction.reply(`Tag couldn\'t be updated:\n\`\`\`${err}\`\`\``))
delete: async()=>{
await client.tags.deleteTag(tagName);
return interaction.reply('Tag successfully deleted.');
},
modify: async()=>{
await client.tags.modifyTag(tagName, interaction.options.getString('new-message'));
return interaction.reply('Tag successfully modified.')
}
} as any)[interaction.options.getSubcommand() ?? interaction.options.getSubcommandGroup()]();
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('tag')
.setDescription('Send user the resources/FAQ provided in the tag')
.setDescription('Send a tag containing the resources/FAQ provided in tag to the user')
.addSubcommand(x=>x
.setName('send')
.setDescription('Send a resource tag')
.addStringOption(x=>x
.setName('name')
.setName('tag-name')
.setDescription('Name of an existing tag to send')
.setAutocomplete(true)
.setRequired(true))
.addUserOption(x=>x
.setName('target_user')
.setDescription('Directly mention the target with this tag')))
.setName('target')
.setDescription('Directly mention the member with this tag')
.setRequired(false)))
.addSubcommandGroup(x=>x
.setName('management')
.setDescription('Add a new tag or delete/edit your current tag')
.setName('tools')
.setDescription('Management tools for the tags system (Discord mods & Bot Tech only)')
.addSubcommand(x=>x
.setName('create')
.setDescription('Create a new tag')
.addStringOption(x=>x
.setName('name')
.setDescription('Name of your tag, must be within 3-25 characters')
.setMinLength(3)
.setMaxLength(25)
.setName('tag-name')
.setDescription('Name of the tag, must be within 4-32 characters')
.setMinLength(4)
.setMaxLength(32)
.setRequired(true))
.addStringOption(x=>x
.setName('message')
.setDescription('Message to be included in your tag; e.g, you\'re giving the user some instructions, newline: \\n')
.setMinLength(6)
.setMaxLength(2048)
.setDescription('Message to be included in your tag, newline: \\n')
.setMaxLength(1990)
.setRequired(true))
.addBooleanOption(x=>x
.setName('embed')
.setDescription('Toggle this option if you want your message to be inside the embed or not')
.setName('toggle-embed')
.setDescription('Message will be sent in an embed description if enabled')
.setRequired(true)))
.addSubcommand(x=>x
.setName('delete')
.setDescription('Delete a tag')
.setDescription('Delete an existing tag')
.addStringOption(x=>x
.setName('name')
.setName('tag-name')
.setDescription('Name of the tag to be deleted')
.setAutocomplete(true)
.setRequired(true)))
.addSubcommand(x=>x
.setName('edit')
.setDescription('Edit an existing tag')
.setName('modify')
.setDescription('Modify an existing tag')
.addStringOption(x=>x
.setName('name')
.setDescription('Name of the tag to be edited')
.setName('tag-name')
.setDescription('Name of the tag to be modified')
.setAutocomplete(true)
.setRequired(true))
.addStringOption(x=>x
.setName('new-message')
.setDescription('Replace the current tag\'s message with a new one, newline: \\n')
.setRequired(true))
.addBooleanOption(x=>x
.setName('embed')
.setDescription('Toggle this option on an existing tag to be updated with embed or not')
.setMaxLength(1990)
.setRequired(true))))
}

View File

@ -2,18 +2,19 @@ import Discord from 'discord.js';
import TClient from '../client.js';
import Logger from '../helpers/Logger.js';
import MessageTool from '../helpers/MessageTool.js';
export default {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
export default class Unpunish {
static async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
if (!MessageTool.isStaff(interaction.member as Discord.GuildMember)) return MessageTool.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 punishment = await client.punishments.findCase(interaction.options.getInteger('case_id', true));
if (!punishment) return interaction.reply({content: 'Case ID is not found in database.', ephemeral: true});
if (['unban', 'unmute', 'punishmentOverride'].includes(punishment.dataValues.type)) return interaction.reply({content: 'This case ID is immutable. (Informative case)', ephemeral: true});
const reason = interaction.options.getString('reason') ?? 'Reason unspecified';
await client.punishments.removePunishment(punishment.id, interaction.user.id, reason, interaction);
Logger.forwardToConsole('log', 'UnpunishmentLog', `Case #${interaction.options.getInteger('case_id')} 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).setTitle('Unpunishment Log').setDescription(`Case #${interaction.options.getInteger('case_id')} was used in \`/${interaction.commandName}\` for \`${reason}\``).setTimestamp()]});
},
data: new Discord.SlashCommandBuilder()
await client.punishments.punishmentRemove(punishment.dataValues.case_id, interaction.user.id, reason, interaction);
Logger.console('log', 'UnpunishmentLog', `Case #${interaction.options.getInteger('case_id')} was used in /${interaction.commandName} for ${reason}`);
(client.channels.cache.get(client.config.dcServer.channels.punishment_log) as Discord.TextChannel).send({embeds:[new client.embed().setColor(client.config.embedColor).setTitle('Unpunishment Log').setDescription(`Case #${interaction.options.getInteger('case_id')} was used in \`/${interaction.commandName}\` for \`${reason}\``).setTimestamp()]});
}
static data = new Discord.SlashCommandBuilder()
.setName('unpunish')
.setDescription('Remove the active punishment from a member')
.addIntegerOption(x=>x

View File

@ -1,11 +1,11 @@
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'>){
import Punish from '../components/Punish.js';
export default class Warn {
static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
Punish(client, interaction, 'warn');
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('warn')
.setDescription('Warn a member')
.addUserOption(x=>x
@ -15,4 +15,4 @@ export default {
.addStringOption(x=>x
.setName('reason')
.setDescription('Reason for the warning'))
}
}

View File

@ -10,8 +10,8 @@ function convert(status?:Discord.ClientPresenceStatus){
else return '⚫'
}
export default {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
export default class Whois {
static async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
const member = interaction.options.getMember('member') as Discord.GuildMember;
if (member === null){
const user = interaction.options.getUser('member') as Discord.User;
@ -34,8 +34,8 @@ export default {
const embed = new client.embed()
.setColor(member.displayColor || client.config.embedColor)
.setURL(`https://discord.com/users/${member.user.id}`)
.setThumbnail(member.user.avatarURL({size:2048}) || member.user.defaultAvatarURL)
.setImage(member.user.bannerURL({size:1024}) as string)
.setThumbnail(member.avatarURL({size:2048}) || member.user.avatarURL({size:2048}) || member.user.defaultAvatarURL)
.setImage(member.user.bannerURL({size:1024}) || null)
.setTitle(`${title} Info: ${member.user.username}`)
.setDescription(`<@${member.user.id}>\n\`${member.user.id}\``)
.addFields(
@ -62,8 +62,8 @@ export default {
}
interaction.reply({embeds: embedArray})
}
},
data: new Discord.SlashCommandBuilder()
}
static data = new Discord.SlashCommandBuilder()
.setName('whois')
.setDescription('View your own or someone else\'s information')
.addUserOption(x=>x

44
src/components/Automod.ts Normal file
View File

@ -0,0 +1,44 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import Logger from '../helpers/Logger.js';
export default class Automoderator {
private static lockQuery:Set<Discord.Snowflake> = new Set();
static Whitelist(message:Discord.Message, ...arr:string[]) {// Array of channel ids for automod to be disabled in (Disables prohibitedWords and advertisement, mind you.)
return arr.includes(message.channelId);
}
static scanMsg(message:Discord.Message) {
return message.content.toLowerCase().replaceAll(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?\n?0-9]|[]|ing\b/g, '').split(' ').join('');
}
static async repeatedMessages(client:TClient, message:Discord.Message, thresholdTime:number, thresholdAmount:number, type:string, duration:string, reason:string) {
const now = Date.now();
if (!client.repeatedMessages[message.author.id]) client.repeatedMessages[message.author.id] = {type: type, count:1, firstTime:now, timeout: null};
else {
const data = client.repeatedMessages[message.author.id];
if (now - data.firstTime < thresholdTime) {
// If the message is within the threshold time, increment.
data.count++;
if (data.count >= thresholdAmount) {
// If the count has reached the threshold amount, punish the user like most daddy would do to their child.
if (!this.lockQuery.has(message.author.id)) {
this.lockQuery.add(message.author.id);
Logger.console('log', 'Automod', `Lock acquired for ${message.author.tag} with reason: ${reason}`);
await client.punishments.punishmentAdd('mute', {time: duration}, client.user.id, `AUTOMOD:${reason}`, message.author, message.member as Discord.GuildMember);
setTimeout(()=>{
this.lockQuery.delete(message.author.id);
Logger.console('log', 'Automod', `Lock released for ${message.author.tag}`);
}, 3500); // Wait 3.5 seconds before releasing the lock.
}
delete client.repeatedMessages[message.author.id];
}
} else {
// If the message is outside the threshold time, reset the count and timestamp.
data.count = 1;
data.firstTime = now;
}
// Reset the timer.
clearTimeout(data.timeout);
data.timeout = setTimeout(()=>delete client.repeatedMessages[message.author.id], thresholdTime);
}
}
}

View File

@ -0,0 +1,58 @@
import {createClient, ErrorReply} from 'redis';
import Logger from '../helpers/Logger.js';
import TSClient from '../helpers/TSClient.js';
let Prefix = 'Cache';
const RedisClient = createClient({
url: (await TSClient()).redis_uri,
database: 0,
name: 'Daggerbot',
socket: { keepAlive: 15000, timeout: 30000 }
});
export default class CacheServer {
protected static eventManager() {
RedisClient
.on('connect', ()=>Logger.console('log', Prefix, 'Connection to Redis has been established'))
.on('error', (err:ErrorReply)=>{
Logger.console('error', Prefix, `Encountered an error in Redis: ${err.message}`)
setTimeout(async()=>{
if (!RedisClient.isReady) {
Logger.console('log', Prefix, 'Client is zombified, starting a fresh connection...');
RedisClient.quit();
await RedisClient.connect();
}
}, 1500)
})
}
public static async get(key:any) {
const cachedResult = await RedisClient.get(key);
if (cachedResult) return JSON.parse(cachedResult);
else return null
}
public static async set(key:any, value:any) {
return await RedisClient.set(key, JSON.stringify(value));
}
public static async getJSON(key:any) {
const cachedResult = await RedisClient.json.get(key);
if (cachedResult) return cachedResult;
else return null
}
public static async setJSON(key:any, value:any) {
return await RedisClient.json.set(key, '.', value);
}
public static async expiry(key:any, time:number) {
return await RedisClient.expire(key, time); // NOTE: time is in seconds, not milliseconds -- you know what you did wrong
}
public static async delete(key:any) {
return await RedisClient.del(key);
}
public static init() {
try {
RedisClient.connect();
this.eventManager();
} catch {
console.error('Cannot initialize RedisClient -- is Redis running?')
}
}
}

View File

@ -0,0 +1,155 @@
import {createCanvas, Canvas, CanvasRenderingContext2D} from 'canvas';
import {Config} from '../interfaces';
import ConfigHelper from '../helpers/ConfigHelper.js';
export default class CanvasBuilder {
private canvas: Canvas;
private ctx: CanvasRenderingContext2D;
private config: Config;
constructor() {
this.canvas = createCanvas(1500, 750);
this.ctx = this.canvas.getContext('2d');
this.config = ConfigHelper.readConfig() as Config;
}
public async generateGraph(data:number[], type:'players'|'leaderboard'):Promise<Canvas> {
// Color layout for the graph -- The variables are named exactly what it shows in graph to make it easier to be referenced to.
let oddHorizontal = '#555B63';
let evenHorizontal = '#3E4245';
let background = '#111111';
let textColor = '#FFFFFF';
let redLine = '#E62C3B';
let yellowLine = '#FFEA00';
let greenLine = '#57F287';
// Handle negative
for (const [i, change] of data.entries()) if (change as number < 0) data[i] = data[i - 1] || data[i + 1] || 0;
const LBdataFirst = Math.ceil(Math.max(...data) * 10 ** (-Math.max(...data).toString().split('').length + 1)) * 10 ** (Math.max(...data).toString().split('').length - 1)
const LBdataSecond = Math.ceil(Math.max(...data) * 10 ** (-Math.max(...data).toString().split('').length + 2)) * 10 ** (Math.max(...data).toString().split('').length - 2)
const firstTop = type === 'leaderboard' ? LBdataFirst : 16;
const secondTop = type === 'leaderboard' ? LBdataSecond : 16;
const textSize = 40;
const origin = [15, 65];
const size = [1300, 630];
const nodeWidth = size[0] / (data.length - 1);
this.ctx.fillStyle = background;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Grey horizontal lines
this.ctx.lineWidth = 5;
const intervalCandidates:[number, number, number][] = [];
for (let i = 4; i < 10; i++) {
const interval = firstTop / i;
if (Number.isInteger(interval)) intervalCandidates.push([interval, i, i * Math.max(interval.toString().split('').filter(x=>x === '0').length / interval.toString().length, 0.3) * (['1', '2', '4', '5', '6', '8'].includes(interval.toString()[0]) ? 1.5 : 0.67)]);
}
const chosenInterval = intervalCandidates.sort((a,b)=>b[2]-a[2])[0];
let prevY:number[] = [];
this.ctx.strokeStyle = oddHorizontal;
if (type === 'leaderboard') for (let i = 0; i <= chosenInterval[1]; i++) {
const y = origin[1] + size[1] - (i * (chosenInterval[0] / secondTop) * size[1]);
if (y < origin[1]) continue;
const even = ((i + 1) % 2) === 0;
if (even) this.ctx.strokeStyle = evenHorizontal;
this.ctx.beginPath();
this.ctx.lineTo(origin[0], y);
this.ctx.lineTo(origin[0] + size[0], y);
this.ctx.stroke();
this.ctx.closePath();
if (even) this.ctx.strokeStyle = oddHorizontal;
prevY = [y, i * chosenInterval[0]];
}
else for (let i = 0; i < data.length; i++) {
const y = origin[1] + size[1] - (i * (chosenInterval[0] / secondTop) * size[1]);
if (y < origin[1]) continue;
const even = ((i + 1) % 2) === 0;
if (even) this.ctx.strokeStyle = evenHorizontal;
this.ctx.beginPath();
this.ctx.lineTo(origin[0], y);
this.ctx.lineTo(origin[0] + size[0], y);
this.ctx.stroke();
this.ctx.closePath();
if (even) this.ctx.strokeStyle = oddHorizontal;
prevY.push(y, i * chosenInterval[0]);
}
// 30 day/minute mark
this.ctx.setLineDash([8, 16]);
this.ctx.beginPath();
const lastStart = origin[0] + (nodeWidth * (data.length - (type === 'players' ? 60 : 30)));
this.ctx.lineTo(lastStart, origin[1]);
this.ctx.lineTo(lastStart, origin[1] + size[1]);
this.ctx.stroke();
this.ctx.closePath();
this.ctx.setLineDash([]);
// Draw points
const isLeaderboard =()=>type === 'leaderboard' ? this.config.embedColor as string : null;
this.ctx.strokeStyle = isLeaderboard();
this.ctx.fillStyle = isLeaderboard();
this.ctx.lineWidth = 5;
const gradient = this.ctx.createLinearGradient(0, origin[1], 0, origin[1] + size[1]);
gradient.addColorStop(1 / 16, redLine);
gradient.addColorStop(5 / 16, yellowLine);
gradient.addColorStop(12 / 16, greenLine);
let lastCoordinates:number[] = [];
for (let [i, currentValue] of data.entries()) {
if (currentValue < 0) currentValue = 0;
const X = i * nodeWidth + origin[0];
const Y = ((1 - (currentValue / secondTop)) * size[1]) + origin[1];
const nextValue = data[i + 1];
const previousValue = data[i - 1];
this.ctx.strokeStyle = type === 'players' ? gradient : null;
this.ctx.beginPath();
if (lastCoordinates.length) this.ctx.moveTo(lastCoordinates[0], lastCoordinates[1]);
// If the line being drawn is straight line, continue until it makes a slope.
if (Y === lastCoordinates[1]) {
let NewX = X;
for (let j = i+1; j <= data.length; j++) {
if (data[j] === currentValue) NewX += nodeWidth;
else break;
}
this.ctx.lineTo(NewX, Y);
} else this.ctx.lineTo(X, Y);
lastCoordinates = [X, Y];
this.ctx.stroke();
this.ctx.closePath();
if (currentValue !== previousValue || currentValue !== nextValue) {
// Ball. What else?
this.ctx.fillStyle = type === 'players' ? gradient : null;
this.ctx.beginPath();
this.ctx.arc(X, Y, this.ctx.lineWidth * 1.2, 0, 2 * Math.PI);
this.ctx.closePath();
this.ctx.fill();
}
}
// Draw text
this.ctx.font = '400 ' + textSize + 'px sans-serif';
this.ctx.fillStyle = textColor;
// Highest value
this.ctx.fillText(type === 'leaderboard'
? prevY[1].toLocaleString('en-US')
: prevY.at(-1).toLocaleString('en-US'), origin[0] + size[0] + textSize / 2, origin[1] + (textSize / 3)
)
// Lowest value
this.ctx.fillText(type === 'leaderboard' ? '0 msgs' : '0', origin[0] + size[0] + textSize / 2, origin[1] + size[1] + (textSize / 3));
// 30 day (minute for /mp players)
this.ctx.fillText(type === 'leaderboard' ? '30 days ago' : '30 mins ago', lastStart, origin[1] - (textSize / 2));
// Time
this.ctx.fillText('time ->', origin[0] + (textSize / 2), origin[1] + size[1] + (textSize));
return this.canvas;
}
}

View File

@ -0,0 +1,25 @@
import {Sequelize} from 'sequelize';
import Logger from '../helpers/Logger.js';
import TSClient from '../helpers/TSClient.js';
const postgresUri = (await TSClient()).postgres_uri;
export default class DatabaseServer {
public static seq:Sequelize = new Sequelize(postgresUri, {dialect: 'postgres', logging: false, ssl: false})
public static async init() {
try {
await this.seq.authenticate();
this.healthCheck();
} catch {
Logger.console('error', 'Database', 'Cannot initialize Sequelize -- is PostgreSQL running?');
process.exit(1);
}
}
private static async healthCheck() {
try {
await this.seq.query('SELECT 1');
Logger.console('log', 'Database', 'Connection to PostgreSQL has been established');
} catch {
Logger.console('error', 'Database', 'Connection to PostgreSQL has been lost');
}
}
}

View File

@ -0,0 +1,33 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import ConfigHelper from '../helpers/ConfigHelper.js';
const config = ConfigHelper.readConfig();
type ChannelList = keyof typeof config.dcServer.channels;
export default class HookMgr {
private client:TClient;
private channel:ChannelList;
private webhookId:Discord.Snowflake;
constructor(client:TClient, channel:ChannelList, webhookId:Discord.Snowflake) {
this.client = client;
this.channel = channel;
this.webhookId = webhookId;
}
protected async channelFetch(client:TClient, channel:ChannelList) {
return await client.channels.fetch(config.dcServer.channels[channel]) as Discord.TextChannel;
}
protected async fetch(client:TClient, channel:ChannelList, webhookId:Discord.Snowflake) {
const hookInstance = await (await this.channelFetch(client, channel)).fetchWebhooks().then(x=>x.find(y=>y.id===webhookId));
if (!hookInstance) throw new Error('[HookManager] Webhook not found.');
return hookInstance;
}
async send(message:string|Discord.MessagePayload|Discord.WebhookMessageCreateOptions) {
const hook = await this.fetch(this.client, this.channel, this.webhookId);
return hook.send(message).catch(err=>(this.client.channels.resolve(config.dcServer.channels.errors) as Discord.TextChannel).send(`Failed to send a webhook message in #${this.channel}:\n\`\`\`\n${err.message}\n\`\`\``));
}
async edit(messageId:Discord.Snowflake, message:string|Discord.MessagePayload|Discord.WebhookMessageEditOptions) {
const hook = await this.fetch(this.client, this.channel, this.webhookId);
return hook.editMessage(messageId, message).catch(err=>(this.client.channels.resolve(config.dcServer.channels.errors) as Discord.TextChannel).send(`Failed to edit a webhook message in #${this.channel}:\n\`\`\`\n${err.message}\n\`\`\``));
}
}

18
src/components/Punish.ts Normal file
View File

@ -0,0 +1,18 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
export default async(client:TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>, type: string)=>{
if (!MessageTool.isStaff(interaction.member)) return MessageTool.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);
if (interaction.user.id === User.id) return interaction.reply(`You cannot ${type} yourself.`);
if (!GuildMember && !['unban', 'ban'].includes(type)) 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.punishmentAdd(type, {time, interaction}, interaction.user.id, reason, User, GuildMember);
}

View File

@ -1,35 +1,25 @@
{
"configName": "Daggerbot",
"embedColor": "#FFFFFF",
"embedColor": "#0052cf",
"embedColorBackup": "#0052cf",
"embedColorGreen": "#57f287",
"embedColorOrange": "#cc5210",
"embedColorYellow": "#ffea00",
"embedColorRed": "#e62c3b",
"embedColorInvis": "#2f3136",
"embedColorBCA": "#ff69b4",
"embedColorXmas": "#FFFFFF",
"embedColorXmas": "#ffffff",
"LRSstart": 1661236321433,
"whitelistedServers": [
"929807948748832798", "468835415093411861", "1058183358267543552", "549114074273677314"
"929807948748832798", "468835415093411861"
],
"MPStatsLocation": {
"mainServer": {
"channel": "543494084363288637",
"message": "1023699243183112192"
},
"secondServer": {
"channel": "543494084363288637",
"message": "1149141188079779900"
}
},
"botSwitches": {
"dailyMsgsBackup": true,
"registerCommands": false,
"registerCommands": true,
"commands": true,
"logs": true,
"mpSys": true,
"buttonRoles": true,
"automod": true,
"mpstats": true,
"autores": true
},
"botPresence": {
@ -38,13 +28,12 @@
],
"status": "online"
},
"eval": true,
"whitelist": [
"190407856527376384",
"633345781780185099",
"215497515934416896",
"141304507249197057",
"309373272594579456"
"309373272594579456",
"301350210926280704"
],
"contribList": [
"190407856527376384",
@ -54,7 +43,7 @@
"178941218510602240",
"700641965787709520"
],
"mainServer": {
"dcServer": {
"id": "468835415093411861",
"staffRoles": [
"admin",
@ -79,7 +68,6 @@
"vtcmember": "802282391652663338"
},
"channels": {
"console": "1011318687065710663",
"errors": "1009754872188506192",
"thismeanswar": "1091300529696673792",
"bot_suggestions": "1040018521746325586",
@ -88,10 +76,9 @@
"welcome": "621134751897616406",
"botcommands": "468888722210029588",
"bankick_log": "1048341961901363352",
"fs_server_log": "1104632399771488317",
"punishment_log": "1102751034754998302",
"dcmod_chat": "742324777934520350",
"mf_chat": "1149238561934151690"
"mpmod_chat": "516344221452599306"
}
}
}

View File

@ -1,8 +1,8 @@
import Discord, { AuditLogEvent } from 'discord.js';
import TClient from '../client.js';
export default {
async run(client:TClient, member:Discord.GuildMember){
if (member.guild?.id != client.config.mainServer.id) return;
export default class GuildBanAdd {
static async run(client:TClient, member:Discord.GuildMember){
if (member.guild?.id != client.config.dcServer.id) return;
const banLog = (await member.guild.fetchAuditLogs({ limit: 1, type: AuditLogEvent.MemberBanAdd })).entries.first();
if (!banLog) return console.log(`Member was banned from ${member.guild.name} but no audit log for this member.`)
const {executor, target, reason } = banLog;
@ -11,8 +11,8 @@ export default {
{name: '🔹 Moderator', value: `<@${executor.id}>\n\`${executor.id}\``},
{name: '🔹 Reason', value: `${reason === null ? 'Reason unspecified': reason}`}
);
if (!await client.userLevels._content.findById(member.user.id)) embed.setFooter({text:'Rank data has been wiped.'});
(client.channels.resolve(client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds: [embed]})
if (!await client.userLevels.fetchUser(member.user.id)) embed.setFooter({text: 'Rank data has been wiped.'});
(client.channels.resolve(client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [embed]})
} else console.log(`User was banned from "${member.guild.name}" but no audit log could be fetched.`)
}
}

View File

@ -1,12 +1,12 @@
import Discord, { AuditLogEvent } from 'discord.js';
import TClient from '../client.js';
export default {
async run(client:TClient, member:Discord.GuildMember){
if (member.guild?.id != client.config.mainServer.id) return;
export default class GuildBanRemove {
static async run(client:TClient, member:Discord.GuildMember){
if (member.guild?.id != client.config.dcServer.id) return;
const unbanLog = (await member.guild.fetchAuditLogs({limit: 1, type: AuditLogEvent.MemberBanRemove})).entries.first();
if (!unbanLog) return console.log(`User was unbanned from ${member.guild.name} but no audit log for this user.`)
const { executor, target, reason } = unbanLog;
if (target.id === member.user.id) (client.channels.resolve(client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds: [
if (target.id === member.user.id) (client.channels.resolve(client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [
new client.embed().setColor(client.config.embedColorGreen).setTimestamp().setThumbnail(member.user.displayAvatarURL({size: 2048})).setTitle(`Member Unbanned: ${target.username}`).setDescription(`🔹 **User**\n<@${target.id}>\n\`${target.id}\``).addFields(
{name: '🔹 Moderator', value: `<@${executor.id}>\n\`${executor.id}\``},
{name: '🔹 Reason', value: `${reason === null ? 'Reason unspecified.': reason}`}

View File

@ -1,9 +1,8 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
export default {
async run(client:TClient, member:Discord.GuildMember){
if (member.partial || member.guild?.id != client.config.mainServer.id) return;
export default class GuildMemberAdd {
static async run(client:TClient, member:Discord.GuildMember){
if (member.partial || member.guild?.id != client.config.dcServer.id) return;
const index = member.guild.memberCount;
const suffix = (index=>{
const numbers = index.toString().split('').reverse(); // eg 1850 --> [0,5,8,1]
@ -18,21 +17,16 @@ export default {
let isBot = 'Bot';
if (!member.user.bot) isBot = 'Member';
if (!client.config.botSwitches.logs) return;
(client.channels.resolve(client.config.mainServer.channels.welcome) as Discord.TextChannel).send({embeds: [new client.embed().setColor(client.config.embedColor).setThumbnail(member.user.displayAvatarURL({size: 2048}) || member.user.defaultAvatarURL).setTitle(`Welcome to Daggerwin, ${member.user.username}!`).setFooter({text: `${index}${suffix} member`})]})
(client.channels.resolve(client.config.dcServer.channels.welcome) as Discord.TextChannel).send({embeds: [new client.embed().setColor(client.config.embedColor).setThumbnail(member.user.displayAvatarURL({size: 2048}) || member.user.defaultAvatarURL).setTitle(`Welcome to Daggerwin, ${member.user.username}!`).setFooter({text: `${index}${suffix} member`})]})
const newInvites = await member.guild.invites.fetch();
const usedInvite = newInvites.find((inv:Discord.Invite)=>client.invites.get(inv.code)?.uses < inv.uses);
newInvites.forEach((inv:Discord.Invite)=>client.invites.set(inv.code,{uses: inv.uses, creator: inv.inviterId, channel: inv.channel.name}));
(client.channels.resolve(client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds: [
(client.channels.resolve(client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [
new client.embed().setColor(client.config.embedColorGreen).setTimestamp().setThumbnail(member.user.displayAvatarURL({size: 2048})).setTitle(`${isBot} Joined: ${member.user.username}`).setDescription(`<@${member.user.id}>\n\`${member.user.id}\``).setFooter({text: `Total members: ${index}${suffix}`}).addFields(
{name: '🔹 Account Creation Date', value: `<t:${Math.round(member.user.createdTimestamp/1000)}>\n<t:${Math.round(member.user.createdTimestamp/1000)}:R>`},
{name: '🔹 Invite Data:', value: usedInvite ? `Invite: \`${usedInvite.code}\`\nCreated by: **${usedInvite.inviter?.username}**\nChannel: **#${usedInvite.channel.name}**` : 'No invite data could be fetched.'}
)]});
if (await client.punishments._content.findOne({'member': member.user.id, type: 'mute', expired: undefined})){
(client.channels.resolve(client.config.mainServer.channels.dcmod_chat) as Discord.TextChannel).send({embeds: [new client.embed().setColor(client.config.embedColorYellow).setTitle('Case evasion detected').setDescription(MessageTool.concatMessage(
`**${member.user.username}** (\`${member.user.id}\`) has been detected for case evasion.`,
'Timeout has been automatically added. (25 days)'
)).setTimestamp()]});
await client.punishments.addPunishment('mute', {time: '25d'}, client.user.id, '[AUTOMOD] Case evasion', member.user, member)
}
await client.punishments.caseEvasionCheck(member);
}
}

View File

@ -1,20 +1,20 @@
import Discord from 'discord.js';
import TClient from '../client.js';
export default {
async run(client:TClient, member:Discord.GuildMember){
export default class GuildMemberRemove {
static async run(client:TClient, member:Discord.GuildMember){
if (!client.config.botSwitches.logs) return;
if (!member.joinedTimestamp || member.guild?.id != client.config.mainServer.id) return;
if (client.guilds.cache.get(client.config.mainServer.id).bans.cache.has(member.id)) return await client.userLevels._content.findByIdAndDelete(member.id);
if (!member.joinedTimestamp || member.guild?.id != client.config.dcServer.id) return;
if (client.guilds.cache.get(client.config.dcServer.id).bans.cache.has(member.id)) return await client.userLevels.deleteUser(member.id);
let isBot = 'Bot';
if (!member.user.bot) isBot = 'Member';
const levelData = await client.userLevels._content.findById(member.id);
const levelData = await client.userLevels.fetchUser(member.id);
const embed = new client.embed().setColor(client.config.embedColorRed).setTimestamp().setThumbnail(member.user.displayAvatarURL({size: 2048}) as string).setTitle(`${isBot} Left: ${member.user.username}`).setDescription(`<@${member.user.id}>\n\`${member.user.id}\``).addFields(
{name: '🔹 Account Creation Date', value: `<t:${Math.round(member.user.createdTimestamp/1000)}>\n<t:${Math.round(member.user.createdTimestamp/1000)}:R>`},
{name: '🔹 Server Join Date', value: `<t:${Math.round(member.joinedTimestamp/1000)}>\n<t:${Math.round(member.joinedTimestamp/1000)}:R>`},
{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)
if (levelData && levelData.dataValues.messages > 1) embed.addFields({name: '🔹 Total messages', value: levelData.dataValues.messages.toLocaleString('en-US'), inline: true});
(client.channels.resolve(client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [embed]});
await client.userLevels.deleteUser(member.id);
}
}

View File

@ -1,10 +1,10 @@
import Discord from 'discord.js';
import TClient from '../client.js';
export default {
run(client:TClient, oldMember:Discord.GuildMember, newMember:Discord.GuildMember){
if (oldMember.guild.id != client.config.mainServer.id) return;
export default class GuildMemberUpdate {
static run(client:TClient, oldMember:Discord.GuildMember, newMember:Discord.GuildMember){
if (oldMember.guild.id != client.config.dcServer.id) return;
if (!client.config.botSwitches.logs) return;
const channel = (client.channels.resolve(client.config.mainServer.channels.logs) as Discord.TextChannel);
const channel = (client.channels.resolve(client.config.dcServer.channels.logs) as Discord.TextChannel);
if (oldMember.nickname != newMember.nickname){
const embed = new client.embed().setColor(client.config.embedColor).setTimestamp().setThumbnail(newMember.displayAvatarURL({size: 2048})).setTitle(`Nickname updated: ${newMember.user.username}`).setDescription(`<@${newMember.user.id}>\n\`${newMember.user.id}\``)
oldMember.nickname === null ? '' : embed.addFields({name: '🔹 Old nickname', value: `\`\`\`${oldMember.nickname}\`\`\``})

View File

@ -1,13 +1,14 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import Logger from '../helpers/Logger.js';
export default {
async run(client:TClient, interaction:Discord.BaseInteraction){
export default class InteractionCreate {
static async run(client:TClient, interaction:Discord.BaseInteraction){
if (!interaction.inGuild() || !interaction.inCachedGuild()) return;
const logPrefix = 'Interaction';
if (interaction.isChatInputCommand()){
const commandFile = client.commands.get(interaction.commandName);
Logger.forwardToConsole('log', 'InteractionLog', `${interaction.user.username} used /${interaction.commandName} ${interaction.options.getSubcommandGroup(false) ?? ''} ${interaction.options.getSubcommand(false) ?? ''} in #${interaction.channel.name}`);
Logger.console('log', logPrefix, `${interaction.user.username} used /${interaction.commandName} ${interaction.options.getSubcommandGroup(false) ?? ''} ${interaction.options.getSubcommand(false) ?? ''} in #${interaction.channel.name}`.replace(/\s\s+/g, ' ').trim());
if (!client.config.botSwitches.commands && !client.config.whitelist.includes(interaction.user.id)) return interaction.reply({content: `I am currently operating in development mode.\nPlease notify <@${client.config.whitelist[0]}> if this is a mistake.`, ephemeral: true});
if (commandFile){
try{
@ -28,28 +29,20 @@ export default {
} else if (interaction.isButton()){
if (interaction.customId.startsWith('reaction-') && client.config.botSwitches.buttonRoles){
const RoleID = interaction.customId.replace('reaction-','');
// Note: This is just a temporary "permanent" fix for the issue of people having both roles and less work for the mods.
let buttonRoleBlocked = 'Cannot have both roles! - Button Role';
if (interaction.member.roles.cache.has('1149139369433776269') && RoleID === '1149139583729160325') {
interaction.member.roles.add('1149139583729160325', buttonRoleBlocked);
interaction.member.roles.remove('1149139369433776269', buttonRoleBlocked);
} else if (interaction.member.roles.cache.has('1149139583729160325') && RoleID === '1149139369433776269') {
interaction.member.roles.add('1149139369433776269', buttonRoleBlocked);
interaction.member.roles.remove('1149139583729160325', buttonRoleBlocked);
}
if (interaction.member.roles.cache.has(RoleID)){
interaction.member.roles.remove(RoleID, 'Button Role');
interaction.reply({content: `You have been removed from <@&${RoleID}>`, ephemeral: true})
} else {
interaction.member.roles.add(RoleID, 'Button Role');
interaction.reply({content: `You have been added to <@&${RoleID}>`, ephemeral: true})
}
} else if (interaction.customId.includes('deleteEmbed')) {
if (!client.config.whitelist.includes(interaction.user.id)) return interaction.reply({content: '*Only whitelisted people can delete this embed.*', ephemeral: true});
interaction.message.edit({content: '*Deleted.*', embeds: [], components: []});
Logger.forwardToConsole('log', 'InteractionLog', `Embed has been deleted at ${interaction.message.url}`);
} else Logger.forwardToConsole('log', 'InteractionLog', `Button has been pressed at ${interaction.message.url}`);
let roleConflictMsg = 'Cannot have both roles! - Button Role';
const WestFarm = '1149139369433776269';
const EastFarm = '1149139583729160325';
if (interaction.member.roles.cache.has(WestFarm) && RoleID === EastFarm) interaction.member.roles.set([EastFarm], roleConflictMsg);
else if (interaction.member.roles.cache.has(EastFarm) && RoleID === WestFarm) interaction.member.roles.set([WestFarm], roleConflictMsg);
if (interaction.member.roles.cache.has(RoleID)) interaction.member.roles.remove(RoleID, 'Button Role').then(()=>interaction.reply({content: `You have been removed from <@&${RoleID}>`, ephemeral: true}));
else interaction.member.roles.add(RoleID, 'Button Role').then(()=>interaction.reply({content: `You have been added to <@&${RoleID}>`, ephemeral: true}));
} else if (interaction.customId.includes('deleteEvalEmbed')) {
if (!client.config.whitelist.includes(interaction.user.id)) return interaction.reply({content: 'You are not whitelisted, therefore you cannot delete this message.', ephemeral: true});
interaction.message.delete();
Logger.console('log', logPrefix, `Eval embed has been deleted in #${interaction.message.channel.name} by ${interaction.member.displayName}`);
} else Logger.console('log', logPrefix, `Button has been pressed at ${interaction.message.url}`);
}
}
}

View File

@ -1,7 +1,7 @@
import Discord from 'discord.js';
import TClient from '../client.js';
export default {
async run(client:TClient, invite: Discord.Invite){
export default class InviteCreate {
static async run(client:TClient, invite:Discord.Invite){
if (!invite.guild) return;
(await (invite.guild as Discord.Guild).invites.fetch()).forEach(inv=>client.invites.set(inv.code,{uses: inv.code, creator: inv.inviterId, channel: inv.channel.name}))
}

View File

@ -1,7 +1,7 @@
import Discord from 'discord.js';
import TClient from '../client.js';
export default {
run(client:TClient, invite: Discord.Invite){
export default class InviteDelete {
static run(client:TClient, invite:Discord.Invite){
client.invites.delete(invite.code)
}
}

View File

@ -1,35 +1,36 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import Response from '../funcs/ResponseModule.js';
import CmdTrigger from '../funcs/CmdModule.js';
import Response from '../modules/ResponseModule.js';
import CmdTrigger from '../modules/CmdModule.js';
import Logger from '../helpers/Logger.js';
import Automoderator from '../funcs/Automod.js';
import ConfigHelper from '../helpers/ConfigHelper.js';
import Automoderator from '../components/Automod.js';
import MessageTool from '../helpers/MessageTool.js';
export default {
async run(client:TClient, message:Discord.Message){
export default class MessageCreate {
static async run(client:TClient, message:Discord.Message){
if (message.author.bot) return;
if (!message.inGuild()) return (client.channels.resolve(client.config.mainServer.channels.logs) as Discord.TextChannel).send({content: `<:fish_unamused:1083675172407623711> ${MessageTool.formatMention(client.config.whitelist[0], 'user')}\n**${message.author.username}** (\`${message.author.id}\`) tried to send me a DM, their message is:\`\`\`${message.content}\`\`\``, allowedMentions: {parse: ['users']}});
if (!message.inGuild()) return (client.channels.resolve(client.config.dcServer.channels.logs) as Discord.TextChannel).send({content: `<:fish_unamused:1083675172407623711> ${MessageTool.formatMention(client.config.whitelist[0], 'user')}\n**${message.author.username}** (\`${message.author.id}\`) tried to send me a DM, their message is:\`\`\`${message.content}\`\`\``, allowedMentions: {parse: ['users']}});
let automodded: boolean;
if (client.config.botSwitches.automod && !message.member.roles.cache.has(client.config.mainServer.roles.admin) && message.guildId === client.config.mainServer.id){
if (client.config.botSwitches.automod && !message.member.roles.cache.has(client.config.dcServer.roles.dcmod) && !message.member.roles.cache.has(client.config.dcServer.roles.admin) && message.guildId === client.config.dcServer.id){
const automodFailReason = 'msg got possibly deleted by another bot.';
if (await client.bannedWords._content.findById(Automoderator.scanMsg(message))/* && !Whitelist.includes(message.channelId) */){
if (await client.prohibitedWords.findWord(Automoderator.scanMsg(message))/* && !Whitelist.includes(message.channelId) */) {
automodded = true;
message.delete().catch(()=>Logger.forwardToConsole('log', 'AUTOMOD-BANNEDWORDS', automodFailReason));
message.delete().catch(()=>Logger.console('log', 'AUTOMOD:PROHIBITEDWORDS', automodFailReason));
message.channel.send('That word isn\'t allowed here.').then(x=>setTimeout(()=>x.delete(), 10000));
await Automoderator.repeatedMessages(client, message, 30000, 3, 'bw', '30m', 'Constant swears');
} else if (message.content.toLowerCase().includes('discord.gg/') && !MessageTool.isStaff(message.member as Discord.GuildMember)){
await Automoderator.repeatedMessages(client, message, 30000, 3, 'bw', '30m', 'Prohibited word spam');
} else if (message.content.toLowerCase().includes('discord.gg/') && !MessageTool.isStaff(message.member as Discord.GuildMember)) {
const validInvite = await client.fetchInvite(message.content.split(' ').find(x=>x.includes('discord.gg/'))).catch(()=>null);
if (validInvite && validInvite.guild?.id !== client.config.mainServer.id){
if (validInvite && validInvite.guild?.id !== client.config.dcServer.id) {
automodded = true;
message.delete().catch(()=>Logger.forwardToConsole('log', 'AUTOMOD-ADVERT', automodFailReason));
message.delete().catch(()=>Logger.console('log', 'AUTOMOD:ADVERTISEMENT', automodFailReason));
message.channel.send('Please don\'t advertise other Discord servers.').then(x=>setTimeout(()=>x.delete(), 15000));
await Automoderator.repeatedMessages(client, message, 60000, 2, 'adv', '1h', 'Discord Advertisement');
await Automoderator.repeatedMessages(client, message, 60000, 2, 'adv', '1h', 'Discord advertisement');
}
}
}
if (message.guildId === client.config.mainServer.id && !automodded) client.userLevels.incrementUser(message.author.id)
if (message.guildId === client.config.dcServer.id && !automodded) client.userLevels.messageIncremental(message.author.id);
// Mop gifs from banned channels without Monster having to mop them.
const bannedChannels = [
'516344221452599306', // #mp-moderators
@ -48,7 +49,7 @@ export default {
const outgoingArrays = {
guildBoost: ['Thanks for boosting our server!', 'Thanks for the boost!', 'We appreciate the boost!', `Thank you for the kind boost, <@${message.author.id}>!`],
}
const GeneralChatID = '468835415093411863';
const GeneralChatID = ConfigHelper.isDevMode() ? '1160707096493441056' : '468835415093411863';
Response.create(client, message, GeneralChatID, 'morning');
Response.create(client, message, GeneralChatID, 'afternoon');
Response.create(client, message, GeneralChatID, 'evening');
@ -57,15 +58,36 @@ export default {
CmdTrigger.registerCmds(client, message, 'wepanikfrfr');
CmdTrigger.MFPwTrigger(message, 'farmpw');
if (message.type === 8 && message.channelId === GeneralChatID) message.channel.send({content: outgoingArrays.guildBoost[Math.floor(Math.random() * outgoingArrays.guildBoost.length)], allowedMentions: {parse: ['users']}})
let picStorage = {
cantRead: 'https://tenor.com/view/aristocats-george-pen-cap-meticulous-gif-5330931',
amAlive: 'https://tenor.com/view/i-still-feel-alive-living-existing-active-singing-gif-14630579',
deadChat: 'https://cdn.discordapp.com/attachments/925589318276382720/1011333656167579849/F57G5ZS.png',
};
let ModsGoGetThisPerson = [// I find this variable amusing but also can't think of anything better so not changing it.
{
user: 'nawdic',
img: 'https://c.tenor.com/JSj9ie_MD9kAAAAC/kopfsch%C3%BCtteln-an-kopf-fassen-oh-no.gif',
title: '*Nawdic has done an oopsie*',
},
{
user: 'monster',
img: 'https://media.tenor.com/ZIzIjb_wvEoAAAAC/face-palm.gif',
title: '*Monster has broken something*',
}
];
if (message.type === Discord.MessageType.GuildBoost && message.channelId === GeneralChatID) message.channel.send({content: outgoingArrays.guildBoost[Math.floor(Math.random() * outgoingArrays.guildBoost.length)], allowedMentions: {parse: ['users']}})
if (message.mentions.members.has('309373272594579456') && !MessageTool.isStaff(message.member)) message.reply('Please don\'t tag Daggerwin, read rule 14 in <#468846117405196289>');
if (message.mentions.members.has('215497515934416896') && !MessageTool.isStaff(message.member) && message.type != 19) message.reply('Please don\'t tag Monster unless it\'s important!');
if (message.mentions.members.has('215497515934416896') && !MessageTool.isStaff(message.member) && message.type != Discord.MessageType.Reply) message.reply('Please don\'t tag Monster unless it\'s important!');
if (incomingArrays.password.some(e=>message.content.toLowerCase().includes(e))) message.reply('Password and other details can be found in <#543494084363288637>');
if (incomingArrays.cantRead.some(e=>message.content.toLowerCase().includes(e))) message.reply('https://tenor.com/view/aristocats-george-pen-cap-meticulous-gif-5330931');
if (message.content.toLowerCase().includes('is daggerbot working')) message.reply('https://tenor.com/view/i-still-feel-alive-living-existing-active-singing-gif-14630579');
if (incomingArrays.deadChat.some(e=>message.content.toLowerCase().includes(e))) message.reply('https://cdn.discordapp.com/attachments/925589318276382720/1011333656167579849/F57G5ZS.png');
if (Automoderator.scanMsg(message).includes('nawdic') && incomingArrays.theyBrokeIt.some(e=>Automoderator.scanMsg(message).includes(e)) && MessageTool.isStaff(message.member) && message.channelId !== '516344221452599306') message.reply({embeds: [new client.embed().setTitle('*Nawdic has done an oopsie*').setImage('https://c.tenor.com/JSj9ie_MD9kAAAAC/kopfsch%C3%BCtteln-an-kopf-fassen-oh-no.gif').setColor(client.config.embedColor)]});
if (Automoderator.scanMsg(message).includes('monster') && incomingArrays.theyBrokeIt.some(e=>Automoderator.scanMsg(message).includes(e)) && MessageTool.isStaff(message.member) && message.channelId !== '516344221452599306') message.reply({embeds: [new client.embed().setTitle('*Monster has broken something*').setImage('https://media.tenor.com/ZIzIjb_wvEoAAAAC/face-palm.gif').setColor(client.config.embedColor)]});
if (incomingArrays.cantRead.some(e=>message.content.toLowerCase().includes(e))) message.reply(picStorage.cantRead);
if (message.content.toLowerCase().includes('is daggerbot working')) message.reply(picStorage.amAlive);
if (incomingArrays.deadChat.some(e=>message.content.toLowerCase().includes(e))) message.reply(picStorage.deadChat);
for (const thisPerson of ModsGoGetThisPerson) {
if (incomingArrays.theyBrokeIt.some(x=>Automoderator.scanMsg(message).includes(x) && Automoderator.scanMsg(message).includes(thisPerson.user)) && MessageTool.isStaff(message.member) && message.channelId !== client.config.dcServer.channels.mpmod_chat)
message.reply({embeds: [new client.embed().setTitle(thisPerson.title).setImage(thisPerson.img).setColor(client.config.embedColor)]});
}
}
}
}

View File

@ -2,12 +2,12 @@ import Discord from 'discord.js';
import TClient from '../client.js';
import Logger from '../helpers/Logger.js';
import {escapeCodeBlock} from 'discord.js';
export default {
run(client:TClient, msg:Discord.Message){
export default class MessageDelete {
static run(client:TClient, msg:Discord.Message){
if (!client.config.botSwitches.logs) return;
const disabledChannels = ['548032776830582794', '541677709487505408', '949380187668242483']
if (msg.guild?.id != client.config.mainServer.id || msg.partial || msg.author.bot || disabledChannels.includes(msg.channelId)) return;
if (Discord.DiscordAPIError.name === '10008') return Logger.forwardToConsole('log', 'MsgDelete', 'Caught an unexpected error returned by Discord API. (Unknown Message)');
if (msg.guild?.id != client.config.dcServer.id || msg.partial || msg.author.bot || disabledChannels.includes(msg.channelId)) return;
if (Discord.DiscordAPIError.name === '10008') return Logger.console('log', 'MsgDelete', 'Caught an unexpected error returned by Discord API. (Unknown Message)');
const embed = new client.embed().setColor(client.config.embedColorRed).setTimestamp().setAuthor({name: `Author: ${msg.author.username} (${msg.author.id})`, iconURL: `${msg.author.displayAvatarURL()}`}).setTitle('Message deleted').setDescription(`<@${msg.author.id}>\n\`${msg.author.id}\``);
if (msg.content.length != 0) embed.addFields({name: 'Content', value: `\`\`\`\n${escapeCodeBlock(msg.content.slice(0,1000))}\n\`\`\``});
embed.addFields(
@ -16,6 +16,6 @@ export default {
)
const attachments: Array<string> = [];
msg.attachments.forEach(x=>attachments.push(x.url));
(client.channels.resolve(client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds: [embed], files: attachments})
(client.channels.resolve(client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [embed], files: attachments})
}
}

View File

@ -1,8 +1,8 @@
import Discord from 'discord.js';
import TClient from '../client.js';
export default {
run(client:TClient, messages:Discord.Collection<string, Discord.Message<boolean>>, channel:Discord.GuildTextBasedChannel){
if (!client.config.botSwitches.logs || channel.guildId != 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().setTitle(`${messages.size} messages were purged`).setDescription(`\`\`\`${messages.map(msgs=>`${msgs.author?.username}: ${msgs.content}`).reverse().join('\n').slice(0,3900)}\`\`\``).addFields({name: 'Channel', value: `<#${messages.first().channel.id}>`})]})
export default class MessageDeleteBulk {
static run(client:TClient, messages:Discord.Collection<string, Discord.Message<boolean>>, channel:Discord.GuildTextBasedChannel){
if (!client.config.botSwitches.logs || channel.guildId != client.config.dcServer.id) return;
(client.channels.resolve(client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [new client.embed().setColor(client.config.embedColorRed).setTimestamp().setTitle(`${messages.size} messages were purged`).setDescription(`\`\`\`${messages.map(msgs=>`${msgs.author?.username}: ${msgs.content}`).reverse().join('\n').slice(0,3900)}\`\`\``).addFields({name: 'Channel', value: `<#${messages.first().channel.id}>`})]})
}
}

View File

@ -1,11 +1,11 @@
import Discord from 'discord.js';
import TClient from '../client.js';
export default {
run(client:TClient, reaction:Discord.MessageReaction, user:Discord.User){
export default class MessageReactionAdd {
static run(client:TClient, reaction:Discord.MessageReaction, user:Discord.User){
if (!client.config.botSwitches.logs) return;
if (reaction.message.guildId != client.config.mainServer.id || reaction.message.partial) return;
if (reaction.message.guildId != client.config.dcServer.id || reaction.message.partial) return;
const ReactedFirst = reaction.users.cache.first();
if (ReactedFirst.id != user.id) return;
if (reaction.emoji.name === '🖕') return (client.channels.cache.get(client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds:[new client.embed().setColor(client.config.embedColorYellow).setTimestamp().setAuthor({name: `Author: ${ReactedFirst.username} (${ReactedFirst.id})`, iconURL: `${ReactedFirst.displayAvatarURL()}`}).setTitle('Message reaction').setDescription(`<@${ReactedFirst.id}>\nAdded a reaction to the message.\n**Emoji**\n${reaction.emoji.name}\n**Channel**\n<#${reaction.message.channelId}>`).setFooter({text: 'Possibly this member, bot fetches who reacted first.'})], components: [new Discord.ActionRowBuilder<Discord.ButtonBuilder>().addComponents(new Discord.ButtonBuilder().setStyle(5).setURL(`${reaction.message.url}`).setLabel('Jump to message'))]});
if (reaction.emoji.name === '🖕') return (client.channels.cache.get(client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds:[new client.embed().setColor(client.config.embedColorYellow).setTimestamp().setAuthor({name: `Author: ${ReactedFirst.username} (${ReactedFirst.id})`, iconURL: `${ReactedFirst.displayAvatarURL()}`}).setTitle('Message reaction').setDescription(`<@${ReactedFirst.id}>\nAdded a reaction to the message.\n**Emoji**\n${reaction.emoji.name}\n**Channel**\n<#${reaction.message.channelId}>`).setFooter({text: 'Possibly this member, bot fetches who reacted first.'})], components: [new Discord.ActionRowBuilder<Discord.ButtonBuilder>().addComponents(new Discord.ButtonBuilder().setStyle(5).setURL(`${reaction.message.url}`).setLabel('Jump to message'))]});
}
}

View File

@ -1,8 +1,8 @@
import Discord from 'discord.js';
import TClient from '../client.js';
export default {
run(client:TClient, reaction:Discord.MessageReaction, user:Discord.User){
if (!client.config.botSwitches.logs || reaction.message.guildId != client.config.mainServer.id || reaction.message.partial) return;
if (reaction.emoji.name === '🖕') return (client.channels.cache.get(client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds:[new client.embed().setColor(client.config.embedColorRed).setTimestamp().setAuthor({name: `Author: ${user.username} (${user.id})`, iconURL: `${user.displayAvatarURL()}`}).setTitle('Message reaction').setDescription(`<@${user.id}>\nRemoved a reaction from the message.\n**Emoji**\n${reaction.emoji.name}\n**Channel**\n<#${reaction.message.channelId}>`)]})
export default class MessageReactionRemove {
static run(client:TClient, reaction:Discord.MessageReaction, user:Discord.User){
if (!client.config.botSwitches.logs || reaction.message.guildId != client.config.dcServer.id || reaction.message.partial) return;
if (reaction.emoji.name === '🖕') return (client.channels.cache.get(client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds:[new client.embed().setColor(client.config.embedColorRed).setTimestamp().setAuthor({name: `Author: ${user.username} (${user.id})`, iconURL: `${user.displayAvatarURL()}`}).setTitle('Message reaction').setDescription(`<@${user.id}>\nRemoved a reaction from the message.\n**Emoji**\n${reaction.emoji.name}\n**Channel**\n<#${reaction.message.channelId}>`)]})
}
}

View File

@ -2,12 +2,12 @@ import Discord from 'discord.js';
import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js';
import {escapeCodeBlock} from 'discord.js';
export default {
async run(client:TClient, oldMsg:Discord.Message, newMsg:Discord.Message){
export default class MessageUpdate {
static async run(client:TClient, oldMsg:Discord.Message, newMsg:Discord.Message){
if (!client.config.botSwitches.logs) return;
if (oldMsg.guild?.id != client.config.mainServer.id || oldMsg.author === null || oldMsg?.author.bot || oldMsg.partial || newMsg.partial || !newMsg.member || ['548032776830582794', '541677709487505408', '949380187668242483'].includes(newMsg.channelId)) return;
if (await client.bannedWords._content.findOne({_id:newMsg.content.toLowerCase().replaceAll(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?\n]/g, ' ').split(' ')}) && (!MessageTool.isStaff(newMsg.member))) newMsg.delete();
if (oldMsg.guild?.id != client.config.dcServer.id || oldMsg.author === null || oldMsg?.author.bot || oldMsg.partial || newMsg.partial || !newMsg.member || ['548032776830582794', '541677709487505408', '949380187668242483'].includes(newMsg.channelId)) return;
if (await client.prohibitedWords.findWord(newMsg.content.toLowerCase().replaceAll(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?\n?0-9]|[]|ing\b/g, '').split(' ').join('')) && (!MessageTool.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.username} (${oldMsg.author.id})`, iconURL: `${oldMsg.author.displayAvatarURL()}`}).setTitle('Message edited').setDescription(`<@${oldMsg.author.id}>\nOld content:\n\`\`\`\n${oldMsg.content.length < 1 ? '(Attachment)' : escapeCodeBlock(oldMsg.content.slice(0,2048))}\n\`\`\`\nNew content:\n\`\`\`\n${escapeCodeBlock(newMsg.content.slice(0,2048))}\`\`\`\nChannel: <#${oldMsg.channelId}>`)], components: [new Discord.ActionRowBuilder<Discord.ButtonBuilder>().addComponents(new Discord.ButtonBuilder().setStyle(5).setURL(`${oldMsg.url}`).setLabel('Jump to message'))]});
(client.channels.resolve(client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [new client.embed().setColor(client.config.embedColor).setTimestamp().setAuthor({name: `Author: ${oldMsg.author.username} (${oldMsg.author.id})`, iconURL: `${oldMsg.author.displayAvatarURL()}`}).setTitle('Message edited').setDescription(`<@${oldMsg.author.id}>\nOld content:\n\`\`\`\n${oldMsg.content.length < 1 ? '(Attachment)' : escapeCodeBlock(oldMsg.content.slice(0,2048))}\n\`\`\`\nNew content:\n\`\`\`\n${escapeCodeBlock(newMsg.content.slice(0,2048))}\`\`\`\nChannel: <#${oldMsg.channelId}>`)], components: [new Discord.ActionRowBuilder<Discord.ButtonBuilder>().addComponents(new Discord.ButtonBuilder().setStyle(5).setURL(`${oldMsg.url}`).setLabel('Jump to message'))]});
}
}

View File

@ -1,11 +1,17 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import ansi from 'ansi-colors';
export default {
async run(client:TClient){
export default class Ready {
static async run(client:TClient){
const botSwitches = Object.entries(client.config.botSwitches).map(([k, v])=>`${ansi.yellow(k)}${ansi.black(':')} ${v}`).join('\n').replace(/true/g, ansi.green('true')).replace(/false/g, ansi.red('false'));
await client.guilds.fetch(client.config.mainServer.id).then(async guild=>{
await client.guilds.fetch(client.config.dcServer.id).then(async guild=>{
const logsArray = [client.config.dcServer.channels.logs, client.config.dcServer.channels.bankick_log];
for (const channelID of logsArray) {
const channel = await client.channels.fetch(channelID) as Discord.TextChannel;
if (channel && channel.type === Discord.ChannelType.GuildText) await channel.messages.fetch({limit: 15});
}
await guild.members.fetch();
setInterval(()=>{
client.user.setPresence(client.config.botPresence);
@ -16,12 +22,12 @@ export default {
console.log('Total commands: '+client.registry.length) //Debugging reasons.
client.config.whitelistedServers.forEach(guildId=>(client.guilds.cache.get(guildId) as Discord.Guild).commands.set(client.registry).catch((e:Error)=>{
console.log(`Couldn't register slash commands for ${guildId} because`, e.stack);
(client.channels.resolve(client.config.mainServer.channels.errors) as Discord.TextChannel).send(`Cannot register slash commands for **${client.guilds.cache.get(guildId).name}** (\`${guildId}\`):\n\`\`\`${e.message}\`\`\``)
(client.channels.resolve(client.config.dcServer.channels.errors) as Discord.TextChannel).send(`Cannot register slash commands for **${client.guilds.cache.get(guildId).name}** (\`${guildId}\`):\n\`\`\`${e.message}\`\`\``)
}))
}
console.log(`${client.user.username} has logged into Discord API`);
console.log(client.config.botSwitches, client.config.whitelistedServers);
(client.channels.resolve(client.config.mainServer.channels.bot_status) as Discord.TextChannel).send({content: `**${client.user.username}** is active`, embeds:[new client.embed().setColor(client.config.embedColor).setDescription(`**\`\`\`ansi\n${botSwitches}\n\`\`\`**`)]});
(client.channels.resolve(client.config.dcServer.channels.bot_status) as Discord.TextChannel).send({content: `**${client.user.username}** is active`, embeds:[new client.embed().setColor(client.config.embedColor).setDescription(`**\`\`\`ansi\n${botSwitches}\n\`\`\`**`)]});
console.timeEnd('Startup')
}
}

View File

@ -1,36 +0,0 @@
import Discord from 'discord.js';
import TClient from '../client.js';
export default class Automoderator {
static Whitelist(message:Discord.Message, ...arr:string[]){// Array of channel ids for automod to be disabled in (Disables bannedWords and advertisement, mind you.)
return arr.includes(message.channelId);
}
static scanMsg(message:Discord.Message){
return message.content.toLowerCase().replaceAll(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?\n?0-9]|ing\b/g, '').split(' ')
}
static async repeatedMessages(client:TClient, message:Discord.Message, thresholdTime:number, thresholdAmount:number, type:string, muteTime:string, muteReason:string){
if (client.repeatedMessages[message.author.id]){
// Add message to the list
client.repeatedMessages[message.author.id].data.set(message.createdTimestamp, {type, channel: message.channelId});
// Reset the timeout
clearTimeout(client.repeatedMessages[message.author.id].timeout);
client.repeatedMessages[message.author.id].timeout = setTimeout(()=>delete client.repeatedMessages[message.author.id], thresholdTime);
// Message sent after (now - threshold), so purge those that were sent earlier
client.repeatedMessages[message.author.id].data = client.repeatedMessages[message.author.id].data.filter((_,i)=>i >= Date.now() - thresholdTime);
// A spammed message is one that has been sent within the threshold parameters
const spammedMessage = client.repeatedMessages[message.author.id].data.find(x=>{
return client.repeatedMessages[message.author.id].data.filter(y=>x.type===y.type).size >= thresholdAmount;
});
if (spammedMessage){
delete client.repeatedMessages[message.author.id];
await client.punishments.addPunishment('mute', {time: muteTime}, (client.user as Discord.User).id, `[AUTOMOD] ${muteReason}`, message.author, message.member as Discord.GuildMember);
}
} else {
client.repeatedMessages[message.author.id] = {data: new client.collection(), timeout: setTimeout(()=>delete client.repeatedMessages[message.author.id], thresholdTime)};
client.repeatedMessages[message.author.id].data.set(message.createdTimestamp, {type, channel: message.channelId});
}
}
}

View File

@ -1,49 +0,0 @@
import {createClient, ErrorReply} from 'redis';
import Logger from '../helpers/Logger.js';
import TSClient from '../helpers/TSClient.js';
let Prefix = 'Cache';
const RedisClient = createClient({
url: (await TSClient.Token()).redis_uri,
database: 0,
name: 'Daggerbot',
socket: {
keepAlive: 15000,
timeout: 30000
}
});
export default class CacheServer {
protected static eventManager() {
RedisClient
.on('connect', ()=>Logger.forwardToConsole('log', Prefix, 'Connection to Redis has been established'))
.on('error', (err:ErrorReply)=>{
Logger.forwardToConsole('error', Prefix, `Encountered an error in Redis: ${err.message}`)
setTimeout(async()=>{
if (!RedisClient.isReady) {
Logger.forwardToConsole('log', Prefix, 'Client is zombified, starting a fresh connection...');
RedisClient.quit();
await RedisClient.connect();
}
}, 1500)
})
}
static async get(key:any) {
const cachedResult = await RedisClient.get(key);
if (cachedResult) return JSON.parse(cachedResult);
else return null
}
static async set(key:any, value:any) {
return await RedisClient.set(key, JSON.stringify(value));
}
static async expiry(key:any, time:number) {
return await RedisClient.expire(key, time); // NOTE: time is in seconds, not milliseconds -- you know what you did wrong
}
static async delete(key:any) {
return await RedisClient.del(key);
}
static init() {
RedisClient.connect();
this.eventManager();
}
}

View File

@ -1,23 +0,0 @@
import Discord from 'discord.js';
import TClient from '../client.js';
export default class CmdTrigger {
private static readonly prefix = '!!_';
private static SenseTrigger(message:Discord.Message, trigger:string): boolean {
return message.content.toLowerCase().startsWith(this.prefix+trigger)
}
static registerCmds(client:TClient, message:Discord.Message, trigger:string) {
if (this.SenseTrigger(message, trigger) && client.config.whitelist.includes(message.author.id)) {
(client.guilds.cache.get(message.guildId) as Discord.Guild).commands.set(client.registry)
.then(()=>message.reply('How did you manage to lose the commands??? Anyways, it\'s re-registered now.'))
.catch((e:Error)=>message.reply(`Failed to deploy slash commands:\n\`\`\`${e.message}\`\`\``));
}
}
static MFPwTrigger(message:Discord.Message, trigger:string) {
if (this.SenseTrigger(message, trigger)) {
let farmPwText = 'The farm password is ';
if (message.channelId === '1149138133514981386') return message.reply(farmPwText += '`westfarm`')
else if (message.channelId === '1149138202662293555') return message.reply(farmPwText += '`eastfarm`')
}
}
}

View File

@ -1,35 +0,0 @@
import mongoose from 'mongoose';
import Logger from '../helpers/Logger.js';
import TSClient from '../helpers/TSClient.js';
const connection:mongoose.Connection = mongoose.connection;
export default class DatabaseServer {
protected static eventManager() {
let dbPrefix = 'Database';
connection
.on('connected', ()=>Logger.forwardToConsole('log', dbPrefix, 'Connection to MongoDB has been established'))
.on('disconnected', ()=>Logger.forwardToConsole('log', dbPrefix, 'Connection to MongoDB has been lost'))
.on('close', ()=>Logger.forwardToConsole('log', dbPrefix, 'MongoDB has closed the connection'))
.on('all', ()=>Logger.forwardToConsole('log', dbPrefix, 'Successfully established a connection to all members'))
.on('fullsetup', ()=>Logger.forwardToConsole('log', dbPrefix, 'Successfully established a connection to Primary server & atleast one member'))
.on('error', (err:mongoose.Error)=>Logger.forwardToConsole('error', dbPrefix, `Encountered an error in MongoDB: ${err.message}`))
}
protected static async connect() {
connection.set('strictQuery', true);
connection.openUri((await TSClient.Token()).mongodb_uri, {
replicaSet: 'toastyy',
autoIndex: true,
authMechanism: 'SCRAM-SHA-256',
authSource: 'admin',
serverSelectionTimeoutMS: 15000,
waitQueueTimeoutMS: 50000,
socketTimeoutMS: 30000,
tls: false,
family: 4
})
}
static init() {
this.connect();
this.eventManager();
}
}

View File

@ -1,23 +0,0 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import ConfigHelper from '../helpers/ConfigHelper.js';
const config = ConfigHelper.readConfig();
type ChannelList = keyof typeof config.mainServer.channels;
export default class HookMgr {
protected static async channelFetch(client:TClient, channel:ChannelList) {
return await client.channels.fetch(config.mainServer.channels[channel]) as Discord.TextChannel;
}
protected static async fetch(client:TClient, channel:ChannelList, webhookId:Discord.Snowflake) {
const hookInstance = await (await this.channelFetch(client, channel)).fetchWebhooks().then(x=>x.find(y=>y.id===webhookId));
if (!hookInstance) throw new Error('[HookManager] Webhook not found.');
return hookInstance;
}
static async send(client:TClient, channel:ChannelList, webhookId:Discord.Snowflake, message:string|Discord.MessagePayload|Discord.WebhookMessageCreateOptions) {
const hook = await this.fetch(client, channel, webhookId);
return hook.send(message).catch(err=>(client.channels.resolve(config.mainServer.channels.errors) as Discord.TextChannel).send(`Failed to send a webhook message in #${channel}:\n\`\`\`\n${err.message}\n\`\`\``));
}
static async edit(client:TClient, channel:ChannelList, webhookId:Discord.Snowflake, messageId:Discord.Snowflake, message:string|Discord.MessagePayload|Discord.WebhookMessageEditOptions) {
const hook = await this.fetch(client, channel, webhookId);
return hook.editMessage(messageId, message).catch(err=>(client.channels.resolve(config.mainServer.channels.errors) as Discord.TextChannel).send(`Failed to edit a webhook message in #${channel}:\n\`\`\`\n${err.message}\n\`\`\``));
}
}

View File

@ -1,138 +0,0 @@
interface TServer {
ip: string
code: string
}
import Discord from 'discord.js';
import TClient from '../client.js';
import FormatPlayer from '../helpers/FormatPlayer.js';
import Logger from '../helpers/Logger.js';
import HookMgr from './HookManager.js';
import {writeFileSync, readFileSync} from 'node:fs';
import {FSPlayer, FSData, FSCareerSavegame} from '../typings/interfaces';
export default async(client:TClient, Channel:string, Message:string, Server:TServer, ServerName:string)=>{
let playerData:Array<string> = [];
let dataUnavailable = 'Unavailable'
const msg = await (client.channels.resolve(Channel) as Discord.TextChannel).messages.fetch(Message);
const serverErrorEmbed = new client.embed().setColor(client.config.embedColorRed).setTitle('Host did not respond back in time');
const genericEmbed = new client.embed();
const refreshIntervalText = 'Refreshes every 35 seconds.';
let sessionInit = {signal: AbortSignal.timeout(8500),headers:{'User-Agent':`${client.user.username} - MPModule/undici`}};
try {
const hitDSS = await fetch(Server.ip+'/feed/dedicated-server-stats.json?code='+Server.code, sessionInit).then(r=>r.json() as Promise<FSData>);
const hitCSG = await fetch(Server.ip+'/feed/dedicated-server-savegame.html?code='+Server.code+'&file=careerSavegame', sessionInit).then(async r=>(new client.fxp.XMLParser({ignoreAttributes: false, attributeNamePrefix: ''}).parse(await r.text()) as any).careerSavegame as FSCareerSavegame);
if (!hitDSS ?? !hitCSG){
if (hitDSS && !hitDSS.slots) return Logger.forwardToConsole('log', 'MPModule', `DSS failed with unknown slots table for ${client.MPServerCache[ServerName].name}`);
else return msg.edit({embeds: [serverErrorEmbed]});
}
// Truncate unnecessary parts of the name for the MPServerCache
// This is a mess, but it works.
for (const filter of ['Official Daggerwin Game Server', 'Daggerwin Multifarm']) {
if (hitDSS.server?.name === undefined) return;
if (hitDSS.server?.name.includes(filter)) client.MPServerCache[ServerName].name = ['Daggerwin', 'DagMF'][['Official Daggerwin Game Server', 'Daggerwin Multifarm'].indexOf(filter)];
}
//Timescale formatting
function formatTimescale(number:number,digits:number,icon:string){
var n = Number(number);
return n.toLocaleString(undefined, {minimumFractionDigits: digits})+icon
}
// Join/Leave log
function playerLogEmbed(player:FSPlayer,joinLog:boolean){
const logEmbed = new client.embed().setDescription(`**${player.name}${FormatPlayer.decoratePlayerIcons(player)}** ${joinLog ? 'joined' : 'left'} **${client.MPServerCache[ServerName].name}** at <t:${Math.round(Date.now()/1000)}:t>`);
if (joinLog) return logEmbed.setColor(client.config.embedColorGreen);
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 Logger.forwardToConsole('log', 'MPModule', 'Array is empty, 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))){
if (player.uptime > 0) serverLog.send({embeds:[playerLogEmbed(player,false)]});
} // Player joining
let playerObject;
if (!playersInCache.length && client.uptime > 32010) playerObject = playersOnServer;
if (playerObject) for (const player of playerObject) serverLog.send({embeds:[playerLogEmbed(player,true)]});
else if (playersInCache.length) playerObject = playersOnServer.filter(x=>!playersInCache.some(y=>y.name === x.name));
if (client.MPServerCache[ServerName].name === null) return;
const Database:Array<number> = JSON.parse(readFileSync(`src/database/${client.MPServerCache[ServerName].name}PlayerData.json`,{encoding:'utf8',flag:'r+'}));
Database.push(hitDSS.slots?.used);
writeFileSync(`src/database/${client.MPServerCache[ServerName].name}PlayerData.json`, JSON.stringify(Database));
client.MPServerCache[ServerName].players = playersOnServer;
if (hitDSS.server.name.length < 1) {
msg.edit({content: 'This embed will resume when server is back online.', embeds: [genericEmbed.setColor(client.config.embedColorRed).setTitle('The server seems to be offline.')]});
client.MPServerCache[ServerName].status = 'offline'
} else {
client.MPServerCache[ServerName].status = 'online';
const serverDetails = new client.embed().setColor(client.config.embedColor).setTitle('Server details').setFields(
{name: 'Current map', value: hitDSS?.server?.mapName === undefined ? dataUnavailable : hitDSS.server.mapName, inline: true},
{name: 'Server version', value: hitDSS?.server?.version === undefined ? dataUnavailable : hitDSS.server.version, inline: true},
{name: 'In-game Time', value: `${('0'+Math.floor((hitDSS.server.dayTime/3600/1000))).slice(-2)}:${('0'+Math.floor((hitDSS.server.dayTime/60/1000)%60)).slice(-2)}`, inline: true},
{name: 'Slot Usage', value: isNaN(Number(hitCSG?.slotSystem?.slotUsage)) === true ? dataUnavailable : Number(hitCSG.slotSystem?.slotUsage).toLocaleString('en-us'), inline: true},
{name: 'Autosave Interval', value: isNaN(Number(hitCSG?.settings?.autoSaveInterval)) === true ? dataUnavailable : Number(hitCSG.settings?.autoSaveInterval).toFixed(0)+' mins', inline:true},
{name: 'Timescale', value: isNaN(Number(hitCSG?.settings?.timeScale)) === true ? dataUnavailable : formatTimescale(Number(hitCSG.settings?.timeScale), 0, 'x'), inline: true}
);
const playersEmbed = new client.embed().setColor(client.config.embedColor).setTitle(hitDSS.server.name).setDescription(hitDSS.slots.used < 1 ? '*No players online*' : playerData.join('\n\n')).setAuthor({name:`${hitDSS.slots.used}/${hitDSS.slots.capacity}`});
msg.edit({content:refreshIntervalText,embeds:[serverDetails, playersEmbed]});
}
// #multifarm_chat webhook
const growthModeTextMap = {
'1': 'Yes',
'2': 'No',
'3': 'Growth paused'
}
const growthModeText = growthModeTextMap[hitCSG?.settings.growthMode] ?? dataUnavailable;
function genericMapping<T>(map: Record<string, T>, key: string, defaultValue: T): T {
return map[key] ?? defaultValue;
}
const genericTextMap = {
'false': 'Off',
'true': 'On'
}
const fuelUsageTextMap = {
'1': 'Low',
'2': 'Normal',
'3': 'High'
}
const fuelUsageText = fuelUsageTextMap[hitCSG?.settings.fuelUsage] ?? dataUnavailable;
const dirtIntervalTextMap = {
'1': 'Off',
'2': 'Slow',
'3': 'Normal',
'4': 'Fast'
}
const dirtIntervalText = dirtIntervalTextMap[hitCSG?.settings.dirtInterval] ?? dataUnavailable;
// Edit the embed in #multifarm_chat
HookMgr.edit(client, 'mf_chat', '1159998634604109897', '1160098458997370941', {
content: refreshIntervalText,
embeds: [new client.embed().setColor(client.config.embedColor).setTitle('Savegame Settings').addFields(
{name: 'Seasonal Growth', value: growthModeText, inline: true},
{name: 'Crop Destruction', value: genericMapping(genericTextMap, hitCSG?.settings.fruitDestruction, dataUnavailable), inline: true},
{name: 'Periodic Plowing', value: genericMapping(genericTextMap, hitCSG?.settings.plowingRequiredEnabled, dataUnavailable), inline: true},
{name: 'Stones', value: genericMapping(genericTextMap, hitCSG?.settings.stonesEnabled, dataUnavailable), inline: true},
{name: 'Lime', value: genericMapping(genericTextMap, hitCSG?.settings.limeRequired, dataUnavailable), inline: true},
{name: 'Weeds', value: genericMapping(genericTextMap, hitCSG?.settings.weedsEnabled, dataUnavailable), inline: true},
{name: 'Fuel Usage', value: fuelUsageText, inline: true},
{name: 'Dirt Interval', value: dirtIntervalText, inline: true},
).setFooter({text: 'Last updated'}).setTimestamp()]
});
} catch(err) {
msg.edit({content: err.message, embeds: [serverErrorEmbed]});
Logger.forwardToConsole('log', 'MPModule', `Failed to make a request for ${ServerName}: ${err.message}`);
}
}

View File

@ -1,21 +0,0 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import Logger from '../helpers/Logger.js';
import MessageTool from '../helpers/MessageTool.js';
export default async(client:TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>, type: string)=>{
if (!MessageTool.isStaff(interaction.member as Discord.GuildMember)) return MessageTool.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);
Logger.forwardToConsole('log', '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 && !['unban', 'ban'].includes(type)) 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);
}

View File

@ -1,38 +0,0 @@
import {TextChannel} from 'discord.js';
import TClient from '../client.js';
import TSClient from '../helpers/TSClient.js';
import Logger from '../helpers/Logger.js';
import CacheServer from './CacheServer.js';
import MessageTool from '../helpers/MessageTool.js';
export default async(client:TClient, YTChannelID:string, YTChannelName:string, DiscordChannelID:string, DiscordRoleID:string)=>{
let Data: any;
let cacheExpiry: number = 7200; // Invalidate cache after sitting in Redis for 2 hours
try {
await fetch(`https://youtube.googleapis.com/youtube/v3/activities?part=snippet&channelId=${YTChannelID}&maxResults=2&key=${(await TSClient.Token()).youtube}`, {
signal: AbortSignal.timeout(10000),
headers: {'User-Agent':'Daggerbot - Notification/undici'},
}).then(async json=>Data = await json.json());
} catch (err) {
Logger.forwardToConsole('log', 'YTModule', `Failed to fetch "${YTChannelName}" from YouTube`);
}
if (!Data) return;
const getVideoId = (index:number)=>Data.items[index].snippet.thumbnails.default.url.split('/')[4];
const videoUrl = `https://www.youtube.com/watch?v=${getVideoId(0)}`;
const cacheKey = `YTCache:${YTChannelID}`;
const cachedVideoId = await CacheServer.get(cacheKey);
if (!cachedVideoId) {
await CacheServer.set(cacheKey, getVideoId(0)).then(async()=>await CacheServer.expiry(cacheKey, cacheExpiry));
return;
}
if (getVideoId(1) === cachedVideoId) {
await CacheServer.delete(cacheKey).then(async()=>{
await CacheServer.set(cacheKey, getVideoId(0)).then(async()=>await CacheServer.expiry(cacheKey, cacheExpiry))
});
(client.channels.resolve(DiscordChannelID) as TextChannel).send({
content: `${MessageTool.formatMention(DiscordRoleID, 'role')}\n**${YTChannelName}** just uploaded a video!\n${videoUrl}`,
allowedMentions: {parse:['roles']},
});
}
}

View File

@ -1,10 +0,0 @@
import {parentPort} from 'node:worker_threads';
import {execSync} from 'node:child_process';
async function commitHashes() {
const localHash = execSync('git rev-parse HEAD').toString().trim().slice(0, 7);
const remoteHash = execSync('git ls-remote origin HEAD').toString().split('\t')[0].slice(0, 7);
return { localHash, remoteHash };
}
commitHashes().then(data=>parentPort.postMessage(data))

View File

@ -1,5 +1,5 @@
import {readFileSync} from 'node:fs';
import {Config} from '../typings/interfaces';
import {Config} from '../interfaces';
export default class ConfigHelper {
static loadConfig() {
let importconfig:Config;
@ -12,7 +12,6 @@ export default class ConfigHelper {
}
return importconfig;
}
static readConfig() {
return JSON.parse(readFileSync(process.argv[2] ?? 'src/config.json', 'utf8')) as Config;
}
static readConfig =()=>JSON.parse(readFileSync(process.argv[2] ?? 'src/config.json', 'utf8')) as Config;
static isDevMode =()=>this.readConfig().configName.includes('Beta');
}

30
src/helpers/FAQHelper.ts Normal file
View File

@ -0,0 +1,30 @@
import Discord from 'discord.js';
import MessageTool from './MessageTool.js';
import ConfigHelper from './ConfigHelper.js';
const config = ConfigHelper.readConfig();
export default class FAQHelper {
private static readonly errorMsg:string = 'Failed to send the message, please report to **Toast** if it continues.';
private static ansiCodeblock=(...lines:string[])=>MessageTool.concatMessage('```ansi', ...lines, '```')
public static async reply(interaction:Discord.ChatInputCommandInteraction, title:string|null, message:string, image:string|null, useEmbed:boolean=false) {
if (useEmbed) return interaction.reply({embeds: [MessageTool.embedStruct(config.embedColor, title, message, image)]}).catch(err=>interaction.reply(this.errorMsg+'\n'+err))
else return interaction.reply(message).catch(err=>interaction.reply(this.errorMsg+'\n'+err))
}
public static CDN=(filename:string)=>'https://cdn.toast-server.net/daggerwin/'+filename+'.png';
public static youCanGetRole=(role:string, roleEmoji:string)=>`You can get the ${MessageTool.formatMention(config.dcServer.roles[role], 'role')} role from <#802283932430106624> by clicking :${roleEmoji}: button on a webhook's message.`;
public static readonly verifyGameFiles = this.ansiCodeblock(
'Steam (Top panel)',
'1. Go to your game library and right click on Farming Simulator 22',
'2. Click on Properties and navigate to "Installed Files"',
'3. Click on "Verify integrity of game files"',
'4. Steam will scan your game installation directory and will re-download anything that is corrupted or tampered with.',
'',
'Epic Games (Bottom panel)',
'1. Go to your game library and click on 3 dots (...)',
'2. Click on Manage and click on "Verify"',
'3. Epic Launcher will scan your game installation directory and will re-download anything that is corrupted or tampered with.'
)
public static readonly linkMapping = {
ballyspring: 'https://www.farming-simulator.com/mod.php?mod_id=270745',
staffTicket: 'https://discord.com/channels/468835415093411861/942173932339986472/1054128182468546631',
}
}

View File

@ -1,11 +0,0 @@
import Discord from 'discord.js';
import MessageTool from './MessageTool.js';
import ConfigHelper from './ConfigHelper.js';
const config = ConfigHelper.readConfig();
export default class FAQStore {
protected static readonly errorMsg:string = 'Failed to send the message, please report to **Toast** if it continues.';
static async reply(interaction:Discord.ChatInputCommandInteraction, title:string|null, message:string, image:string|null, useEmbed:boolean=false) {
if (useEmbed) return interaction.reply({embeds: [MessageTool.embedStruct(config.embedColor, title, message, image)]}).catch(err=>interaction.reply(this.errorMsg+'\n'+err))
else return interaction.reply(message).catch(err=>interaction.reply(this.errorMsg+'\n'+err))
}
}

View File

@ -1,5 +0,0 @@
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]
}

View File

@ -1,5 +0,0 @@
export default (number:number)=>{
const suffixes = ['th', 'st', 'nd', 'rd'];
const suffix = number % 100;
return number + (suffixes[(suffix - 20) % 10] || suffixes[suffix] || suffixes[0]);
}

View File

@ -1,22 +1,26 @@
import {FSPlayer} from '../typings/interfaces';
import {FSPlayer} from '../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 convertUptime(playTime:number) {
let Minutes:number;
let Hours:number;
let Days:number;
playTime = Math.floor(playTime);
if (playTime >= 60) {
Hours = Math.floor(playTime/60);
Minutes = playTime-Hours*60;
} else Minutes = playTime
if (Hours >= 24) {
Days = Math.floor(Hours/24);
Hours = Hours-Days*24;
}
return (Days > 0 ? Days+' d ':'')+(Hours > 0 ? Hours+' h ':'')+(Minutes > 0 ? Minutes+' m':'')
}
static decoratePlayerIcons(player:FSPlayer){
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
}
}

View File

@ -1,35 +0,0 @@
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();
}

50
src/helpers/Formatters.ts Normal file
View File

@ -0,0 +1,50 @@
export default class Formatters {
public static timeFormat(int:number, accuracy:number = 1, opts?:{longNames?:boolean, commas?:boolean}) {
let achievedAccuracy:number = 0;
let txt:string = '';
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 fullTimeLen = Math.floor(int/timeName.length);
if (fullTimeLen === 0) continue;
achievedAccuracy++;
txt += fullTimeLen + (opts?.longNames ? (' '+timeName.name+(fullTimeLen === 1 ? '' : 's')) : timeName.name.slice(0, timeName.name === 'month' ? 2 : 1)) + (opts?.commas ? ', ' : ' ');
int -= fullTimeLen*timeName.length;
} else break;
}
if (txt.length === 0) txt = int + (opts?.longNames ? ' milliseconds' : 'ms') + (opts?.commas ? ', ' : '');
if (opts?.commas) {
txt = txt.slice(0, -2);
if (opts?.longNames) {
const txtArr = txt.split('');
txtArr[txt.lastIndexOf(',')] = ' and';
txt = txtArr.join('');
}
} return txt.trim();
}
public static DayOfTheYear(num:number) {
const suffixes = ['th', 'st', 'nd', 'rd'];
const suffix = num % 100;
return num +(suffixes[(suffix-20)%10]||suffixes[suffix]||suffixes[0])
}
public static byteFormat(bytes:number) {
if (bytes === null ?? bytes === undefined ?? bytes <= 0) return '0 Bytes';
let scaleCount = 0;
let dataInitials = ['Bytes', 'KB', 'MB', 'GB'];
while (bytes >= 1024 && scaleCount < dataInitials.length -1) {
bytes /= 1024;
scaleCount++;
}
if (scaleCount >= dataInitials.length) scaleCount = dataInitials.length -1;
let compactedBytes = bytes.toFixed(2).replace(/\.?0+$/, '').replace(/\B(?=(\d{3})+(?!\d))/g, ',');
compactedBytes += ' '+dataInitials[scaleCount];
return compactedBytes.trim();
}
}

17
src/helpers/GitHub.ts Normal file
View File

@ -0,0 +1,17 @@
import {Octokit} from '@octokit/rest';
import {createTokenAuth} from '@octokit/auth-token';
import TSClient from './TSClient.js';
import git from 'simple-git';
const summonAuth = createTokenAuth((await TSClient()).octokit);
const octokit = new Octokit({auth: await summonAuth().token, timeZone: 'Australia/NSW', userAgent: 'Daggerbot-TS'});
export default class GitHub {
private static repositoryConfig = {owner: 'AnxietyisReal', repo: 'Daggerbot-TS', ref: 'HEAD'};
public static async RemoteRepository() {
const {data} = await octokit.repos.getCommit(this.repositoryConfig);
return data;
}
public static async LocalRepository() {
const {latest} = await git().log({maxCount: 1});
return latest;
}
}

View File

@ -1,13 +1,5 @@
import dayjs from 'dayjs';
export default class Logger {
static logTime() {
return `[${dayjs().format('DD/MM/YY HH:mm:ss')}]`;
}
static logPrefix(prefix:string) {
return `[${prefix}]`;
}
static forwardToConsole(logType:'log'|'error', prefix:string, message:string|any) {
console[logType](this.logTime(), this.logPrefix(prefix), message);
}
public static console =(logType:'log'|'error', prefix:string, message:any)=>console[logType](`[${dayjs().format('DD/MM/YY HH:mm:ss')}]`, `[${prefix}]`, message);
}

View File

@ -1,26 +1,17 @@
import Discord from 'discord.js';
import ConfigHelper from './ConfigHelper.js';
const config = ConfigHelper.readConfig();
type RoleKeys = keyof typeof config.mainServer.roles;
type RoleKeys = keyof typeof config.dcServer.roles;
export default class MessageTool {
static embedStruct(color:Discord.ColorResolvable, title:string, description?:string|null, image?:string|null){
static embedStruct(color:Discord.ColorResolvable, title:string, description?:string|null, image?:string|null) {
const embed = new Discord.EmbedBuilder().setColor(color).setTitle(title);
if (description) embed.setDescription(description);
if (image) embed.setImage(image);
return embed
}
static concatMessage(...messages:string[]){
return messages.join('\n')
}
static formatMention(mention:string, type:'user'|'channel'|'role'){
return `<@${type === 'role' ? '&' : type === 'channel' ? '#' : ''}${mention}>`
}
static isStaff(guildMember:Discord.GuildMember){
return config.mainServer.staffRoles.map((x:string)=>config.mainServer.roles[x]).some((x:string)=>guildMember.roles.cache.has(x));
}
static youNeedRole(interaction:Discord.CommandInteraction, role:RoleKeys){
return interaction.reply(`This command is restricted to ${this.formatMention(config.mainServer.roles[role], 'role')}`);
}
static concatMessage =(...messages:string[])=>messages.join('\n');
static formatMention =(mention:string, type:'user'|'channel'|'role')=>`<${type === 'role' ? '@&' : type === 'channel' ? '#' : '@'}${mention}>`;
static isStaff =(guildMember:Discord.GuildMember)=>config.dcServer.staffRoles.map((x:string)=>config.dcServer.roles[x]).some((x:string)=>guildMember.roles.cache.has(x));
static youNeedRole =(interaction:Discord.CommandInteraction, role:RoleKeys)=>interaction.reply(`You do not have ${this.formatMention(config.dcServer.roles[role], 'role')} role to use this command.`);
}
// I want to come up with better name instead of calling this file "MessageTool", but I am super bad at naming things.

View File

@ -1,12 +1,10 @@
import {FSData} from 'src/typings/interfaces';
import {FSData} from '../interfaces';
export default (serverEndpoint:FSData)=>{
const getAmount =(type:string)=>serverEndpoint.vehicles.filter(v=>v.type === 'pallet').map(v=>v.fills).flat().map(t=>t.type).filter(t=>t===type).length;
let palletTypeName = serverEndpoint.vehicles.filter(v=>v.type === 'pallet').map(v=>v.fills).flat().map(t=>t.type).filter((t,i,a)=>a.indexOf(t)===i).map(t=>({
[t]:{
name: t.toLowerCase().slice(0,1).toUpperCase()+t.toLowerCase().slice(1),
size: getAmount(t.toUpperCase())
},
})).reduce((a,b)=>({...a,...b}));
return palletTypeName;
export default function(data:FSData) {
const pallets = data.vehicles.filter(x=>x.category === 'PALLETS');
const counts = pallets.reduce((acc, name)=>{
acc[name.name] = (acc[name.name] ?? 0) + 1;
return acc;
}, {} as {[key:string]:number});
return counts;
}

View File

@ -0,0 +1,19 @@
export default ()=>{
const ranIntoSomething = [
'tree', 'rock',
'wall', 'fence',
'sign', 'car',
'bike', 'pedestrian',
'dog', 'cat',
'cow', 'sheep',
'bench', 'table',
'chair', 'house',
'building', 'skyscraper',
'statue', 'lamp post',
'traffic light', 'bridge',
'fountain', 'dumpster',
'mailbox', 'parking meter',
'bus', 'truck'
] as string[];
return ranIntoSomething[Math.floor(Math.random()*ranIntoSomething.length)];
}

View File

@ -1,13 +1,3 @@
interface TokenService_API {
main: string,
octokit: string,
youtube: string,
mongodb_uri: string,
redis_uri: string
}
import TokenService from '@toast/tokenservice-client';
export default class TSClient {
static async Token() {
return await fetch('http://192.168.68.18/daggerbot').then(x=>x.json()) as Promise<TokenService_API>
}
}
export default async()=>new TokenService('daggerbotbeta').connect();

View File

@ -1,6 +1,4 @@
export default class UsernameHelper {
static stripName(text: string){
const dirSlash = process.platform === 'linux' ? '\/' : '\\';
return text.replace(/(?<=\/Users\/|\/media\/)[^/]+/g, match=>'・'.repeat(match.length)).split(dirSlash).join(dirSlash);
}
export default (text:string)=>{
const dirSlash = process.platform === 'linux' ? '\/' : '\\';
return text?.replace(/(?<=\/Users\/|\/media\/)[^/]+/g, match=>'・'.repeat(match.length)).split(dirSlash).join(dirSlash);
}

View File

@ -3,55 +3,60 @@ import TClient from './client.js';
const client = new TClient;
client.init();
import Logger from './helpers/Logger.js';
import YTModule from './funcs/YTModule.js';
import MPModule from './funcs/MPModule.js';
import YTModule from './modules/YTModule.js';
import MPModule, {refreshTimerSecs} from './modules/MPModule.js';
import UsernameHelper from './helpers/UsernameHelper.js';
import {Punishment} from './typings/interfaces';
import {writeFileSync, readFileSync} from 'node:fs';
import {Punishment} from './interfaces';
import {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.
function _(error:Error, type:string) {
if (JSON.parse(readFileSync('src/errorBlocklist.json', 'utf8')).includes(error.message)) return;
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\`\`\`${`${UsernameHelper.stripName(error.stack)}`.slice(0, 2500)}\`\`\``)]})
(client.channels.resolve(client.config.dcServer.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\`\`\`${`${UsernameHelper(error.stack)}`.slice(0, 2500)}\`\`\``)]})
}
process.on('unhandledRejection', (error: Error)=>DZ(error, 'unhandledRejection'));
process.on('uncaughtException', (error: Error)=>DZ(error, 'uncaughtException'));
process.on('error', (error: Error)=>DZ(error, 'nodeError'));
client.on('error', (error: Error)=>DZ(error, 'clientError'));
process.on('unhandledRejection', (error: Error)=>_(error, 'unhandledRejection'));
process.on('uncaughtException', (error: Error)=>_(error, 'uncaughtException'));
process.on('error', (error: Error)=>_(error, 'processError'));
client.on('error', (error: Error)=>_(error, 'clientError'));
// YouTube Upload notification and MP loop
if (client.config.botSwitches.mpstats) setInterval(async()=>{
const serverlake = (await client.MPServer.findInCache(client.config.mainServer.id));
for await (const [locName, locArea] of Object.entries(client.config.MPStatsLocation)) await MPModule(client, locArea.channel, locArea.message, serverlake[locName], locName)
}, 35000); // 35 seconds
setInterval(async()=>{
YTModule(client, 'UCQ8k8yTDLITldfWYKDs3xFg', 'Daggerwin', '528967918772551702', '1155760735612305408'); // 528967918772551702 = #videos-and-streams; 1155760735612305408 = YT Upload Ping;
YTModule(client, 'UCguI73--UraJpso4NizXNzA', 'Machinery Restorer', '767444045520961567', '1155760735612305408') // 767444045520961567 = #machinery-restorer; ^^
}, 300000); // 5 minutes
// Interval timers for modules
setInterval(async()=>await MPModule(client), refreshTimerSecs); // Second param got moved to inside MPModule function to reduce the amount of failure rates.
setInterval(()=>YTModule(client), 180000); // 3 minutes
// Event loop for punishments and daily msgs
setInterval(async()=>{
const now = Date.now();
const punishments = await client.punishments.findInCache();
punishments.filter((x:Punishment)=>x.endTime && x.endTime<= now && !x.expired).forEach(async (punishment:Punishment)=>{
Logger.forwardToConsole('log', 'Punishment', `${punishment.member}\'s ${punishment.type} should expire now`);
Logger.forwardToConsole('log', 'Punishment', await client.punishments.removePunishment(punishment._id, client.user.id, 'Time\'s up!'));
punishments.filter((x:Punishment)=>x.endTime && x.endTime <= now && !x.expired).forEach(async (punishment:Punishment)=>{
Logger.console('log', 'Punishment', `${punishment.member}\'s ${punishment.type} should expire now`);
Logger.console('log', 'Punishment', await client.punishments.punishmentRemove(punishment.case_id, client.user.id, 'Time\'s up!'));
});
const formattedDate = Math.floor((now - client.config.LRSstart)/1000/60/60/24);
const dailyMsgs = JSON.parse(readFileSync('./src/database/dailyMsgs.json', 'utf8'))
if (client.config.botSwitches.dailyMsgsBackup && !dailyMsgs.some((x:Array<number>)=>x[0] === formattedDate)){
client.userLevels.resetAllData(); // reset all data on 1st of January every year
const dailyMsgs = await client.dailyMsgs.fetchDays();
if (client.config.botSwitches.dailyMsgsBackup && !dailyMsgs.some(x=>x[0] === formattedDate)) {
if (!dailyMsgs.find(x=>x.dataValues.day === formattedDate)) {
let total = (await client.userLevels.fetchEveryone()).reduce((a,b)=>a + b.messages, 0); // Sum of all users
const yesterday = dailyMsgs.find(x=>x.day === formattedDate - 1)
if (total < yesterday?.total) total = yesterday.total; // Messages went down.
await client.dailyMsgs.newDay(formattedDate, total);
Logger.console('log', 'DailyMsgs', `Pushed [${formattedDate}, ${total}]`)
let total = (await client.userLevels._content.find({})).reduce((a,b)=>a + b.messages, 0); // sum of all users
const yesterday = dailyMsgs.find((x:Array<number>)=>x[0] === formattedDate - 1);
if (total < yesterday) total = yesterday // messages went down.
dailyMsgs.push([formattedDate, total]);
writeFileSync('./src/database/dailyMsgs.json', JSON.stringify(dailyMsgs))
Logger.forwardToConsole('log', 'DailyMsgs', `Pushed [${formattedDate}, ${total}]`)
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 </rank leaderboard:${commands.find(x=>x.name === 'rank').id}>`));
(client.channels.resolve(client.config.mainServer.channels.thismeanswar) as Discord.TextChannel).send({files:['./src/database/dailyMsgs.json']}).catch(fileErr=>console.log(fileErr))
// Send notification to #bot-logs that the data has been pushed to database.
const commands = await client.guilds.cache.get(client.config.dcServer.id)?.commands.fetch();
if (commands) (client.channels.resolve(client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [
new client.embed().setDescription(`Pushed the following\ndata to </rank leaderboard:${commands.find(x=>x.name === 'rank').id}>`).setFields(
{name: 'Day', value: formattedDate.toString(), inline: true},
{name: 'Messages', value: Intl.NumberFormat('en-us').format(total).toString(), inline: true}
).setColor(client.config.embedColor)
]});
else Logger.console('log', 'DailyMsgs', 'Rank command not found, cannot send notification in channel')
}
}
}, 5000)
if (client.config.botSwitches.dailyMsgsBackup) client.userLevels.initSelfdestruct()
// Initiate the nuke on userLevels and dailyMsgs tables
// Also don't ask why it's outside the interval

View File

@ -1,7 +1,7 @@
import Discord from 'discord.js';
import {ColorResolvable, PresenceData} from 'discord.js';
export interface Punishment {
_id: number;
case_id: number;
type: string;
member: string;
moderator: string;
@ -27,9 +27,9 @@ export interface FSData {
slots: {
capacity: number,
used: number,
players: Array<FSPlayer>
players: FSPlayer[]
},
vehicles: Array<FSVehicle>
vehicles: FSVehicle[]
}
interface FSVehicle {
name: string,
@ -38,7 +38,7 @@ interface FSVehicle {
x: number,
y: number,
z: number,
fills: Array<FSVehicleFill>,
fills: FSVehicleFill[],
controller: string
}
interface FSVehicleFill {
@ -46,7 +46,7 @@ interface FSVehicleFill {
level: number
}
export interface FSPlayer {
isUsed: boolean,
isUsed?: boolean,
isAdmin: boolean,
uptime: number,
name: string
@ -108,42 +108,31 @@ export interface FSCareerSavegame {
}
export interface Config {
configName: string,
embedColor: Discord.ColorResolvable,
embedColorGreen: Discord.ColorResolvable,
embedColorOrange: Discord.ColorResolvable,
embedColorYellow: Discord.ColorResolvable,
embedColorRed: Discord.ColorResolvable,
embedColorBCA: Discord.ColorResolvable,
embedColorXmas: Discord.ColorResolvable,
embedColor: ColorResolvable,
embedColorGreen: ColorResolvable,
embedColorYellow: ColorResolvable,
embedColorRed: ColorResolvable,
embedColorInvis: ColorResolvable,
embedColorBCA: ColorResolvable,
embedColorXmas: ColorResolvable,
LRSstart: number,
whitelistedServers: Array<string>,
MPStatsLocation: {
mainServer: {
channel: string
message: string
},
secondServer: {
channel: string
message: string
}
},
whitelistedServers: string[],
botSwitches: {
dailyMsgsBackup: boolean,
registerCommands: boolean,
commands: boolean,
logs: boolean,
mpSys: boolean,
buttonRoles: boolean,
automod: boolean,
mpstats: boolean,
autores: boolean
},
botPresence: Discord.PresenceData,
eval: boolean,
whitelist: Array<string>,
contribList: Array<string>,
mainServer: {
botPresence: PresenceData,
whitelist: string[],
contribList: string[],
dcServer: {
id: string,
staffRoles: Array<string>,
staffRoles: string[],
roles: {
admin: string,
bottech: string,
@ -158,7 +147,6 @@ export interface Config {
vtcmember: string
},
channels: {
console: string,
errors: string,
thismeanswar: string,
bot_suggestions: string,
@ -167,10 +155,9 @@ export interface Config {
welcome: string,
botcommands: string,
bankick_log: string,
fs_server_log: string,
punishment_log: string,
dcmod_chat: string,
mf_chat: string
mpmod_chat: string
}
}
}

9
src/models/IMPORTS.ts Normal file
View File

@ -0,0 +1,9 @@
export * from './MPServer.js';
export * from './dailyMsgs.js';
export * from './bonkCount.js';
export * from './userLevels.js';
export * from './punishments.js';
export * from './prohibitedWords.js';
export * from './suggestions.js';
export * from './tagSystem.js';
export * from './ytChannels.js';

View File

@ -1,45 +1,131 @@
import TClient from '../client.js';
import mongoose from 'mongoose';
import CacheServer from '../funcs/CacheServer.js';
import DatabaseServer from '../components/DatabaseServer.js';
import {Model, DataTypes} from 'sequelize';
import CacheServer from '../components/CacheServer.js';
const Schema = mongoose.model('mpserver', new mongoose.Schema({
_id: {type: String, required:true},
mainServer: {required:true, type: new mongoose.Schema({
ip: {type: String, required:true},
code: {type: String, required:true}
}, {versionKey: false})},
secondServer: {required:true, type: new mongoose.Schema({
ip: {type: String, required:true},
code: {type: String, required:true}
}, {versionKey: false})},
}, {versionKey: false}));
class MPServer extends Model {
declare public serverName: string;
declare public isActive: boolean;
declare public ip: string;
declare public code: string;
declare public playerData: number[];
}
export default class MPServer extends Schema {
client: TClient;
_content: typeof Schema;
constructor(client:TClient){
super();
this.client = client;
this._content = Schema;
}
async findInCache(query:any): Promise<any> {
const cacheKey = `MPServer:${query}`;
const cachedResult = await CacheServer.get(cacheKey);
let result;
if (cachedResult) {
try {
result = cachedResult;
} catch (error) {
console.error('Error parsing cached result:', error);
result = await this._content.findById(query);
CacheServer.set(cacheKey, result);
CacheServer.expiry(cacheKey, 1800);
export interface IServer {
serverName: string
isActive: boolean
ip: string
code: string
playerData: number[]
}
const cacheKey = 'MPServer';
export class MPServerSvc {
private model: typeof MPServer;
constructor() {
this.model = MPServer;
this.model.init({
serverName: {
type: DataTypes.STRING,
allowNull: false,
primaryKey: true,
},
isActive: {
type: DataTypes.BOOLEAN,
allowNull: false
},
ip: {
type: DataTypes.STRING,
allowNull: false,
},
code: {
type: DataTypes.STRING,
allowNull: false,
},
playerData: {
type: DataTypes.ARRAY(DataTypes.INTEGER),
allowNull: true,
}
}, {
tableName: 'mpserver',
createdAt: false,
updatedAt: false,
sequelize: DatabaseServer.seq,
})
this.model.sync();
}
async getServers() {
return await this.model.findAll();
}
async fetchPlayerData(serverName:string) {
const findServerByName = await this.model.findOne({where: {serverName: serverName}});
if (findServerByName) return findServerByName.dataValues.playerData;
else return [];
}
async addServer(serverName:string, ip:string, code:string) {
const findServerByName = await this.model.findOne({where: {serverName: serverName}});
if (findServerByName) {
(await findServerByName.update({serverName: serverName, ip: ip, code: code})).save();
await CacheServer.delete(cacheKey).then(async()=>await this.findInCache());
} else {
result = await this._content.findById(query);
CacheServer.set(cacheKey, result);
CacheServer.expiry(cacheKey, 1800);
await this.model.create({
serverName: serverName,
isActive: true,
ip: ip,
code: code,
playerData: []
});
await CacheServer.delete(cacheKey).then(async()=>await this.findInCache());
}
}
async removeServer(serverName:string) {
const findServerByName = await this.model.findOne({where: {serverName: serverName}});
if (findServerByName) {
await this.model.destroy({where: {serverName: serverName}});
await CacheServer.delete(cacheKey).then(async()=>await this.findInCache());
}
}
async toggleServerUsability(serverName:string, isActive:boolean) {
const findServerByName = await this.model.findOne({where: {serverName: serverName}});
if (findServerByName) {
this.model.update({isActive: isActive}, {where: {serverName: serverName}}).then(async flagUpdated=>{
if (flagUpdated) {
await CacheServer.delete(cacheKey).then(async()=>await this.findInCache());
return true;
}
});
} else return false;
}
async incrementPlayerCount(serverName:string, playerCount:number) {
const findServerByName = await this.model.findOne({where: {serverName: serverName}});
if (findServerByName) {
let PD = findServerByName.dataValues.playerData;
if (PD.length > 256) PD = [];
PD.push(playerCount);
const updatePD = await this.model.update({playerData: PD}, {where: {serverName: serverName}});
if (updatePD) true;
else return false;
} else return false;
}
async findInCache(): Promise<IServer[]> {
const cachedResult = await CacheServer.getJSON(cacheKey);
let result;
if (cachedResult) result = cachedResult;
else {
result = await this.model.findAll();
CacheServer.setJSON(cacheKey, result).then(()=>CacheServer.expiry(cacheKey, 1800));
}
return result;
}
async getServerData(serverName:string):Promise<IServer> {
return new Promise(async resolve=>{
const serverInfo = await this.findInCache();
for (let i = 0; i < serverInfo.length; i++) {
if (serverInfo[i].serverName === serverName) resolve(serverInfo[i]);
break;
}
})
}
}

View File

@ -1,37 +0,0 @@
import TClient from '../client.js';
import mongoose from 'mongoose';
import CacheServer from '../funcs/CacheServer.js';
const Schema = mongoose.model('bannedWords', new mongoose.Schema({
_id: {type: String, required:true}
}, {versionKey: false}));
export default class bannedWords extends Schema {
client: TClient;
_content: typeof Schema;
constructor(client:TClient){
super();
this.client = client;
this._content = Schema;
}
async findInCache(): Promise<any> {
const cacheKey = 'bannedWords';
const cachedResult = await CacheServer.get(cacheKey);
let result;
if (cachedResult) {
try {
result = cachedResult;
} catch (error) {
console.error('Error parsing cached result:', error);
result = await this._content.find();
CacheServer.set(cacheKey, result);
CacheServer.expiry(cacheKey, 180);
}
} else {
result = await this._content.find();
CacheServer.set(cacheKey, result);
CacheServer.expiry(cacheKey, 180);
}
return result;
}
}

View File

@ -1,23 +1,43 @@
import TClient from '../client.js';
import mongoose from 'mongoose';
import DatabaseServer from '../components/DatabaseServer.js';
import {Model, DataTypes} from 'sequelize';
const Schema = mongoose.model('bonkCount', new mongoose.Schema({
_id: {type: String, required:true},
value: {type: Number, required:true}
}, {versionKey: false}));
class bonkCount extends Model {
declare public id: string;
declare public count: number;
}
export default class bonkCount extends Schema {
client: TClient;
_content: typeof Schema;
constructor(client:TClient){
super();
this.client = client;
this._content = Schema;
export class BonkCountSvc {
private model: typeof bonkCount;
constructor(){
this.model = bonkCount;
this.model.init({
id: {
type: DataTypes.STRING,
unique: true,
primaryKey: true
},
count: {
type: DataTypes.INTEGER,
allowNull: false,
}
}, {
tableName: 'bonkcount',
createdAt: false,
updatedAt: false,
sequelize: DatabaseServer.seq
});
this.model.sync();
}
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})
async hitCountIncremental(userId:string) {
const getUser = await this.model.findByPk(userId);
if (getUser) getUser.increment('count');
else await this.model.create({id: userId, count: 1});
return this;
}
async fetchUser(userId:string) {
const getUser = await this.model.findByPk(userId);
if (getUser) return getUser.dataValues;
else return 0;
}
}

54
src/models/dailyMsgs.ts Normal file
View File

@ -0,0 +1,54 @@
import DatabaseServer from '../components/DatabaseServer.js';
import {Model, DataTypes} from 'sequelize';
class dailyMsgs extends Model {
declare public day: number;
declare public total: number;
}
export class DailyMsgsSvc {
private model: typeof dailyMsgs;
constructor() {
this.model = dailyMsgs;
this.model.init({
day: {
type: DataTypes.INTEGER,
autoIncrement: true,
unique: true,
primaryKey: true
},
total: {
type: DataTypes.INTEGER,
allowNull: false,
}
}, {
tableName: 'dailymsgs',
createdAt: false,
updatedAt: false,
sequelize: DatabaseServer.seq
})
this.model.sync();
}
async nukeDays() {
return await this.model.destroy({truncate: true})
// Drop a nuclear bomb on the table.
}
async fetchDays() {
return await this.model.findAll();
// Fetch every rows from database.
}
async fetchSpecificDay(dayId:number) {
return await this.model.findOne({where: {day: dayId}});
// Fetch a specific row from database by id column.
}
async newDay(formattedDate:number, total:number) {
if (await this.model.findOne({where: {day: formattedDate}})) return console.log('This day already exists!')
return await this.model.create({day: formattedDate, total: total});
// Save previous day's total messages into database when a new day starts.
}
async updateDay(formattedDate:number, total:number) {
return await this.model.update({total: total}, {where: {day: formattedDate}});
// THIS IS FOR DEVELOPMENT PURPOSES ONLY, NOT TO BE USED IN LIVE ENVIRONMENT!
}
}

View File

@ -0,0 +1,60 @@
import DatabaseServer from '../components/DatabaseServer.js';
import {Model, DataTypes} from 'sequelize';
import {get} from 'node:https';
class prohibitedWords extends Model {
declare public word: string;
}
export class ProhibitedWordsSvc {
private model: typeof prohibitedWords;
constructor() {
this.model = prohibitedWords;
this.model.init({
word: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
primaryKey: true
}
}, {
tableName: 'prohibitedwords',
createdAt: false,
updatedAt: false,
sequelize: DatabaseServer.seq
})
this.model.sync();
}
async findWord(word:string) {
return await this.model.findByPk(word);
}
async importWords(file:string) {
const jsonData = await new Promise<string>((resolve, reject)=>{
get(file, res=>{
let data = '';
res.on('data', chunk=>data += chunk);
res.on('end', ()=>resolve(data));
res.on('error', reject);
})
});
const data = JSON.parse(jsonData);
const dataMapping = data.map((x:string)=>({word: x}));
try {
await this.model.bulkCreate(dataMapping, {ignoreDuplicates: true});
return true;
} catch(err) {
throw new Error(`Failed to insert words into Postgres database: ${err.message}`)
}
}
async getAllWords() {
return await this.model.findAll();
}
async insertWord(word:string) {
return await this.model.create({word: word})
}
async removeWord(word:string) {
return await this.model.destroy({where: {word: word}})
}
}

View File

@ -1,171 +1,280 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import mongoose from 'mongoose';
import CacheServer from '../funcs/CacheServer.js';
import ms from 'ms';
import FormatTime from '../helpers/FormatTime.js';
import {Punishment} from '../typings/interfaces';
import {Punishment} from '../interfaces';
import DatabaseServer from '../components/DatabaseServer.js';
import {Model, DataTypes} from 'sequelize';
import CacheServer from '../components/CacheServer.js';
import MessageTool from '../helpers/MessageTool.js';
import Formatters from '../helpers/Formatters.js';
import {readFileSync, existsSync} from 'node:fs';
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}
}, {versionKey: false}));
class punishments extends Model {
declare public case_id: number;
declare public type: string;
declare public member: string;
declare public moderator: string;
declare public expired: boolean;
declare public time: number;
declare public reason: string;
declare public endTime: number;
declare public cancels: number;
declare public duration: number;
}
export default class punishments extends Schema {
client: TClient;
_content: typeof Schema;
constructor(client:TClient){
super();
export class PunishmentsSvc {
private client: TClient;
private model: typeof punishments;
constructor(client:TClient) {
this.client = client;
this._content = Schema;
}
async findInCache(): Promise<any> {
const cacheKey = 'punishments';
const cachedResult = await CacheServer.get(cacheKey);
let result;
if (cachedResult) {
try {
result = cachedResult;
} catch (error) {
console.error('Error parsing cached result:', error);
result = await this._content.find();
CacheServer.set(cacheKey, result);
CacheServer.expiry(cacheKey, 15);
this.model = punishments;
this.model.init({
case_id: {
type: DataTypes.INTEGER,
unique: true,
primaryKey: true
},
type: {
type: DataTypes.STRING,
allowNull: false
},
member: {
type: DataTypes.STRING,
allowNull: false
},
moderator: {
type: DataTypes.STRING,
allowNull: false
},
expired: {
type: DataTypes.BOOLEAN,
allowNull: true
},
time: {
type: DataTypes.BIGINT,
allowNull: false
},
reason: {
type: DataTypes.STRING,
allowNull: false
},
endTime: {
type: DataTypes.BIGINT,
allowNull: true
},
cancels: {
type: DataTypes.INTEGER,
allowNull: true
},
duration: {
type: DataTypes.BIGINT,
allowNull: true
}
} else {
result = await this._content.find();
CacheServer.set(cacheKey, result);
CacheServer.expiry(cacheKey, 15);
}, {
tableName: 'punishments',
createdAt: false,
updatedAt: false,
sequelize: DatabaseServer.seq
});
this.model.sync();
}
async migrate() {
let file:string = 'src/punishments.json';
if (!existsSync(file)) return Error(`File not found, have you tried checking if it exists? (${file})`);
await this.model.bulkCreate(JSON.parse(readFileSync(file, 'utf8')).map(x=>({
case_id: x._id,
type: x.type,
member: x.member,
moderator: x.moderator,
expired: x.expired,
time: x.time ? Number(x.time.$numberLong) : undefined,
reason: x.reason,
endTime: x.endTime ? Number(x.endTime.$numberLong) : undefined,
cancels: x.cancels,
duration: x.duration ? (typeof x.duration === 'object' ? Number(x.duration.$numberLong) : x.duration) : undefined
})));
}
async updateReason(caseId:number, reason:string) {
const findCase = this.findCase(caseId);
if (findCase) return this.model.update({reason: reason}, {where: {case_id: caseId}});
}
async findCase(caseId:number) {
return this.model.findOne({where: {case_id: caseId}});
}
async findByCancels(caseId:number) {
return this.model.findOne({where: {cancels: caseId}})
}
async getAllCases() {
return this.model.findAll();
}
async generateCaseId() {
const result = await this.model.findAll();
return Math.max(...result.map((x:Punishment)=>x.case_id), 0) + 1;
}
async caseEvasionCheck(member:Discord.GuildMember) {
if (await this.model.findOne({where: {member: member.id, type: 'mute', expired: undefined}})) {
(this.client.channels.cache.get(this.client.config.dcServer.channels.dcmod_chat) as Discord.TextChannel).send({embeds: [new this.client.embed().setColor(this.client.config.embedColorYellow).setTitle('Case evasion detected').setDescription(MessageTool.concatMessage(
`**${member.user.username}** (\`${member.user.id}\`) has been detected for case evasion.`,
'Timeout has been automatically added. (25 days)'
)).setTimestamp()]});
await this.punishmentAdd('mute', {time: '25d'}, this.client.user.id, '[AUTOMOD] Case evasion', member.user, member)
}
}
async findInCache():Promise<any> {
const cacheKey = 'punishments';
const cachedResult = await CacheServer.getJSON(cacheKey);
let result;
if (cachedResult) result = cachedResult;
else {
result = await this.model.findAll();
CacheServer.setJSON(cacheKey, result).then(()=>CacheServer.expiry(cacheKey, 20));
}
return result;
}
createId = async()=>Math.max(...(await this._content.find()).map(x=>x.id), 0) + 1;
async makeModlogEntry(punishment:Punishment){
// Format data into an embed
const channel = ['kick', 'ban'].includes(punishment.type) ? this.client.config.mainServer.channels.bankick_log : this.client.config.mainServer.channels.logs;
const embed = new this.client.embed().setTitle(`${punishment.type[0].toUpperCase() + punishment.type.slice(1)} | Case #${punishment._id}`)
async createModlog(punishment:Punishment) {
const channel = ['kick', 'ban', 'softban'].includes(punishment.type) ? this.client.config.dcServer.channels.bankick_log : this.client.config.dcServer.channels.logs;
const embed = new this.client.embed()
.setColor(this.client.config.embedColor)
.setTitle(`${punishment.type[0].toUpperCase() + punishment.type.slice(1)} | Case #${punishment.case_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: `${FormatTime(punishment.duration, 100)}`, inline: true}, {name: '\u200b', value: '\u200b', inline: true})
{name: '🔹 Reason', value: `\`${punishment.reason}\``, inline: true}
).setTimestamp(punishment.time);
if (punishment.duration) embed.addFields({name: '🔹 Duration', value: `${Formatters.timeFormat(punishment.duration, 4, {longNames: false, commas: true})}`, 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}\``})
const cancels = await this.model.findOne({where: {case_id: punishment.cancels}})
embed.addFields({name: '🔹 Overwrites', value: `This case invalidates Case #${cancels.dataValues.case_id}\n\`${cancels.dataValues.reason}\``});
}
// Send it off to specific Discord channel.
(this.client.channels.cache.get(channel) as Discord.TextChannel).send({embeds:[embed]});
(this.client.channels.cache.get(channel) as Discord.TextChannel).send({embeds: [embed]});
}
getTense(type:string){// Get past tense form of punishment type, grammar yes
getPastTense(type:string) {
return {
ban: 'banned',
softban: 'softbanned',
kick: 'kicked',
mute: 'muted',
warn: 'warned'
}[type]
}[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;
async punishmentAdd(type:string, options:{time?:string, interaction?:Discord.ChatInputCommandInteraction}, moderator:string, reason: string, user:Discord.User, guildUser?: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 guild = this.client.guilds.cache.get(this.client.config.dcServer.id) as Discord.Guild;
const punishment:Punishment = {type, case_id: await this.generateCaseId(), member: user.id, reason, moderator, time: now};
const inOrFromBoolean = ['warn', 'mute'].includes(type) ? 'in' : 'from';
const auditLogReason = `${reason || 'Reason unspecified'} | Case #${punData._id}`;
const auditLogReason = `${reason ?? 'Reason unspecified'} | Case #${punishment.case_id}`;
const embed = new this.client.embed()
.setColor(this.client.config.embedColor)
.setTitle(`Case #${punData._id}: ${type[0].toUpperCase()+type.slice(1)}`)
.setDescription(`${User.username}\n<@${User.id}>\n(\`${User.id}\`)`)
.addFields({name: 'Reason', value: reason})
let punResult;
let timeInMillis;
let DM;
.setTitle(`${type[0].toUpperCase() + type.slice(1)} | Case #${punishment.case_id}`)
.setDescription(`${user.username}\n<@${user.id}>\n\`${user.id}\``)
.addFields({name: 'Reason', value: `\`${reason}\``});
let punishmentResult:any;
let millisecondTime:number;
if (type == 'mute') timeInMillis = time ? ms(time) : 2419140000; // Timeouts have a limit of 4 weeks
else timeInMillis = time ? ms(time) : null;
if (type === 'mute') millisecondTime = time ? ms(time) : 2419200000; // Timeouts have a maximum duration of 4 weeks (28 days)
else millisecondTime = time ? ms(time) : null;
const durationText = timeInMillis ? ` for ${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.'})
const durText = millisecondTime ? ` for ${Formatters.timeFormat(millisecondTime, 4, {longNames: true, commas: true})}` : '';
if (time) embed.addFields({name: 'Duration', value: durText});
if (guildUser) {
try {
await guildUser.send(`You've been ${this.getPastTense(type)} ${inOrFromBoolean} **${guild.name}**${durText}\n\`${reason}\` (Case #${punishment.case_id})`)
} catch {
embed.setFooter({text: 'Unable 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 (['ban', 'softban'].includes(type)) {
const alreadyBanned = await guild.bans.fetch(user.id).catch(()=>null); // 172800 seconds is 48 hours, just for future reference
if (!alreadyBanned) punishmentResult = await guild.bans.create(user.id, {reason: auditLogReason, deleteMessageSeconds: 172800}).catch((err:Error)=>err.message);
else punishmentResult = 'This user already exists in the guild\'s ban list.';
} else if (type === 'kick') punishmentResult = await guildUser?.kick(auditLogReason).catch((err:Error)=>err.message);
else if (type === 'mute') punishmentResult = await guildUser?.timeout(millisecondTime, auditLogReason).catch((err:Error)=>err.message);
if (timeInMillis && ['mute','ban'].includes(type)){
punData.endTime = now + timeInMillis;
punData.duration = timeInMillis;
if (type === 'softban' && typeof punishmentResult !== 'string') punishmentResult = await guild.bans.remove(user.id, auditLogReason).catch((err:Error)=>err.message);
if (millisecondTime && ['ban', 'mute'].includes(type)) {
punishment.endTime = now + millisecondTime;
punishment.duration = millisecondTime;
}
if (typeof punResult == 'string'){// Unsuccessful punishment
if (DM) DM.delete();
if (interaction) return interaction.editReply(punResult);
else return punResult;
if (typeof punishmentResult === 'string') { // Punishment was unsuccessful
if (interaction) return interaction.editReply(punishmentResult);
else return punishmentResult;
} else {
await this.makeModlogEntry(punData);
await this._content.create(punData);
const checkIfExists = await this.model.findOne({where: {case_id: punishment.case_id}});
if (checkIfExists) this.model.update({expired: punishment.expired, time: punishment.time, endTime: punishment.endTime}, {where: {case_id: punishment.case_id}})
else await this.model.create({
case_id: punishment.case_id,
type: punishment.type,
member: punishment.member,
moderator: punishment.moderator,
expired: punishment.expired,
time: punishment.time,
reason: punishment.reason,
endTime: punishment.endTime,
cancels: punishment.cancels,
duration: punishment.duration
});
await this.createModlog(punishment);
if (interaction) return interaction.editReply({embeds:[embed]});
else return punResult;
if (interaction) return interaction.editReply({embeds: [embed]});
else return punishmentResult;
}
}
async removePunishment(caseId:number,moderator:string,reason:string,interaction?:Discord.ChatInputCommandInteraction<'cached'>){
async punishmentRemove(caseId:number, moderator:string, reason:string, interaction?:Discord.ChatInputCommandInteraction) {
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).catch(()=>null);
const ID = await this.generateCaseId();
const punishment = await this.model.findByPk(caseId);
if (!punishment) return 'Case not found in database.';
const guild = this.client.guilds.cache.get(this.client.config.dcServer.id) as Discord.Guild;
const auditLogReason = `${reason ?? 'Reason unspecified'} | Case #${ID}`;
const user = await this.client.users.fetch(punishment.member);
const guildUser = await guild.members.fetch(punishment.member).catch(()=>null);
let removePunishmentData:Punishment={type:`un${punishment.type}`, _id, cancels:punishment.id, member:punishment.member, reason, moderator, time:now};
let removePunishmentResult;
let removePunishmentData:Punishment = {type: `un${punishment.type}`, case_id: ID, cancels: punishment.case_id, member: punishment.member, reason, moderator, time: now};
let removePunishmentResult:any;
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});
if (punishment.type === 'ban') removePunishmentResult = await guild.bans.remove(punishment.member, auditLogReason).catch((err:Error)=>err.message);
else if (punishment.type === 'mute') {
if (guildUser) {
removePunishmentResult = await guildUser.timeout(null, auditLogReason).catch((err:Error)=>err.message);
guildUser.send(`You've been unmuted in **${guild.name}**.`).catch(()=>null);
} else this.model.update({expired: true}, {where: {case_id: caseId}});
} else removePunishmentData.type = 'punishmentOverride';
if (typeof removePunishmentResult == 'string'){//Unsuccessful punishment
if (interaction) return interaction.reply(removePunishmentResult);
if (typeof removePunishmentResult === 'string') {// Punishment was unsuccessful
if (interaction) return interaction.editReply(removePunishmentResult);
else return removePunishmentResult;
} else {
await this._content.findByIdAndUpdate(caseId,{expired:true},{new:true});
await this._content.create(removePunishmentData);
await this.makeModlogEntry(removePunishmentData);
this.model.update({expired: true}, {where: {case_id: caseId}}).then(()=>
this.model.create({
case_id: removePunishmentData.case_id,
type: removePunishmentData.type,
member: removePunishmentData.member,
moderator: removePunishmentData.moderator,
expired: removePunishmentData.expired,
time: removePunishmentData.time,
reason: removePunishmentData.reason,
endTime: removePunishmentData.endTime,
cancels: removePunishmentData.cancels,
duration: removePunishmentData.duration
})
);
await this.createModlog(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.username}\n<@${User.id}>\n(\`${User.id}\`)`)
.addFields({name: 'Reason', value: reason},{name: 'Overwrites', value: `Case #${punishment.id}`})
if (interaction) return interaction.reply({embeds: [new this.client.embed()
.setColor(this.client.config.embedColor)
.setTitle(`${removePunishmentData.type[0].toUpperCase() + removePunishmentData.type.slice(1)} | Case #${removePunishmentData.case_id}`)
.setDescription(`${user.username}\n<@${user.id}>\n\`${user.id}\``)
.addFields({name: 'Reason', value: reason}, {name: 'Overwrites', value: `Case #${punishment.case_id}`})
]});
else return `Successfully un${this.getTense(removePunishmentData.type.replace('un', ''))} ${User.username} (\`${User.id}\`) for ${reason}`
else return `Successfully un${this.getPastTense(removePunishmentData.type.replace('un', ''))} ${user.username} (\`${user.id}\`) for ${reason}`
}
}
}

View File

@ -1,22 +0,0 @@
import TClient from '../client.js';
import mongoose from 'mongoose';
const Schema = mongoose.model('suggestion', new mongoose.Schema({
_id: {type: String, required:true},
idea: {type: String, required:true},
user: {required:true, type: new mongoose.Schema({
name: {type: String},
_id: {type: String}
}, {versionKey: false})},
state: {type: String, required:true}
}, {versionKey: false}));
export default class suggestion extends Schema {
client: TClient;
_content: typeof Schema;
constructor(client:TClient){
super();
this.client = client;
this._content = Schema;
}
}

55
src/models/suggestions.ts Normal file
View File

@ -0,0 +1,55 @@
import DatabaseServer from '../components/DatabaseServer.js';
import {Model, DataTypes} from 'sequelize';
class suggestions extends Model {
declare public id: number;
declare public suggestion: string;
declare public userid: string;
declare public status: string;
}
export class SuggestionsSvc {
private model: typeof suggestions;
constructor() {
this.model = suggestions;
this.model.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
unique: true,
primaryKey: true
},
suggestion: {
type: DataTypes.TEXT,
allowNull: false
},
userid: {
type: DataTypes.STRING,
allowNull: false
},
status: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'suggestions',
createdAt: false,
updatedAt: false,
sequelize: DatabaseServer.seq
})
this.model.sync();
}
async fetchById(id:number) {
return await this.model.findByPk(id);
}
async updateStatus(id:number, status:string) {
return await this.model.update({status: status}, {where: {id: id}})
}
async create(userid:string, description:string) {
return this.model.create({userid: userid, suggestion: description, status: 'Pending'})
}
async delete(id:number) {
return this.model.destroy({where: {id: id}});
}
}

View File

@ -1,51 +1,100 @@
import TClient from '../client.js';
import mongoose from 'mongoose';
import CacheServer from '../funcs/CacheServer.js';
import MessageTool from '../helpers/MessageTool.js';
import CacheServer from '../components/CacheServer.js';
import DatabaseServer from '../components/DatabaseServer.js';
import {Model, DataTypes} from 'sequelize';
import {readFileSync, existsSync} from 'node:fs';
import {ChatInputCommandInteraction, Snowflake} from 'discord.js';
const Schema = mongoose.model('tags', new mongoose.Schema({
_id: {type: String, required:true},
message: {type: String, required:true},
embedBool: {type: Boolean, required:true},
user: {required:true, type: new mongoose.Schema({
name: {type: String, required:true},
_id: {type: String, required:true}
}, {versionKey: false})}
}, {versionKey: false}));
class tagsystem extends Model {
declare public tagname: string;
declare public message: string;
declare public embedFlag: boolean;
declare public userid: string;
}
export default class tags extends Schema {
client: TClient;
_content: typeof Schema;
constructor(client:TClient){
super();
this.client = client;
this._content = Schema;
}
async findInCache(): Promise<any> {
const cacheKey = 'Tags';
const cachedResult = await CacheServer.get(cacheKey);
let result;
if (cachedResult) {
try {
result = cachedResult;
} catch (error) {
console.error('Error parsing cached result:', error);
result = await this._content.find();
CacheServer.set(cacheKey, result);
CacheServer.expiry(cacheKey, 240);
interface Tags {
tagname: string;
message: string;
embedFlag: boolean;
userid: string;
}
export class TagSystemSvc {
private model: typeof tagsystem;
constructor() {
this.model = tagsystem;
this.model.init({
tagname: {
type: DataTypes.TEXT,
unique: true,
allowNull: false
},
message: {
type: DataTypes.TEXT,
allowNull: false
},
embedFlag: {
type: DataTypes.BOOLEAN,
allowNull: false
},
userid: {
type: DataTypes.STRING,
allowNull: false
}
} else {
result = await this._content.find();
CacheServer.set(cacheKey, result);
CacheServer.expiry(cacheKey, 240);
}, {
tableName: 'tags',
createdAt: false,
updatedAt: false,
sequelize: DatabaseServer.seq
});
this.model.sync();
}
async migrate() {
let file:string = 'src/tags.json';
if (!existsSync(file)) return Error(`File not found, have you tried checking if it exists? (${file})`);
await this.model.bulkCreate(JSON.parse(readFileSync(file, 'utf8')).map(x=>({
tagname: x._id,
message: x.message,
embedFlag: x.embedBool,
userid: x.user._id
})));
}
async createTag(userid:string, tagName:string, message:string, embedFlag:boolean) {
CacheServer.delete('tags');
return await this.model.create({userid: userid, tagname: tagName, message: message.replace(/\\n/g, '\n'), embedFlag: embedFlag});
}
async deleteTag(tagname:string) {
CacheServer.delete('tags');
return await this.model.destroy({where: {tagname: tagname}});
}
async sendTag(interaction:ChatInputCommandInteraction, tagName:string, targetId:Snowflake) {
const getTag = await this.model.findOne({where: {tagname: tagName}});
const targetMsg = targetId ? `*This tag is directed at ${MessageTool.formatMention(targetId, 'user')}*` : '';
const ic = interaction.client as TClient;
const embedFormat = [
new ic.embed().setTitle(tagName).setColor(ic.config.embedColor)
.setAuthor({name: interaction.user.username, iconURL: interaction.user.avatarURL({size: 2048, extension: 'webp'})})
.setDescription(getTag.dataValues.message)
];
if (getTag.dataValues.embedFlag) return await interaction.reply({content: targetMsg, embeds: embedFormat, allowedMentions: {parse: ['users']}});
else return await interaction.reply({content: targetMsg+`\n**${getTag.dataValues.message}**`, allowedMentions: {parse: ['users']}});
}
async modifyTag(tagname:string, message:string) {
CacheServer.delete('tags');
return await this.model.update({message: message.replace(/\\n/g, '\n')}, {where: {tagname: tagname}});
}
async findInCache(): Promise<Tags[]> {
const cacheKey = 'tags';
const cachedResult = await CacheServer.getJSON(cacheKey);
let result;
if (cachedResult) result = cachedResult;
else {
result = await this.model.findAll();
CacheServer.setJSON(cacheKey, result).then(()=>CacheServer.expiry(cacheKey, 240));
}
return result;
}
async updateCache(): Promise<any> {
const cacheKey = 'Tags';
CacheServer.delete(cacheKey);
const result = await this._content.find();
CacheServer.set(cacheKey, result);
CacheServer.expiry(cacheKey, 10);
return result;
}
}

View File

@ -1,65 +1,109 @@
import Discord from 'discord.js';
import TClient from '../client.js';
import mongoose from 'mongoose';
import MessageTool from '../helpers/MessageTool.js';
import DatabaseServer from '../components/DatabaseServer.js';
import {Model, DataTypes} from 'sequelize';
import {writeFileSync} from 'node:fs';
import cron from 'node-cron';
import {writeFileSync, readFileSync} from 'node:fs';
import Logger from '../helpers/Logger.js';
const Schema = mongoose.model('userLevels', new mongoose.Schema({
_id: {type: String},
messages: {type: Number, required: true},
level: {type: Number, required: true},
notificationPing: {type: Boolean}
}, {versionKey: false}));
class userLevels extends Model {
declare public id: string;
declare public messages: number;
declare public level: number;
declare public pingToggle: boolean;
}
export default class userLevels extends Schema {
client: TClient;
_content: typeof Schema;
constructor(client:TClient){
super();
export class UserLevelsSvc {
private client: TClient;
private model: typeof userLevels;
constructor(client:TClient) {
this.client = client;
this._content = Schema;
this.model = userLevels;
this.model.init({
id: {
type: DataTypes.STRING,
unique: true,
primaryKey: true
},
messages: {
type: DataTypes.INTEGER,
allowNull: false,
},
level: {
type: DataTypes.INTEGER,
allowNull: false,
},
pingToggle: {
type: DataTypes.BOOLEAN,
allowNull: true,
}
}, {
tableName: 'userlevels',
createdAt: false,
updatedAt: false,
sequelize: DatabaseServer.seq
});
this.model.sync();
}
async resetAllData(){
// Every 1st of January at 11:00 (Midnight in London, 11AM in Sydney)
async fetchEveryone() {
return await this.model.findAll();
}
async fetchUser(userId:string) {
return await this.model.findByPk(userId);
}
async deleteUser(userId:string) {
return await this.model.destroy({where: {id: userId}});
}
async messageIncremental(userId:string) {
const data = await this.model.findByPk(userId);
if (data) {
await this.model.update({messages: data.dataValues.messages + 1}, {where: {id: userId}});
if (data.messages >= this.algorithm(data.dataValues.level+2)) {
const oldLevel = data.dataValues.level;
while (data.messages > this.algorithm(data.dataValues.level+1)) await this.model.update({level: data.dataValues.level++}, {where: {id: userId}});
const updatedUser = await this.model.findByPk(userId);
Logger.console('log', 'LevelSystem', `${userId} superseded to level ${updatedUser.dataValues.level} from ${oldLevel}`);
} else if (data.dataValues.messages >= this.algorithm(data.dataValues.level+1)) {
await this.model.update({level: data.dataValues.level+1}, {where: {id: userId}});
const getUser = await this.model.findByPk(userId);
const levelUpMsg = `${getUser.pingToggle === true ? `<@${userId}>` : `**${(await this.client.users.fetch(userId)).displayName}**`} has reached level **${getUser.level}**. Well done!`;
(this.client.channels.resolve(this.client.config.dcServer.channels.botcommands) as Discord.TextChannel).send({content: levelUpMsg, allowedMentions: {parse: ['users']}});
}
} else await this.model.create({id: userId, messages: 1, level: 0, pingToggle: true});
}
async initSelfdestruct() {
// Every 1st of January at 11:00 (Midnight in London, Middayish in Sydney)
cron.schedule('0 11 1 1 *', async()=>{
Logger.forwardToConsole('log', 'Cron', 'Running job "resetAllData", this is activated every 1st of January');
const countDataBeforeReset = await this._content.countDocuments();
Logger.forwardToConsole('log', 'Cron:resetAllData', `Counted ${countDataBeforeReset.toLocaleString()} documents before reset`);
await this._content.deleteMany();
Logger.forwardToConsole('log', 'Cron:resetAllData', 'Deleted all documents, now resetting dailyMsgs');
const dailyMsgsBak = readFileSync('src/database/dailyMsgs.json', 'utf-8');
writeFileSync(`src/database/dailyMsgs_${new Date().getTime()}.json`, dailyMsgsBak);
writeFileSync('src/database/dailyMsgs.json', JSON.stringify([]));
// Send notification to mainServer's logs channel after cronjob is complete.
(this.client.channels.resolve(this.client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds: [new this.client.embed().setColor('#A3FFE3').setTitle('Yearly data reset has begun!').setDescription(`I have gone ahead and reset everyone's rank data. There was ${Intl.NumberFormat('en-US').format(await countDataBeforeReset)} documents in database before reset.`).setFooter({text: 'dailyMsgs has been backed up and wiped too.'}).setTimestamp()]});
Logger.console('log', 'Cron', 'Running job "resetAllData", this is activated every 1st of January');
const performCountBeforeReset = await this.model.count();
Logger.console('log', 'Cron:resetAllData', `Counted ${performCountBeforeReset.toLocaleString()} documents before reset`);
await this.client.dailyMsgs.nukeDays();
await this.model.drop().then(async()=>await this.model.sync());
// Reset LRSstart to current epoch and write it to config file
// Send notification to dcServer's logs channel after cronjob is complete.
(this.client.channels.resolve(this.client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [new this.client.embed()
.setColor('#A3FFE3').setTitle('Yearly data reset has begun!')
.setDescription(MessageTool.concatMessage(
'I have gone ahead and reset everyone\'s rank data.',
`There was ${Intl.NumberFormat('en-US').format(performCountBeforeReset)} documents in database before reset.`
)).setTimestamp()
]});
// Reset LRSstart to current Epoch and save it to config file
const newEpoch = new Date().getTime();
this.client.config.LRSstart = newEpoch;
const logText = `Resetting LRSstart to \`${newEpoch}\`, saved to config file`;
Logger.forwardToConsole('log', 'DailyMsgs', logText);
(this.client.channels.resolve(this.client.config.mainServer.channels.logs) as Discord.TextChannel).send({embeds: [new this.client.embed().setColor(this.client.config.embedColorXmas).setTitle('Happy New Years! Level System is clean!').setDescription(logText).setTimestamp()]}).catch(err=>console.log(err));
writeFileSync('./src/config.json', JSON.stringify(this.client.config, null, 2));
Logger.forwardToConsole('log', 'Cron:resetAllData', 'Job completed');
Logger.console('log', 'DailyMsgs', logText);
(this.client.channels.resolve(this.client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [new this.client.embed()
.setColor(this.client.config.embedColorXmas).setTitle('Happy New Years! Level System is clean!')
.setDescription(logText).setTimestamp()
]}).catch(err=>console.log(err));
writeFileSync('src/config.json', JSON.stringify(this.client.config, null, 2));
Logger.console('log', 'Cron:resetAllData', 'Job completed');
})
}
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++}, {new: true});
Logger.forwardToConsole('log', 'LevelSystem', `${userid} extended to level ${newData.level}`);
}
} else if (userData.messages >= this.algorithm(userData.level+1)) {
const newData = await this._content.findByIdAndUpdate(userid, {level:userData.level+1}, {new: true});
const fetchUserSchema = await this._content.findById(userid);
(this.client.channels.resolve(this.client.config.mainServer.channels.botcommands) as Discord.TextChannel).send({content: `${fetchUserSchema.notificationPing === true ? `<@${userid}>` : `**${(await this.client.users.fetch(userid)).displayName}**`} has reached level **${newData.level}**. GG!`, allowedMentions: {parse: ['users']}});
}
} else await this._content.create({_id: userid, notificationPing: true, 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.
algorithm = (level:number)=>level*level*18;
// Algorithm for determining levels. If adjusting, recommended to only change the integer at the end of equation.
}

48
src/models/ytChannels.ts Normal file
View File

@ -0,0 +1,48 @@
import DatabaseServer from '../components/DatabaseServer.js';
import {Model, DataTypes} from 'sequelize';
class youtubeChannels extends Model {
declare public ytchannel: string;
declare public dcchannel: string;
declare public dcrole: string;
}
export class YouTubeChannelsSvc {
private model: typeof youtubeChannels;
constructor() {
this.model = youtubeChannels;
this.model.init({
ytchannel: {
type: DataTypes.STRING,
allowNull: false,
primaryKey: true
},
dcchannel: {
type: DataTypes.STRING,
allowNull: false
},
dcrole: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'ytchannels',
createdAt: false,
updatedAt: false,
sequelize: DatabaseServer.seq
})
this.model.sync();
}
async getChannels() {
return await this.model.findAll();
}
async addChannel(YTChannelID:string, DCChannelID:string, DCRole:string) {
if (await this.model.findOne({where: {ytchannel: YTChannelID}})) return false;
await this.model.create({ytchannel: YTChannelID, dcchannel: DCChannelID, dcrole: DCRole});
return true;
}
async delChannel(YTChannelID:string) {
return await this.model.destroy({where: {ytchannel: YTChannelID}});
}
}

Some files were not shown because too many files have changed in this diff Show More