1
0
mirror of https://github.com/toast-ts/Daggerbot-TS.git synced 2024-12-26 11:45:37 -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 stuff
.yarn/cache/ .yarn
.yarn/unplugged/ # TypeScript stuff
.yarn/sdks/ dist
.yarn/install-state.gz
# NodeJS stuff
.ncurc.json
# Bot stuff
dist/
src/database/
src/*.json 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"> <p align="center">
<img width="650" height="240" src="https://cdn.discordapp.com/attachments/1118960531135541318/1151036641717260348/Daggerbot-TS-repo.gif"> <img width="630" height="250" src="https://cdn.toast-server.net/daggerwin/DaggerbotV3-Repo.gif">
<h1 align="center">Daggerbot-TS Description</h1> <h1 align="center">Daggerbot V3 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>
</p> </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: services:
cache: cache:
container_name: redis-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 restart: unless-stopped
ports: 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", "type": "module",
"repository": { "repository": {
"type": "git", "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", "author": "Toast",
"license": "ISC", "license": "ISC",
@ -21,30 +25,37 @@
"!ia32" "!ia32"
], ],
"engines": { "engines": {
"node": ">=18.17.0", "node": ">=18.18.0, <19 || >=20",
"yarn": ">=3.6.1", "yarn": ">=4.0.0",
"npm": "please use yarn instead of npm" "npm": "yarn is required, dont use npm"
}, },
"engineStrict": true, "engineStrict": true,
"packageManager": "yarn@3.6.3", "packageManager": "yarn@4.0.2+sha256.825003a0f561ad09a3b1ac4a3b3ea6207af2796d54f62a9420520915721f5186",
"dependencies": { "dependencies": {
"@octokit/auth-token": "4.0.0", "@octokit/auth-token": "4.0.0",
"@octokit/rest": "20.0.2", "@octokit/rest": "20.0.2",
"@toast/tokenservice-client": "1.0.5",
"ansi-colors": "4.1.3", "ansi-colors": "4.1.3",
"canvas": "2.11.2", "canvas": "2.11.2",
"dayjs": "1.11.10", "dayjs": "1.11.10",
"discord.js": "14.13.0", "discord.js": "14.14.1",
"fast-xml-parser": "4.3.2", "fast-xml-parser": "4.3.2",
"mongoose": "7.6.3",
"ms": "2.1.3", "ms": "2.1.3",
"node-cron": "3.0.2", "node-cron": "3.0.3",
"redis": "4.6.10", "pg": "8.11.3",
"systeminformation": "5.21.13" "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": { "devDependencies": {
"@types/ms": "0.7.33", "@types/ms": "0.7.34",
"@types/node": "20.8.8", "@types/node": "20.10.5",
"@types/node-cron": "3.0.10", "@types/node-cron": "3.0.11",
"typescript": "5.2.2" "@types/pg": "8.10.9",
"@types/sequelize": "4.28.19",
"typescript": "5.3.3"
} }
} }

View File

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

View File

@ -1,11 +1,11 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import Punish from '../funcs/Punish.js'; import Punish from '../components/Punish.js';
export default { export default class Ban {
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
Punish(client, interaction, 'ban'); Punish(client, interaction, 'ban');
}, }
data: new Discord.SlashCommandBuilder() static data = new Discord.SlashCommandBuilder()
.setName('ban') .setName('ban')
.setDescription('Ban a member from the server') .setDescription('Ban a member from the server')
.addUserOption(x=>x .addUserOption(x=>x
@ -18,4 +18,4 @@ export default {
.addStringOption(x=>x .addStringOption(x=>x
.setName('reason') .setName('reason')
.setDescription('Reason for the ban')) .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 Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
export default { export default class Bonk {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ 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.') //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 member = interaction.options.getMember('member') as Discord.GuildMember;
const reason = interaction.options.getString('reason'); const reason = interaction.options.getString('reason');
if (member.permissions.has('Administrator')) return interaction.reply('You cannot bonk an admin!'); 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) interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor)
.setDescription(`> <@${member.id}> has been bonked!\n${reason?.length == null ? '' : `> Reason: **${reason}**`}`) .setDescription(`> <@${member.id}> has been bonked!\n${reason?.length == null ? '' : `> Reason: **${reason}**`}`)
.setImage('https://media.tenor.com/7tRddlNUNNcAAAAd/hammer-on-head-minions.gif') .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') .setName('bonk')
.setDescription('Bonk a member') .setDescription('Bonk a member')
.addUserOption(x=>x .addUserOption(x=>x

View File

@ -1,10 +1,10 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js'; import MessageTool from '../helpers/MessageTool.js';
import FormatTime from '../helpers/FormatTime.js'; import Formatters from '../helpers/Formatters.js';
export default { export default class Calculator {
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
const now = Date.now(); const now = Date.now();
const exp = interaction.options.getString('expression', true).replace(/[^-()\d/*+.]/g, ''); const exp = interaction.options.getString('expression', true).replace(/[^-()\d/*+.]/g, '');
try { try {
@ -22,12 +22,12 @@ export default {
'-- Siri, 2015' '-- 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 { } catch {
interaction.reply('The given expression is invalid.'); interaction.reply('The given expression is invalid.');
} }
}, }
data: new Discord.SlashCommandBuilder() static data = new Discord.SlashCommandBuilder()
.setName('calculator') .setName('calculator')
.setDescription('Calculate a math expression or simple 2+2') .setDescription('Calculate a math expression or simple 2+2')
.addStringOption(x=>x .addStringOption(x=>x

View File

@ -1,42 +1,59 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import FormatTime from '../helpers/FormatTime.js'; import Formatters from '../helpers/Formatters.js';
import MessageTool from '../helpers/MessageTool.js'; import MessageTool from '../helpers/MessageTool.js';
export default { export default class Case {
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ 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'); if (!MessageTool.isStaff(interaction.member)) return MessageTool.youNeedRole(interaction, 'dcmod');
const caseId = interaction.options.getInteger('id'); const caseId = interaction.options.getInteger('id');
({ ({
update: async()=>{ update: async()=>{
const reason = interaction.options.getString('reason'); const reason = interaction.options.getString('reason');
await client.punishments._content.findByIdAndUpdate(caseId, {reason}); await client.punishments.updateReason(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}\``)]}); if (client.punishments.findCase(caseId)) {
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 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()=>{ view: async()=>{
const punishment = await client.punishments._content.findById(caseId); const punishment = await client.punishments.findCase(caseId);
if (!punishment) return interaction.reply('Invalid Case ID'); if (!punishment) return interaction.reply('Case ID is not found in database.');
const cancelledBy = punishment.expired ? await client.punishments._content.findOne({cancels:punishment.id}) : null; const cancelledBy = punishment.dataValues.expired ? await client.punishments.findByCancels(punishment.dataValues.case_id) : null;
const cancels = punishment.cancels ? await client.punishments._content.findOne({_id:punishment.cancels}) : null; const cancels = punishment.dataValues.cancels ? await client.punishments.findCase(punishment.dataValues.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( 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.member, 'user')} \`${punishment.member}\``, inline: true}, {name: 'User', value: `${MessageTool.formatMention(punishment.dataValues.member, 'user')} \`${punishment.dataValues.member}\``, inline: true},
{name: '🔹 Moderator', value: `${MessageTool.formatMention(punishment.moderator, 'user')} \`${punishment.moderator}\``, inline: true}, {name: 'Moderator', value: `${MessageTool.formatMention(punishment.dataValues.moderator, 'user')} \`${punishment.dataValues.moderator}\``, inline: true},
{name: '\u200b', value: '\u200b', inline: true}, {name: '\u200b', value: '\u200b', inline: true},
{name: '🔹 Reason', value: `\`${punishment.reason || 'Reason unspecified'}\``, 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.dataValues.duration) embed.addFields({name: 'Duration', value: `${Formatters.timeFormat(punishment.dataValues.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.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.cancels) embed.addFields({name: '🔹 Overwrites', value: `This case overwrites Case #${cancels.id} with reason \`${cancels.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]}); interaction.reply({embeds: [embed]});
}, },
member: async()=>{ member: async()=>{
const user = (interaction.options.getUser('user') as Discord.User); 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.`) 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 punishments = await client.punishments.getAllCases();
const userPunishmentData = await client.punishments._content.find({'member':user.id}); const userPunishmentData = punishments.filter(x=>x.dataValues.member === user.id);
const userPunishment = userPunishmentData.sort((a,b)=>a.time-b.time).map((punishment)=>{ const userPunishment = userPunishmentData.sort((a,b)=>a.dataValues.time-b.dataValues.time).map(punishment=>{
return { return {
name: `${punishment.type[0].toUpperCase()+punishment.type.slice(1)} | Case #${punishment.id}`, name: `${punishment.dataValues.type[0].toUpperCase()+punishment.dataValues.type.slice(1)} | Case #${punishment.dataValues.case_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}` : ''}` 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.`) 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))]}); 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()](); } as any)[interaction.options.getSubcommand()]();
}, }
data: new Discord.SlashCommandBuilder() static data = new Discord.SlashCommandBuilder()
.setName('case') .setName('case')
.setDescription('Retrieve case information or user\'s punishment history') .setDescription('Retrieve case information or user\'s punishment history')
.addSubcommand(x=>x .addSubcommand(x=>x
.setName('view') .setName('view')
.setDescription('View a multiple or single case') .setDescription('View information of the case ID')
.addIntegerOption(x=>x .addIntegerOption(x=>x
.setName('id') .setName('id')
.setDescription('Case ID') .setDescription('Case ID')
@ -76,4 +93,4 @@ export default {
.setName('reason') .setName('reason')
.setDescription('New reason for the case') .setDescription('New reason for the case')
.setRequired(true))) .setRequired(true)))
}; }

View File

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

View File

@ -1,28 +1,26 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import {Octokit} from '@octokit/rest';
import {createTokenAuth} from '@octokit/auth-token';
import {exec} from 'node:child_process'; import {exec} from 'node:child_process';
import MessageTool from '../helpers/MessageTool.js'; import MessageTool from '../helpers/MessageTool.js';
import UsernameHelper from '../helpers/UsernameHelper.js'; import UsernameHelper from '../helpers/UsernameHelper.js';
import FormatTime from '../helpers/FormatTime.js'; import Formatters from '../helpers/Formatters.js';
import TSClient from '../helpers/TSClient.js'; import GitHub from '../helpers/GitHub.js';
import TClient from '../client.js'; import TClient from '../client.js';
import fs from 'node:fs';
import util from 'node:util'; import util from 'node:util';
export default { import fs from 'node:fs';
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>) { 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'); if (!client.config.whitelist.includes(interaction.user.id)) return MessageTool.youNeedRole(interaction, 'bottech');
({ ({
eval: async()=>{ 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; const code = interaction.options.getString('code') as string;
let consoleOutput: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 deleteEmbedRow = new Discord.ActionRowBuilder<Discord.ButtonBuilder>().addComponents(deleteEmbedBtn);
const deleteEmbedCollector = interaction.channel.createMessageComponentCollector({componentType: Discord.ComponentType.Button}); const deleteEmbedCollector = interaction.channel.createMessageComponentCollector({componentType: Discord.ComponentType.Button});
deleteEmbedCollector.on('collect', async i=>{ deleteEmbedCollector.on('collect', async i=>{
if (i.customId === 'deleteEmbed') deleteEmbedCollector.stop(); if (i.customId === 'deleteEvalEmbed') deleteEmbedCollector.stop();
}); });
try { try {
@ -33,13 +31,13 @@ export default {
} }
const output = await eval(interaction.options.getBoolean('async') ? `(async()=>{${code}})()` : code); 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)); if (outVal && outVal.includes && outVal.includes(client.token)) outVal = outVal.replace(client.token, '*'.repeat(8));
const embedFields:Discord.APIEmbedField[] = [ const embedFields:Discord.APIEmbedField[] = [
{name: 'Input', value: `\`\`\`js\n${code.slice(0,1020)}\n\`\`\``}, {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') { if (typeof output === 'object') {
const embed = new client.embed().setColor(client.config.embedColor).addFields(embedFields); 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]})); 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}); const messagecollector = (interaction.channel as Discord.TextChannel).createMessageCollector({filter, max: 1, time: 60000});
messagecollector.on('collect', collected=>{ messagecollector.on('collect', collected=>{
console.log(err) console.log(err)
collected.reply(`\`\`\`\n${UsernameHelper.stripName(err.stack)}\n\`\`\``); collected.reply(`\`\`\`\n${UsernameHelper(err.stack)}\n\`\`\``);
}); });
}); });
} finally { } finally {
@ -65,32 +63,31 @@ export default {
} }
}, },
update: async()=>{ update: async()=>{
const SummonAuthentication = createTokenAuth((await TSClient.Token()).octokit); const hammondYouIdiot = await interaction.reply({content: 'Pulling...', fetchReply: true});
const {token} = await SummonAuthentication(); const repoData = await GitHub.RemoteRepository();
var githubRepo = {owner: 'AnxietyisReal', repo: 'Daggerbot-TS', ref: 'HEAD'}; const commitStats = {
const hammond = await interaction.reply({content: 'Pulling from repository...', fetchReply: true}); total: repoData.stats.total.toLocaleString('en-US'),
const octokit = new Octokit({auth: token, timeZone: 'Australia/NSW', userAgent: 'Daggerbot-TS'}); addition: repoData.stats.additions.toLocaleString('en-US'),
const github = { deletion: repoData.stats.deletions.toLocaleString('en-US')
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)
}
}; };
exec('git pull',{windowsHide:true},(err:Error,stdout)=>{ const msgBody = MessageTool.concatMessage(
if (err) hammond.edit(`\`\`\`${UsernameHelper.stripName(err.message)}\`\`\``) `[Commit pulled:](<${repoData.commit.url}>)`,
else if (stdout.includes('Already up to date')) hammond.edit('I am already up to date with the upstream repository.') `Message: **${repoData.commit.message.length === 0 ? '*No commit message*' : repoData.commit.message}**`,
else hammond.edit('Compiling TypeScript files...').then(()=>exec('yarn tsc', {windowsHide:true}, (err:Error)=>{ `Author: **${repoData.commit.author.name}**`,
if (err) hammond.edit(`\`\`\`${UsernameHelper.stripName(err.message)}\`\`\``) 'Changes',
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})); `${commitStats.addition.length > 1 ? `Additions:` : 'Addition:'} **${commitStats.addition}**`,
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!`) `${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: ()=>{ presence: ()=>{
function convertType(Type?: number){ function convertType(Type?: number){
@ -121,24 +118,11 @@ export default {
`URL: \`${currentActivities[0].url}\`` `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()=>{ restart: async()=>{
const i = await interaction.reply({content: 'Compiling TypeScript files...', fetchReply: true}); const int = await interaction.reply({content: 'Running `yarn tsc`...', fetchReply: true});
exec('yarn tsc',{windowsHide:true},(err:Error)=>{ exec('yarn tsc', {windowsHide:true}, (err:Error)=>{
if (err) i.edit(`\`\`\`${UsernameHelper.stripName(err.message)}\`\`\``) if (err) int.edit(`\`\`\`${UsernameHelper(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})) else int.edit(`Restarting...\nUptime: **${Formatters.timeFormat(process.uptime()*1000, 4, {longNames:true, commas:true})}**`).then(()=>process.exit(0));
})
},
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.||')
}) })
}, },
dm: async()=>{ 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}\``)) 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()](); } as any)[interaction.options.getSubcommand()]();
}, }
data: new Discord.SlashCommandBuilder() static data = new Discord.SlashCommandBuilder()
.setName('dev') .setName('dev')
.setDescription('Developer commands') .setDescription('Developer commands')
.addSubcommand(x=>x .addSubcommand(x=>x
@ -163,9 +147,6 @@ export default {
.setName('async') .setName('async')
.setDescription('Asynchronously execute your code') .setDescription('Asynchronously execute your code')
.setRequired(false))) .setRequired(false)))
.addSubcommand(x=>x
.setName('logs')
.setDescription('Retrieve the logs from host and sends it to dev server'))
.addSubcommand(x=>x .addSubcommand(x=>x
.setName('restart') .setName('restart')
.setDescription('Restart the bot for technical reasons')) .setDescription('Restart the bot for technical reasons'))
@ -177,24 +158,6 @@ export default {
.setDescription('Restart the bot after pulling from repository') .setDescription('Restart the bot after pulling from repository')
.setRequired(true) .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 .addSubcommand(x=>x
.setName('presence') .setName('presence')
.setDescription('Update the bot\'s presence') .setDescription('Update the bot\'s presence')
@ -224,13 +187,6 @@ export default {
{name: 'Do Not Distrub', value: Discord.PresenceUpdateStatus.DoNotDisturb}, {name: 'Do Not Distrub', value: Discord.PresenceUpdateStatus.DoNotDisturb},
{name: 'Invisible', value: Discord.PresenceUpdateStatus.Offline} {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 .addSubcommand(x=>x
.setName('dm') .setName('dm')
.setDescription('Reply or send a DM to a member') .setDescription('Reply or send a DM to a member')

View File

@ -1,45 +1,28 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js'; import FAQHelper from '../helpers/FAQHelper.js';
import FAQStore from '../helpers/FAQStore.js'; export default class FAQ {
export default { static run(_client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
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.`;
({ ({
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), 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(youCanGetRole('vtcmember', 'truck')+'\n*VTC skin can also be found in <#801975222609641472> as well.*'), vtcR: ()=>interaction.reply(FAQHelper.youCanGetRole('vtcmember', 'truck')+'\n*VTC skin can also be found in <#801975222609641472> as well.*'),
mpR: ()=>interaction.reply(youCanGetRole('mpplayer', 'tractor')), mpR: ()=>interaction.reply(FAQHelper.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), 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: ()=>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), 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: ()=>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), 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: ()=>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), 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: ()=>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), 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: ()=>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) 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)](); } as any)[interaction.options.getString('question', true)]();
}, }
data: new Discord.SlashCommandBuilder() static data = new Discord.SlashCommandBuilder()
.setName('faq') .setName('faq')
.setDescription('List of questions, e.g; log file for FS, YT Scams and etc.') .setDescription('List of questions, e.g; log file for FS, YT Scams and etc.')
.addStringOption(x=>x .addStringOption(x=>x
.setName('question') .setName('question')
.setDescription('What question do you want answered?') .setDescription('What question do you want answered?')
.setRequired(true) .setRequired(true)
.addChoices( .setChoices(
{ name: 'Survival Roleplay Map', value: 'srp'}, { name: 'Survival Roleplay Map', value: 'srp'},
{ name: 'Scams in YT comments', value: 'ytscam' }, { name: 'Scams in YT comments', value: 'ytscam' },
{ name: 'Steam account report scam', value: 'steamscam' }, { name: 'Steam account report scam', value: 'steamscam' },

View File

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

View File

@ -1,13 +1,13 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import Punish from '../funcs/Punish.js'; import Punish from '../components/Punish.js';
export default { export default class Kick {
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
Punish(client, interaction, 'kick'); Punish(client, interaction, 'kick');
}, }
data: new Discord.SlashCommandBuilder() static data = new Discord.SlashCommandBuilder()
.setName('kick') .setName('kick')
.setDescription('Boot a member from the server') .setDescription('Kick a member from the server')
.addUserOption(x=>x .addUserOption(x=>x
.setName('member') .setName('member')
.setDescription('Which member to kick?') .setDescription('Which member to kick?')
@ -15,4 +15,4 @@ export default {
.addStringOption(x=>x .addStringOption(x=>x
.setName('reason') .setName('reason')
.setDescription('Reason for the kick')) .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 Discord from 'discord.js';
import TClient from '../client.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 Logger from '../helpers/Logger.js';
import {readFileSync} from 'node:fs'; import CanvasBuilder from '../components/CanvasGraph.js';
import {FSData} from '../typings/interfaces'; 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 = [ async function fetchData(client:TClient, interaction:Discord.ChatInputCommandInteraction, serverName:string):Promise<FSData|Discord.InteractionResponse> {
{name: 'Main Server', value: 'mainServer'}, const db = await client.MPServer.findInCache();
{name: 'Second Server', value: 'secondServer'} 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 { const logPrefix = 'MPDB';
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ const channels = {
if (client.uptime < 35000) return interaction.reply('I have just restarted, please wait for MPLoop to finish initializing.'); activePlayers: '739084625862852715',
const serverSelector = interaction.options.getString('server'); announcements: '1084864116776251463',
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)); mainMpChat: '468835769092669461',
mfMpChat: '1149238561934151690',
const database = await client.MPServer.findInCache(interaction.guildId); serverInfo: '543494084363288637',
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(); 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()=>{ players: async()=>{
const data = JSON.parse(readFileSync(path.join(`src/database/${client.MPServerCache[serverSelector].name}PlayerData.json`), {encoding: 'utf8'})).slice(client.statsGraph); const DSS = await fetchData(client, interaction, choiceSelector) as FSData;
// handle negative days if (!DSS) return console.log('Endpoint failed - players');
for (const [i, change] of data.entries()) if (change < 0) data[i] = data[i - 1] || data[i + 1] || 0;
const first_graph_top = 16; const PDArr = await client.MPServer.fetchPlayerData(choiceSelector);
const second_graph_top = 16; const canvas = await new CanvasBuilder().generateGraph(PDArr.slice(client.statsGraph), 'players');
const textSize = 40; const players:string[] = [];
const img = canvas.createCanvas(1500, 750); let embedColor:Discord.ColorResolvable;
const ctx = img.getContext('2d'); switch (true){
const graphOrigin = [15, 65]; case DSS?.slots?.used === DSS?.slots.capacity:
const graphSize = [1300, 630]; embedColor = client.config.embedColorRed;
const nodeWidth = graphSize[0] / (data.length - 1); break;
ctx.fillStyle = '#36393f'; case DSS?.slots?.used > 8:
ctx.fillRect(0, 0, img.width, img.height); embedColor = client.config.embedColorYellow;
break;
// grey horizontal lines default:
ctx.lineWidth = 5; embedColor = client.config.embedColorGreen;
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 chosen_interval = interval_candidates.sort((a, b) => b[2] - a[2])[0]; for (const player of DSS.slots.players.filter(x=>x.isUsed)) players.push(playtimeStat(player))
const previousY: number[] = [];
ctx.strokeStyle = '#202225';
for (let i = 0; i <= chosen_interval[1]; i++) { let attachmentName:string = 'MPModule.jpg';
const y = graphOrigin[1] + graphSize[1] - (i * (chosen_interval[0] / second_graph_top) * graphSize[1]); await interaction.reply({embeds:[new client.embed()
if (y < graphOrigin[1]) continue; .setTitle(DSS.server?.name.length > 0 ? DSS.server.name : 'Offline')
const even = ((i + 1) % 2) === 0; .setColor(embedColor)
if (even) ctx.strokeStyle = '#2c2f33'; .setDescription(DSS?.slots?.used < 1 ? '*Nobody is playing*' : players.join('\n\n'))
ctx.beginPath(); .setImage('attachment://'+attachmentName)
ctx.lineTo(graphOrigin[0], y); .setAuthor({name: `${DSS.slots.used}/${DSS.slots.capacity}`})
ctx.lineTo(graphOrigin[0] + graphSize[0], y); .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)}`})
ctx.stroke(); ], files: [new client.attachment(canvas.toBuffer(), {name: attachmentName})]})
ctx.closePath(); },
if (even) ctx.strokeStyle = '#202225'; details: async()=>{
previousY.push(y, i * chosen_interval[0]); 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 const dEmbed = new client.embed().setColor(client.config.embedColor).setAuthor({name: 'Crossplay server'}).setDescription(MessageTool.concatMessage(
ctx.setLineDash([8, 16]); `**Name:** \`${DSS?.server.name.length > 0 ? DSS.server.name : '\u200b'}\``,
ctx.beginPath(); `**Password:** \`mf4700\``,
const lastMonthStart = graphOrigin[0] + (nodeWidth * (data.length - 60)); `**Map:** \`${DSS.server.mapName.length > 0 ? DSS.server.mapName : 'No map'}\``,
ctx.lineTo(lastMonthStart, graphOrigin[1]); `**Mods:** [Click here](http://${server.ip}/mods.html) **|** [Direct link](http://${server.ip}/all_mods_download?onlyActive=true)`,
ctx.lineTo(lastMonthStart, graphOrigin[1] + graphSize[1]); '**Filters:** [Click here](https://discord.com/channels/468835415093411861/468835769092669461/926581585938120724)',
ctx.stroke(); `Please see <#${channels.serverInfo}> for more additional information and rules.`
ctx.closePath(); ));
ctx.setLineDash([]); if (DSS.server.name.length < 1) dEmbed.setFooter({text: 'Server is currently offline'});
await interaction.reply({embeds: [dEmbed]})
// 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'})]})
}, },
status: async()=>{ status: async()=>{
if (!endpoint) return console.log('Endpoint failed - status'); const DSS = await fetchData(client, interaction, choiceSelector) as FSData;
try { if (!DSS) return console.log('Endpoint failed - status');
if (endpoint.server.name.length > 1){ if (DSS.server.name.length > 0) {
interaction.reply({embeds: [embed.setTitle('Status/Details').setColor(client.config.embedColor).addFields( await interaction.reply({embeds: [new client.embed().setColor(client.config.embedColor).addFields(
{name: 'Server name', value: `${endpoint?.server.name.length === 0 ? '\u200b' : `\`${endpoint?.server.name}\``}`, inline: true}, {name: 'Name', value: `\`${DSS?.server.name}\``, inline: true},
{name: 'Players', value: `${endpoint.slots.used} out of ${endpoint.slots.capacity}`, inline: true}, {name: 'Players', value: `${DSS.slots.used}/${DSS.slots.capacity}`, inline: true},
{name: 'Current map', value: `${endpoint?.server.mapName.length === 0 ? '\u200b' : endpoint.server.mapName}`, inline: true}, {name: 'Map', value: DSS?.server.mapName, inline: true}
{name: 'Version', value: `${endpoint?.server.version.length === 0 ? '\u200b' : endpoint.server.version}`, 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)}`}`})]})
{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 return interaction.reply('Server is currently offline.')
)]})
} 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}\`\`\``))
}
}
}, },
pallets: async()=>{ pallets: async()=>{
if (!endpoint) return console.log('Endpoint failed - pallets'); const DSS = await fetchData(client, interaction, choiceSelector) as FSData;
const filter = endpoint.vehicles.filter(v=>v.type === 'pallet'); if (!DSS) return console.log('Endpoint failed - pallets');
if (filter.length < 1) return interaction.reply('There are no pallets on the server.'); const filter = DSS?.vehicles.filter(x=>x.category === 'PALLETS');
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')}\`\`\``) 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()](); })[interaction.options.getSubcommand() ?? interaction.options.getSubcommandGroup()]();
}, }
data: new Discord.SlashCommandBuilder() private static async reactionSystem(message:Discord.Message, length:number) {
.setName('mp') const numbersArr = ['1⃣','2⃣','3⃣','4⃣','5⃣','6⃣','7⃣','8⃣','9⃣','🔟'];
.setDescription('Display MP status and other things') await Promise.all(numbersArr.slice(0, length).map(emote=>message.react(emote)));
.addSubcommand(x=>x }
.setName('status') static data = new Discord.SlashCommandBuilder()
.setDescription('Display server status') .setName('mp')
.addStringOption(x=>x .setDescription('Get information from the FSMP server(s)')
.setName('server') .addSubcommand(x=>x
.setDescription('The server to update') .setName('players')
.setRequired(true) .setDescription('Fetches the player list from the requested server')
.setChoices(...serverChoices))) .addStringOption(x=>x
.addSubcommand(x=>x .setName('server')
.setName('players') .setDescription('The server to fetch the player list from')
.setDescription('Display players on server') .setAutocomplete(true)
.addStringOption(x=>x .setRequired(true)))
.setName('server') .addSubcommand(x=>x
.setDescription('The server to display players for') .setName('details')
.setRequired(true) .setDescription('Fetches the information about the requested server')
.setChoices(...serverChoices))) .addStringOption(x=>x
.addSubcommand(x=>x .setName('server')
.setName('url') .setDescription('The server to fetch the information from')
.setDescription('View or update the server URL') .setAutocomplete(true)
.addStringOption(x=>x .setRequired(true)))
.setName('server') .addSubcommand(x=>x
.setDescription('The server to update') .setName('status')
.setRequired(true) .setDescription('Display the status of the requested server')
.setChoices(...serverChoices)) .addStringOption(x=>x
.addStringOption(x=>x .setName('server')
.setName('address') .setDescription('The server to fetch the status from')
.setDescription('The URL to the dedicated-server-stats.json file') .setAutocomplete(true)
.setRequired(false))) .setRequired(true)))
.addSubcommand(x=>x .addSubcommand(x=>x
.setName('info') .setName('pallets')
.setDescription('Display server information') .setDescription('Fetches how many pallets are on the requested server')
.addStringOption(x=>x .addStringOption(x=>x
.setName('server') .setName('server')
.setDescription('The server to display information for') .setDescription('The server to fetch the pallet count from')
.setRequired(true) .setAutocomplete(true)
.setChoices(...serverChoices))) .setRequired(true)))
.addSubcommand(x=>x .addSubcommandGroup(x=>x
.setName('pallets') .setName('server_mgmnt')
.setDescription('Check total amount of pallets on the server') .setDescription('Manage the server entries in database, e.g toggling server visiblity, adding/removing, etc.')
.addStringOption(x=>x .addSubcommand(x=>x
.setName('server') .setName('create_server')
.setDescription('The server to get amount of pallets from') .setDescription('View or update the URL for the requested server')
.setRequired(true) .addStringOption(x=>x
.setChoices(...serverChoices))) .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 Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import Punish from '../funcs/Punish.js'; import Punish from '../components/Punish.js';
export default { export default class Mute {
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
Punish(client, interaction, 'mute'); Punish(client, interaction, 'mute');
}, }
data: new Discord.SlashCommandBuilder() static data = new Discord.SlashCommandBuilder()
.setName('mute') .setName('mute')
.setDescription('Mute a member') .setDescription('Mute a member')
.addUserOption(x=>x .addUserOption(x=>x
@ -18,4 +18,4 @@ export default {
.addStringOption(x=>x .addStringOption(x=>x
.setName('reason') .setName('reason')
.setDescription('Reason for the mute')) .setDescription('Reason for the mute'))
} }

View File

@ -1,13 +1,20 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import FormatTime from '../helpers/FormatTime.js'; import {fetch} from 'undici';
export default { import Formatters from '../helpers/Formatters.js';
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ export default class Ping {
if (client.uptime < 15500) return interaction.reply('I just restarted, wait 15 seconds and try again.') static async run(client:TClient, interaction:Discord.ChatInputCommandInteraction<'cached'>) {
const msg = await interaction.reply({content: 'Pinging...', fetchReply: true}) const expectedUptime:number = 16300;
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})}\``) 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};
data: new Discord.SlashCommandBuilder() 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') .setName('ping')
.setDescription('Check latency between bot and Discord API') .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 Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js'; import MessageTool from '../helpers/MessageTool.js';
export default { export default class Purge {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ static 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');
if (!MessageTool.isStaff(interaction.member)) return MessageTool.youNeedRole(interaction, 'dcmod');
}
const amount = interaction.options.getInteger('amount') as number; 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}) if (amount > 100) return interaction.reply({content: 'Discord API limits purging up to 100 messages.', ephemeral: true})
const user = interaction.options.getUser('user'); const user = interaction.options.getUser('user');
@ -23,8 +21,8 @@ export default {
}) })
} }
await interaction.reply({content: `Successfully purged ${amount} messages.`, ephemeral: true}) await interaction.reply({content: `Successfully purged ${amount} messages.`, ephemeral: true})
}, }
data: new Discord.SlashCommandBuilder() static data = new Discord.SlashCommandBuilder()
.setName('purge') .setName('purge')
.setDescription('Purge the amount of messages in this channel') .setDescription('Purge the amount of messages in this channel')
.addIntegerOption(x=>x .addIntegerOption(x=>x

View File

@ -1,20 +1,20 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js'; import MessageTool from '../helpers/MessageTool.js';
import path from 'node:path'; import CanvasBuilder from '../components/CanvasGraph.js';
import {readFileSync} from 'node:fs'; export default class Rank {
import canvas from 'canvas'; static async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
export default { if (interaction.guildId !== client.config.dcServer.id) return interaction.reply({content: 'This command doesn\'t work in this server.', ephemeral: true});
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ // const allData = await client.userLevels._content.find();
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.fetchEveryone();
const allData = await client.userLevels._content.find({});
({ ({
view: async()=>{ view: async()=>{
// fetch user or user interaction sender // fetch user or user interaction sender
const member = interaction.options.getMember('member') ?? interaction.member as Discord.GuildMember; 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.'); 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 // 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 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; 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.`) 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 index = allData.sort((a, b) => b.messages - a.messages).map(x=>x.dataValues.id).indexOf(member.id) + 1;
const memberDifference = userData.messages - client.userLevels.algorithm(userData.level); const memberDifference = userData.dataValues.messages - client.userLevels.algorithm(userData.dataValues.level);
const levelDifference = client.userLevels.algorithm(userData.level+1) - client.userLevels.algorithm(userData.level); const levelDifference = client.userLevels.algorithm(userData.dataValues.level+1) - client.userLevels.algorithm(userData.dataValues.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'})]}) 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: ()=>{ leaderboard: async()=>{
const data = JSON.parse(readFileSync(path.join('./src/database/dailyMsgs.json'), 'utf8')).map((x: Array<number>, i: number, a: any) => { 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]) 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 const graph = await new CanvasBuilder().generateGraph(data, 'leaderboard');
data.forEach((change: number, i: number) => { interaction.reply({
if (change < 0) data[i] = data[i - 1] || data[i + 1] || 0; 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.`,
const maxValue = Math.max(...data); `Since then, a total of **${allData.reduce((a, b)=>a+b.messages, 0).toLocaleString('en-US')}** messages have been sent in this server.`
const maxValueArr = maxValue.toString().split(''); )).addFields({
name: 'Top users sorted by messages sent:',
const first_graph_top = Math.ceil(maxValue * 10 ** (-maxValueArr.length + 1)) * 10 ** (maxValueArr.length - 1); 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')
const second_graph_top = Math.ceil(maxValue * 10 ** (-maxValueArr.length + 2)) * 10 ** (maxValueArr.length - 2); }).setImage('attachment://dailyMessages.jpg').setFooter({text: 'Graph updates daily'})],
const textSize = 32; files: [new client.attachment(graph.toBuffer(),{name: 'dailyMessages.jpg'})]
})
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'})]})
}, },
notification: async()=>{ 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.' const textDeco = ' be pinged for level-up notification in the future.'
if (!findUserInMongo.notificationPing) { if (!findUserInDatabase.pingToggle) {
await findUserInMongo.updateOne({_id: interaction.user.id, notificationPing: true}) await findUserInDatabase.update({pingToggle: true}, {where: {id: interaction.user.id}})
interaction.reply({content: 'You will'+textDeco, ephemeral: true}) interaction.reply({content: 'You will'+textDeco, ephemeral: true})
} else if (findUserInMongo.notificationPing) { } else if (findUserInDatabase.pingToggle) {
await findUserInMongo.updateOne({_id: interaction.user.id, notificationPing: false}) await findUserInDatabase.update({pingToggle: false}, {where: {id: interaction.user.id}})
interaction.reply({content: 'You won\'t'+textDeco, ephemeral: true}) interaction.reply({content: 'You won\'t'+textDeco, ephemeral: true})
} }
} }
} as any)[interaction.options.getSubcommand()](); } as any)[interaction.options.getSubcommand()]();
}, }
data: new Discord.SlashCommandBuilder() static data = new Discord.SlashCommandBuilder()
.setName('rank') .setName('rank')
.setDescription('Level system') .setDescription('Level system')
.addSubcommand(x=>x .addSubcommand(x=>x

View File

@ -1,7 +1,7 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
export default { export default class RoleInfo {
run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ static run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
const role = interaction.options.getRole('role') as Discord.Role; const role = interaction.options.getRole('role') as Discord.Role;
const permissions = role.permissions.toArray(); const permissions = role.permissions.toArray();
interaction.reply({embeds: [new client.embed().setColor(role.color || '#fefefe').setThumbnail(role?.iconURL()).setTitle(`Role Info: ${role.name}`).addFields( 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: '🔹 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: '🔹 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} {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') .setName('roleinfo')
.setDescription('View information about the selected role') .setDescription('View information about the selected role')
.addRoleOption(x=>x .addRoleOption(x=>x
.setName('role') .setName('role')
.setDescription('Role name to view information') .setDescription('Role name to view information')
.setRequired(true)) .setRequired(true))
} }

View File

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

View File

@ -1,94 +1,83 @@
interface CommitHashes {
localHash: string,
remoteHash: string
}
import Discord from 'discord.js'; 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 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 os from 'node:os';
import {Octokit} from '@octokit/rest'; import ts from 'typescript';
import {createTokenAuth} from '@octokit/auth-token'; import {readFileSync} from 'fs';
import {readFileSync} from 'node:fs'; export default class Statistics {
import {Worker} from 'node:worker_threads'; static async run(client:TClient, interaction:Discord.ChatInputCommandInteraction<'cached'>) {
const packageJson = JSON.parse(readFileSync('package.json', 'utf8')); const initialMsg = await interaction.reply({content: '<a:sakjdfsajkfhsdjhjfsa:1065342869428252743>', fetchReply:true});
const repoData = await GitHub.LocalRepository();
const workerThread = new Worker(new URL('../helpers/CommitHashes.js', import.meta.url)); const embed = new client.embed().setColor(client.config.embedColor).setTitle('Statistics').setDescription(MessageTool.concatMessage(
const hashData = await new Promise<CommitHashes>(resolve=>workerThread.on('message', (data:CommitHashes)=>resolve(data))); '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.'
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)}`
)); ));
if (rows.join('').length > 1024){ const systemInfo = {
let fieldValue = ''; cpu: await si.cpu(),
rows.forEach(row=>{ mem: await si.mem(),
if (fieldValue.length + row.length > 1024){ osInfo: await si.osInfo(),
embed.addFields({name: '\u200b', value: `\`\`\`\n${fieldValue}\`\`\``}); currLoad: await si.currentLoad()
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 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( embed.addFields(
{ {name: '🔹 *Dependencies*', value: MessageTool.concatMessage(
name: '> __Repository__', value: MessageTool.concatMessage( `>>> **Yarn:** ${pkg.packageManager.split('@')[1].split('+')[0]}`,
`**Local:** [${hashData.localHash}](${github.localCommit.data.html_url})`, `**Node.js:** ${process.version.slice(1)}`,
`**Remote:** [${hashData.remoteHash}](${github.remoteCommit.data.html_url})` `**Discord.js:** ${pkg.dependencies['discord.js']}`,
) `**TypeScript:** ${ts.version}`,
}, `**Postgres:** ${pkg.dependencies.pg}`,
{name: '> __Dependencies__', value: MessageTool.concatMessage( `**Redis:** ${pkg.dependencies.redis}`
`**TypeScript:** ${pkg.version}`,
`**NodeJS:** ${process.version}`,
`**DiscordJS:** ${Discord.version}`,
`**Yarn:** ${packageJson.packageManager.slice(5)}`
)}, )},
{name: '> __Host__', value: MessageTool.concatMessage( {name: '🔹 *Host*', value: MessageTool.concatMessage(
`**Operating System:** ${osInfo.distro + ' ' + osInfo.release}`, `>>> **OS:** ${systemInfo.osInfo.distro} ${systemInfo.osInfo.release}`,
`**CPU:** ${cpu.manufacturer} ${cpu.brand}`, `**CPU:** ${systemInfo.cpu.manufacturer} ${systemInfo.cpu.brand}${systemInfo.cpu.speed} GHz`,
`**Memory:** ${FormatBytes(ram.used)}/${FormatBytes(ram.total)}`, '**RAM**',
`**Process:** ${FormatBytes(process.memoryUsage().heapUsed)}/${FormatBytes(process.memoryUsage().heapTotal)}`, `╰ **Host:** ${this.progressBar(systemInfo.mem.used, systemInfo.mem.total)} (${Formatters.byteFormat(systemInfo.mem.used)}/${Formatters.byteFormat(systemInfo.mem.total)})`,
`**Load Usage:**\nUser: ${currentLoad.currentLoadUser.toFixed(1)}%\nSystem: ${currentLoad.currentLoadSystem.toFixed(1)}%`, `╰ **Bot:** ${this.progressBar(process.memoryUsage().heapUsed, process.memoryUsage().heapTotal)} (${Formatters.byteFormat(process.memoryUsage().heapUsed)}/${Formatters.byteFormat(process.memoryUsage().heapTotal)})`,
`**Uptime:**\nHost: ${FormatTime((os.uptime()*1000), 2, {longNames: true, commas: true})}\nBot: ${FormatTime(client.uptime, 2, {commas: true, longNames: true})}` '**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)}%)`
)} )}
); ).setFooter({text: `Version: ${repoData.hash.slice(0,7)}${repoData.message}`});
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})}`})]})) initialMsg.edit({content: null, embeds: [embed]});
}, }
data: new Discord.SlashCommandBuilder() 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') .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 Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js'; import MessageTool from '../helpers/MessageTool.js';
import HookMgr from '../funcs/HookManager.js'; import HookMgr from '../components/HookManager.js';
export default { export default class Suggest {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ static async run(client:TClient, interaction:Discord.ChatInputCommandInteraction<'cached'>) {
const replyInDM = interaction.options.getString('message'); const idVal = interaction.options.getInteger('id');
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}**`;
({ ({
your: async()=>{ create: async()=>{
const suggestionText = interaction.options.getString('suggestion'); const suggestion = interaction.options.getString('suggestion', true);
const suggestionImage = interaction.options.getAttachment('image'); const newSugg = await client.suggestions.create(interaction.user.id, suggestion);
const notifEmbed = new client.embed() this.newWebhookMessage(client, newSugg.dataValues.id, suggestion, interaction.user.username);
.setColor(client.config.embedColor) return interaction.reply({content: `Your suggestion has been sent to bot developers. \`#${newSugg.dataValues.id}\``, ephemeral: true});
.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})
}, },
approve: async()=>{ delete: async()=>{
if (client.config.mainServer.id === interaction.guildId) { if (client.config.dcServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.mainServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'bottech'); 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}); const sugg = await this.deleteSuggestion(client, idVal);
(await client.users.fetch(userid)).send({embeds: [new client.embed() if (sugg) return interaction.reply(`Suggestion \`#${idVal}\` has been deleted.`);
.setColor(client.config.embedColorGreen) else return interaction.reply(`Suggestion \`#${idVal}\` does not exist.`);
.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)]});
}, },
reject: async()=>{ update: async()=>{
if (client.config.mainServer.id === interaction.guildId) { if (client.config.dcServer.id === interaction.guildId) {
if (!interaction.member.roles.cache.has(client.config.mainServer.roles.bottech)) return MessageTool.youNeedRole(interaction, 'bottech'); 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}); const status = interaction.options.getString('status', true);
(await client.users.fetch(userid)).send({embeds: [new client.embed() await this.updateSuggestion(client, idVal, status as 'Accepted'|'Rejected');
.setColor(client.config.embedColorRed) 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}\``))
.setAuthor({name: interaction.user.username, iconURL: interaction.user.avatarURL({size: 256})}) return await interaction.reply(`Suggestion \`#${idVal}\` has been updated to \`${status}\`.`);
.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)]});
} }
} as any)[interaction.options.getSubcommand()](); } 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') .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 .addSubcommand(x=>x
.setName('your') .setName('create')
.setDescription('What do you want to suggest?') .setDescription('Create a new suggestion for your idea')
.addStringOption(x=>x .addStringOption(x=>x
.setName('suggestion') .setName('suggestion')
.setDescription('Suggest something to bot techs. (You will be DM\'d by bot if your idea was approved/rejected)') .setDescription('Your precious idea')
.setMaxLength(1024) .setRequired(true)))
.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.')))
.addSubcommand(x=>x .addSubcommand(x=>x
.setName('approve') .setName('delete')
.setDescription('[Bot Tech] Approve the suggestion sent by the user') .setDescription('Delete a suggestion (Bot Tech only)')
.addStringOption(x=>x .addIntegerOption(x=>x
.setName('id') .setName('id')
.setDescription('User\'s suggestion ID') .setDescription('The ID of the suggestion')
.setRequired(true)) .setRequired(true)))
.addStringOption(x=>x
.setName('message')
.setDescription('(Optional) Include a message with your approval')
.setRequired(true)
.setMaxLength(256)))
.addSubcommand(x=>x .addSubcommand(x=>x
.setName('reject') .setName('update')
.setDescription('[Bot Tech] Reject the suggestion sent by the user') .setDescription('Update a suggestion (Bot Tech only)')
.addStringOption(x=>x .addIntegerOption(x=>x
.setName('id') .setName('id')
.setDescription('User\'s suggestion ID') .setDescription('The ID of the suggestion')
.setRequired(true)) .setRequired(true))
.addStringOption(x=>x .addStringOption(x=>x
.setName('message') .setName('status')
.setDescription('(Optional) Include a message with your rejection') .setDescription('The status of the suggestion (Accepted/Rejected)')
.setRequired(true) .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 Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js'; export default class Tag {
export default { static async autocomplete(client:TClient, interaction:Discord.AutocompleteInteraction<'cached'>) {
async autocomplete(client: TClient, interaction: Discord.AutocompleteInteraction){ const tagsInCache = await client.tags?.findInCache();
const array = (await client.tags?.findInCache())?.map(x=>x._id).filter(c=>c.startsWith(interaction.options.getFocused())); const filterArray = tagsInCache?.map(x=>x.tagname).filter(x=>x.startsWith(interaction.options.getFocused()));
await interaction?.respond(array?.map(c=>({name: c, value: c}))); await interaction?.respond(filterArray?.map(tag=>({name: tag, value: tag})));
// 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. }
}, static async run(client:TClient, interaction:Discord.ChatInputCommandInteraction<'cached'>) {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ const tagName = interaction.options.getString('tag-name');
if (interaction.options.getSubcommandGroup() === 'management' && !MessageTool.isStaff(interaction.member) && !client.config.whitelist.includes(interaction.member.id)) return MessageTool.youNeedRole(interaction, 'dcmod'); const tagMsg = interaction.options.getString('message');
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
};
({ ({
send: async()=>{ send: async()=>await client.tags.sendTag(interaction, tagName, interaction.options.getMember('target')?.id),
if (!await tagData()) return interaction.reply({content:'This tag is not available in the database.',ephemeral:true}); create: async()=>{
let targetField = ''; const newTag = await client.tags.createTag(interaction.member.id, tagName, tagMsg, interaction.options.getBoolean('toggle-embed'));
const targetMember = interaction.options.getMember('target_user'); await interaction.reply(newTag ? 'Tag successfully created, should be available in the list soon!' : 'Tag already exists, try again with a different name.');
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']}})
}, },
create: async()=>await client.tags._content.create({ delete: async()=>{
_id: interaction.options.getString('name'), await client.tags.deleteTag(tagName);
message: interaction.options.getString('message').replaceAll(/\\n/g, '\n'), return interaction.reply('Tag successfully deleted.');
embedBool: interaction.options.getBoolean('embed'), },
user: { modify: async()=>{
_id: interaction.member.id, await client.tags.modifyTag(tagName, interaction.options.getString('new-message'));
name: interaction.user.username return interaction.reply('Tag successfully modified.')
} }
})
.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}\`\`\``))
} as any)[interaction.options.getSubcommand() ?? interaction.options.getSubcommandGroup()](); } as any)[interaction.options.getSubcommand() ?? interaction.options.getSubcommandGroup()]();
}, }
data: new Discord.SlashCommandBuilder() static data = new Discord.SlashCommandBuilder()
.setName('tag') .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 .addSubcommand(x=>x
.setName('send') .setName('send')
.setDescription('Send a resource tag') .setDescription('Send a resource tag')
.addStringOption(x=>x .addStringOption(x=>x
.setName('name') .setName('tag-name')
.setDescription('Name of an existing tag to send') .setDescription('Name of an existing tag to send')
.setAutocomplete(true) .setAutocomplete(true)
.setRequired(true)) .setRequired(true))
.addUserOption(x=>x .addUserOption(x=>x
.setName('target_user') .setName('target')
.setDescription('Directly mention the target with this tag'))) .setDescription('Directly mention the member with this tag')
.setRequired(false)))
.addSubcommandGroup(x=>x .addSubcommandGroup(x=>x
.setName('management') .setName('tools')
.setDescription('Add a new tag or delete/edit your current tag') .setDescription('Management tools for the tags system (Discord mods & Bot Tech only)')
.addSubcommand(x=>x .addSubcommand(x=>x
.setName('create') .setName('create')
.setDescription('Create a new tag') .setDescription('Create a new tag')
.addStringOption(x=>x .addStringOption(x=>x
.setName('name') .setName('tag-name')
.setDescription('Name of your tag, must be within 3-25 characters') .setDescription('Name of the tag, must be within 4-32 characters')
.setMinLength(3) .setMinLength(4)
.setMaxLength(25) .setMaxLength(32)
.setRequired(true)) .setRequired(true))
.addStringOption(x=>x .addStringOption(x=>x
.setName('message') .setName('message')
.setDescription('Message to be included in your tag; e.g, you\'re giving the user some instructions, newline: \\n') .setDescription('Message to be included in your tag, newline: \\n')
.setMinLength(6) .setMaxLength(1990)
.setMaxLength(2048)
.setRequired(true)) .setRequired(true))
.addBooleanOption(x=>x .addBooleanOption(x=>x
.setName('embed') .setName('toggle-embed')
.setDescription('Toggle this option if you want your message to be inside the embed or not') .setDescription('Message will be sent in an embed description if enabled')
.setRequired(true))) .setRequired(true)))
.addSubcommand(x=>x .addSubcommand(x=>x
.setName('delete') .setName('delete')
.setDescription('Delete a tag') .setDescription('Delete an existing tag')
.addStringOption(x=>x .addStringOption(x=>x
.setName('name') .setName('tag-name')
.setDescription('Name of the tag to be deleted') .setDescription('Name of the tag to be deleted')
.setAutocomplete(true) .setAutocomplete(true)
.setRequired(true))) .setRequired(true)))
.addSubcommand(x=>x .addSubcommand(x=>x
.setName('edit') .setName('modify')
.setDescription('Edit an existing tag') .setDescription('Modify an existing tag')
.addStringOption(x=>x .addStringOption(x=>x
.setName('name') .setName('tag-name')
.setDescription('Name of the tag to be edited') .setDescription('Name of the tag to be modified')
.setAutocomplete(true) .setAutocomplete(true)
.setRequired(true)) .setRequired(true))
.addStringOption(x=>x .addStringOption(x=>x
.setName('new-message') .setName('new-message')
.setDescription('Replace the current tag\'s message with a new one, newline: \\n') .setDescription('Replace the current tag\'s message with a new one, newline: \\n')
.setRequired(true)) .setMaxLength(1990)
.addBooleanOption(x=>x
.setName('embed')
.setDescription('Toggle this option on an existing tag to be updated with embed or not')
.setRequired(true)))) .setRequired(true))))
} }

View File

@ -2,18 +2,19 @@ import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import Logger from '../helpers/Logger.js'; import Logger from '../helpers/Logger.js';
import MessageTool from '../helpers/MessageTool.js'; import MessageTool from '../helpers/MessageTool.js';
export default { export default class Unpunish {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ static async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
if (!MessageTool.isStaff(interaction.member as Discord.GuildMember)) return MessageTool.youNeedRole(interaction, 'dcmod'); 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)); const punishment = await client.punishments.findCase(interaction.options.getInteger('case_id', true));
if (!punishment) return interaction.reply({content: 'Invalid Case ID', ephemeral: true}); if (!punishment) return interaction.reply({content: 'Case ID is not found in database.', ephemeral: true});
if (punishment.expired) return interaction.reply('This case has been overwritten by another case.'); 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'; const reason = interaction.options.getString('reason') ?? 'Reason unspecified';
await client.punishments.removePunishment(punishment.id, interaction.user.id, reason, interaction); await client.punishments.punishmentRemove(punishment.dataValues.case_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()]}); 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()]});
data: new Discord.SlashCommandBuilder() }
static data = new Discord.SlashCommandBuilder()
.setName('unpunish') .setName('unpunish')
.setDescription('Remove the active punishment from a member') .setDescription('Remove the active punishment from a member')
.addIntegerOption(x=>x .addIntegerOption(x=>x

View File

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

View File

@ -10,8 +10,8 @@ function convert(status?:Discord.ClientPresenceStatus){
else return '⚫' else return '⚫'
} }
export default { export default class Whois {
async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ static async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){
const member = interaction.options.getMember('member') as Discord.GuildMember; const member = interaction.options.getMember('member') as Discord.GuildMember;
if (member === null){ if (member === null){
const user = interaction.options.getUser('member') as Discord.User; const user = interaction.options.getUser('member') as Discord.User;
@ -34,8 +34,8 @@ export default {
const embed = new client.embed() const embed = new client.embed()
.setColor(member.displayColor || client.config.embedColor) .setColor(member.displayColor || client.config.embedColor)
.setURL(`https://discord.com/users/${member.user.id}`) .setURL(`https://discord.com/users/${member.user.id}`)
.setThumbnail(member.user.avatarURL({size:2048}) || member.user.defaultAvatarURL) .setThumbnail(member.avatarURL({size:2048}) || member.user.avatarURL({size:2048}) || member.user.defaultAvatarURL)
.setImage(member.user.bannerURL({size:1024}) as string) .setImage(member.user.bannerURL({size:1024}) || null)
.setTitle(`${title} Info: ${member.user.username}`) .setTitle(`${title} Info: ${member.user.username}`)
.setDescription(`<@${member.user.id}>\n\`${member.user.id}\``) .setDescription(`<@${member.user.id}>\n\`${member.user.id}\``)
.addFields( .addFields(
@ -62,8 +62,8 @@ export default {
} }
interaction.reply({embeds: embedArray}) interaction.reply({embeds: embedArray})
} }
}, }
data: new Discord.SlashCommandBuilder() static data = new Discord.SlashCommandBuilder()
.setName('whois') .setName('whois')
.setDescription('View your own or someone else\'s information') .setDescription('View your own or someone else\'s information')
.addUserOption(x=>x .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", "configName": "Daggerbot",
"embedColor": "#FFFFFF", "embedColor": "#0052cf",
"embedColorBackup": "#0052cf", "embedColorBackup": "#0052cf",
"embedColorGreen": "#57f287", "embedColorGreen": "#57f287",
"embedColorOrange": "#cc5210",
"embedColorYellow": "#ffea00", "embedColorYellow": "#ffea00",
"embedColorRed": "#e62c3b", "embedColorRed": "#e62c3b",
"embedColorInvis": "#2f3136",
"embedColorBCA": "#ff69b4", "embedColorBCA": "#ff69b4",
"embedColorXmas": "#FFFFFF", "embedColorXmas": "#ffffff",
"LRSstart": 1661236321433, "LRSstart": 1661236321433,
"whitelistedServers": [ "whitelistedServers": [
"929807948748832798", "468835415093411861", "1058183358267543552", "549114074273677314" "929807948748832798", "468835415093411861"
], ],
"MPStatsLocation": {
"mainServer": {
"channel": "543494084363288637",
"message": "1023699243183112192"
},
"secondServer": {
"channel": "543494084363288637",
"message": "1149141188079779900"
}
},
"botSwitches": { "botSwitches": {
"dailyMsgsBackup": true, "dailyMsgsBackup": true,
"registerCommands": false, "registerCommands": true,
"commands": true, "commands": true,
"logs": true, "logs": true,
"mpSys": true,
"buttonRoles": true, "buttonRoles": true,
"automod": true, "automod": true,
"mpstats": true,
"autores": true "autores": true
}, },
"botPresence": { "botPresence": {
@ -38,13 +28,12 @@
], ],
"status": "online" "status": "online"
}, },
"eval": true,
"whitelist": [ "whitelist": [
"190407856527376384", "190407856527376384",
"633345781780185099",
"215497515934416896", "215497515934416896",
"141304507249197057", "141304507249197057",
"309373272594579456" "309373272594579456",
"301350210926280704"
], ],
"contribList": [ "contribList": [
"190407856527376384", "190407856527376384",
@ -54,7 +43,7 @@
"178941218510602240", "178941218510602240",
"700641965787709520" "700641965787709520"
], ],
"mainServer": { "dcServer": {
"id": "468835415093411861", "id": "468835415093411861",
"staffRoles": [ "staffRoles": [
"admin", "admin",
@ -79,7 +68,6 @@
"vtcmember": "802282391652663338" "vtcmember": "802282391652663338"
}, },
"channels": { "channels": {
"console": "1011318687065710663",
"errors": "1009754872188506192", "errors": "1009754872188506192",
"thismeanswar": "1091300529696673792", "thismeanswar": "1091300529696673792",
"bot_suggestions": "1040018521746325586", "bot_suggestions": "1040018521746325586",
@ -88,10 +76,9 @@
"welcome": "621134751897616406", "welcome": "621134751897616406",
"botcommands": "468888722210029588", "botcommands": "468888722210029588",
"bankick_log": "1048341961901363352", "bankick_log": "1048341961901363352",
"fs_server_log": "1104632399771488317",
"punishment_log": "1102751034754998302", "punishment_log": "1102751034754998302",
"dcmod_chat": "742324777934520350", "dcmod_chat": "742324777934520350",
"mf_chat": "1149238561934151690" "mpmod_chat": "516344221452599306"
} }
} }
} }

View File

@ -1,8 +1,8 @@
import Discord, { AuditLogEvent } from 'discord.js'; import Discord, { AuditLogEvent } from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
export default { export default class GuildBanAdd {
async run(client:TClient, member:Discord.GuildMember){ static async run(client:TClient, member:Discord.GuildMember){
if (member.guild?.id != client.config.mainServer.id) return; if (member.guild?.id != client.config.dcServer.id) return;
const banLog = (await member.guild.fetchAuditLogs({ limit: 1, type: AuditLogEvent.MemberBanAdd })).entries.first(); 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.`) if (!banLog) return console.log(`Member was banned from ${member.guild.name} but no audit log for this member.`)
const {executor, target, reason } = banLog; const {executor, target, reason } = banLog;
@ -11,8 +11,8 @@ export default {
{name: '🔹 Moderator', value: `<@${executor.id}>\n\`${executor.id}\``}, {name: '🔹 Moderator', value: `<@${executor.id}>\n\`${executor.id}\``},
{name: '🔹 Reason', value: `${reason === null ? 'Reason unspecified': reason}`} {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.'}); if (!await client.userLevels.fetchUser(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]}) (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.`) } 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 Discord, { AuditLogEvent } from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
export default { export default class GuildBanRemove {
async run(client:TClient, member:Discord.GuildMember){ static async run(client:TClient, member:Discord.GuildMember){
if (member.guild?.id != client.config.mainServer.id) return; if (member.guild?.id != client.config.dcServer.id) return;
const unbanLog = (await member.guild.fetchAuditLogs({limit: 1, type: AuditLogEvent.MemberBanRemove})).entries.first(); 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.`) if (!unbanLog) return console.log(`User was unbanned from ${member.guild.name} but no audit log for this user.`)
const { executor, target, reason } = unbanLog; 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( 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: '🔹 Moderator', value: `<@${executor.id}>\n\`${executor.id}\``},
{name: '🔹 Reason', value: `${reason === null ? 'Reason unspecified.': reason}`} {name: '🔹 Reason', value: `${reason === null ? 'Reason unspecified.': reason}`}

View File

@ -1,9 +1,8 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js'; export default class GuildMemberAdd {
export default { static async run(client:TClient, member:Discord.GuildMember){
async run(client:TClient, member:Discord.GuildMember){ if (member.partial || member.guild?.id != client.config.dcServer.id) return;
if (member.partial || member.guild?.id != client.config.mainServer.id) return;
const index = member.guild.memberCount; const index = member.guild.memberCount;
const suffix = (index=>{ const suffix = (index=>{
const numbers = index.toString().split('').reverse(); // eg 1850 --> [0,5,8,1] const numbers = index.toString().split('').reverse(); // eg 1850 --> [0,5,8,1]
@ -18,21 +17,16 @@ export default {
let isBot = 'Bot'; let isBot = 'Bot';
if (!member.user.bot) isBot = 'Member'; if (!member.user.bot) isBot = 'Member';
if (!client.config.botSwitches.logs) return; 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 newInvites = await member.guild.invites.fetch();
const usedInvite = newInvites.find((inv:Discord.Invite)=>client.invites.get(inv.code)?.uses < inv.uses); 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})); 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( 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: '🔹 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.'} {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( await client.punishments.caseEvasionCheck(member);
`**${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)
}
} }
} }

View File

@ -1,20 +1,20 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
export default { export default class GuildMemberRemove {
async run(client:TClient, member:Discord.GuildMember){ static async run(client:TClient, member:Discord.GuildMember){
if (!client.config.botSwitches.logs) return; if (!client.config.botSwitches.logs) return;
if (!member.joinedTimestamp || member.guild?.id != client.config.mainServer.id) return; if (!member.joinedTimestamp || member.guild?.id != client.config.dcServer.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 (client.guilds.cache.get(client.config.dcServer.id).bans.cache.has(member.id)) return await client.userLevels.deleteUser(member.id);
let isBot = 'Bot'; let isBot = 'Bot';
if (!member.user.bot) isBot = 'Member'; 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( 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: '🔹 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: '🔹 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} {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}); 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.mainServer.channels.logs) as Discord.TextChannel).send({embeds:[embed]}); (client.channels.resolve(client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [embed]});
await client.userLevels._content.findByIdAndDelete(member.id) await client.userLevels.deleteUser(member.id);
} }
} }

View File

@ -1,10 +1,10 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
export default { export default class GuildMemberUpdate {
run(client:TClient, oldMember:Discord.GuildMember, newMember:Discord.GuildMember){ static run(client:TClient, oldMember:Discord.GuildMember, newMember:Discord.GuildMember){
if (oldMember.guild.id != client.config.mainServer.id) return; if (oldMember.guild.id != client.config.dcServer.id) return;
if (!client.config.botSwitches.logs) 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){ 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}\``) 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}\`\`\``}) oldMember.nickname === null ? '' : embed.addFields({name: '🔹 Old nickname', value: `\`\`\`${oldMember.nickname}\`\`\``})

View File

@ -1,13 +1,14 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import Logger from '../helpers/Logger.js'; import Logger from '../helpers/Logger.js';
export default class InteractionCreate {
export default { static async run(client:TClient, interaction:Discord.BaseInteraction){
async run(client:TClient, interaction:Discord.BaseInteraction){
if (!interaction.inGuild() || !interaction.inCachedGuild()) return; if (!interaction.inGuild() || !interaction.inCachedGuild()) return;
const logPrefix = 'Interaction';
if (interaction.isChatInputCommand()){ if (interaction.isChatInputCommand()){
const commandFile = client.commands.get(interaction.commandName); 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 (!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){ if (commandFile){
try{ try{
@ -28,28 +29,20 @@ export default {
} else if (interaction.isButton()){ } else if (interaction.isButton()){
if (interaction.customId.startsWith('reaction-') && client.config.botSwitches.buttonRoles){ if (interaction.customId.startsWith('reaction-') && client.config.botSwitches.buttonRoles){
const RoleID = interaction.customId.replace('reaction-',''); 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)){ let roleConflictMsg = 'Cannot have both roles! - Button Role';
interaction.member.roles.remove(RoleID, 'Button Role'); const WestFarm = '1149139369433776269';
interaction.reply({content: `You have been removed from <@&${RoleID}>`, ephemeral: true}) const EastFarm = '1149139583729160325';
} else { if (interaction.member.roles.cache.has(WestFarm) && RoleID === EastFarm) interaction.member.roles.set([EastFarm], roleConflictMsg);
interaction.member.roles.add(RoleID, 'Button Role'); else if (interaction.member.roles.cache.has(EastFarm) && RoleID === WestFarm) interaction.member.roles.set([WestFarm], roleConflictMsg);
interaction.reply({content: `You have been added to <@&${RoleID}>`, ephemeral: true})
} 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 if (interaction.customId.includes('deleteEmbed')) { else interaction.member.roles.add(RoleID, 'Button Role').then(()=>interaction.reply({content: `You have been added to <@&${RoleID}>`, ephemeral: true}));
if (!client.config.whitelist.includes(interaction.user.id)) return interaction.reply({content: '*Only whitelisted people can delete this embed.*', ephemeral: true}); } else if (interaction.customId.includes('deleteEvalEmbed')) {
interaction.message.edit({content: '*Deleted.*', embeds: [], components: []}); if (!client.config.whitelist.includes(interaction.user.id)) return interaction.reply({content: 'You are not whitelisted, therefore you cannot delete this message.', ephemeral: true});
Logger.forwardToConsole('log', 'InteractionLog', `Embed has been deleted at ${interaction.message.url}`); interaction.message.delete();
} else Logger.forwardToConsole('log', 'InteractionLog', `Button has been pressed at ${interaction.message.url}`); 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 Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
export default { export default class InviteCreate {
async run(client:TClient, invite: Discord.Invite){ static async run(client:TClient, invite:Discord.Invite){
if (!invite.guild) return; 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})) (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 Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
export default { export default class InviteDelete {
run(client:TClient, invite: Discord.Invite){ static run(client:TClient, invite:Discord.Invite){
client.invites.delete(invite.code) client.invites.delete(invite.code)
} }
} }

View File

@ -1,35 +1,36 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import Response from '../funcs/ResponseModule.js'; import Response from '../modules/ResponseModule.js';
import CmdTrigger from '../funcs/CmdModule.js'; import CmdTrigger from '../modules/CmdModule.js';
import Logger from '../helpers/Logger.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'; import MessageTool from '../helpers/MessageTool.js';
export default { export default class MessageCreate {
async run(client:TClient, message:Discord.Message){ static async run(client:TClient, message:Discord.Message){
if (message.author.bot) return; 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; 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.'; 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; 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)); 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'); 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)){ } 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); 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; 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)); 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. // Mop gifs from banned channels without Monster having to mop them.
const bannedChannels = [ const bannedChannels = [
'516344221452599306', // #mp-moderators '516344221452599306', // #mp-moderators
@ -48,7 +49,7 @@ export default {
const outgoingArrays = { 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}>!`], 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, 'morning');
Response.create(client, message, GeneralChatID, 'afternoon'); Response.create(client, message, GeneralChatID, 'afternoon');
Response.create(client, message, GeneralChatID, 'evening'); Response.create(client, message, GeneralChatID, 'evening');
@ -57,15 +58,36 @@ export default {
CmdTrigger.registerCmds(client, message, 'wepanikfrfr'); CmdTrigger.registerCmds(client, message, 'wepanikfrfr');
CmdTrigger.MFPwTrigger(message, 'farmpw'); 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('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.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 (incomingArrays.cantRead.some(e=>message.content.toLowerCase().includes(e))) message.reply(picStorage.cantRead);
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 (message.content.toLowerCase().includes('is daggerbot working')) message.reply(picStorage.amAlive);
if (incomingArrays.deadChat.some(e=>message.content.toLowerCase().includes(e))) message.reply('https://cdn.discordapp.com/attachments/925589318276382720/1011333656167579849/F57G5ZS.png'); if (incomingArrays.deadChat.some(e=>message.content.toLowerCase().includes(e))) message.reply(picStorage.deadChat);
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)]}); 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 TClient from '../client.js';
import Logger from '../helpers/Logger.js'; import Logger from '../helpers/Logger.js';
import {escapeCodeBlock} from 'discord.js'; import {escapeCodeBlock} from 'discord.js';
export default { export default class MessageDelete {
run(client:TClient, msg:Discord.Message){ static run(client:TClient, msg:Discord.Message){
if (!client.config.botSwitches.logs) return; if (!client.config.botSwitches.logs) return;
const disabledChannels = ['548032776830582794', '541677709487505408', '949380187668242483'] const disabledChannels = ['548032776830582794', '541677709487505408', '949380187668242483']
if (msg.guild?.id != client.config.mainServer.id || msg.partial || msg.author.bot || disabledChannels.includes(msg.channelId)) return; 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.forwardToConsole('log', 'MsgDelete', 'Caught an unexpected error returned by Discord API. (Unknown Message)'); 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}\``); 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\`\`\``}); if (msg.content.length != 0) embed.addFields({name: 'Content', value: `\`\`\`\n${escapeCodeBlock(msg.content.slice(0,1000))}\n\`\`\``});
embed.addFields( embed.addFields(
@ -16,6 +16,6 @@ export default {
) )
const attachments: Array<string> = []; const attachments: Array<string> = [];
msg.attachments.forEach(x=>attachments.push(x.url)); 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 Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
export default { export default class MessageDeleteBulk {
run(client:TClient, messages:Discord.Collection<string, Discord.Message<boolean>>, channel:Discord.GuildTextBasedChannel){ static 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; if (!client.config.botSwitches.logs || channel.guildId != client.config.dcServer.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}>`})]}) (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 Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
export default { export default class MessageReactionAdd {
run(client:TClient, reaction:Discord.MessageReaction, user:Discord.User){ static run(client:TClient, reaction:Discord.MessageReaction, user:Discord.User){
if (!client.config.botSwitches.logs) return; 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(); const ReactedFirst = reaction.users.cache.first();
if (ReactedFirst.id != user.id) return; 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 Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
export default { export default class MessageReactionRemove {
run(client:TClient, reaction:Discord.MessageReaction, user:Discord.User){ static 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 (!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.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}>`)]}) 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 TClient from '../client.js';
import MessageTool from '../helpers/MessageTool.js'; import MessageTool from '../helpers/MessageTool.js';
import {escapeCodeBlock} from 'discord.js'; import {escapeCodeBlock} from 'discord.js';
export default { export default class MessageUpdate {
async run(client:TClient, oldMsg:Discord.Message, newMsg:Discord.Message){ static async run(client:TClient, oldMsg:Discord.Message, newMsg:Discord.Message){
if (!client.config.botSwitches.logs) return; 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 (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.bannedWords._content.findOne({_id:newMsg.content.toLowerCase().replaceAll(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?\n]/g, ' ').split(' ')}) && (!MessageTool.isStaff(newMsg.member))) newMsg.delete(); 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; 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 Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import ansi from 'ansi-colors'; import ansi from 'ansi-colors';
export default { export default class Ready {
async run(client:TClient){ 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')); 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(); await guild.members.fetch();
setInterval(()=>{ setInterval(()=>{
client.user.setPresence(client.config.botPresence); client.user.setPresence(client.config.botPresence);
@ -16,12 +22,12 @@ export default {
console.log('Total commands: '+client.registry.length) //Debugging reasons. 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)=>{ 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); 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.user.username} has logged into Discord API`);
console.log(client.config.botSwitches, client.config.whitelistedServers); 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') 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 {readFileSync} from 'node:fs';
import {Config} from '../typings/interfaces'; import {Config} from '../interfaces';
export default class ConfigHelper { export default class ConfigHelper {
static loadConfig() { static loadConfig() {
let importconfig:Config; let importconfig:Config;
@ -12,7 +12,6 @@ export default class ConfigHelper {
} }
return importconfig; return importconfig;
} }
static readConfig() { static readConfig =()=>JSON.parse(readFileSync(process.argv[2] ?? 'src/config.json', 'utf8')) as Config;
return 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 { export default class FormatPlayer {
static uptimeFormat(playTime: number){ static convertUptime(playTime:number) {
var Hours = 0; let Minutes:number;
playTime = Math.floor(Number(playTime)); let Hours:number;
if(playTime >= 60){ let Days:number;
var Hours = Math.floor(Number(playTime)/60);
var Minutes = (Number(playTime)-(Hours*60)); playTime = Math.floor(playTime);
} else Minutes = Number(playTime) if (playTime >= 60) {
if(Hours >= 24){ Hours = Math.floor(playTime/60);
var Days = Math.floor(Number(Hours)/24); Minutes = playTime-Hours*60;
var Hours = (Hours-(Days*24)); } else Minutes = playTime
} return (Days > 0 ? Days+' d ':'')+(Hours > 0 ? Hours+' h ':'')+(Minutes > 0 ? Minutes+' m':'')
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:' : ''; let decorator = player.isAdmin ? ':detective:' : '';
decorator += player.name.includes('Toast') ? '<:toast:1132681026662056079>' : ''; decorator += player.name.includes('Toast') ? '<:toast:1132681026662056079>' : '';
decorator += player.name.includes('Daggerwin') ? '<:Daggerwin:549283056079339520>' : ''; // Probably useless lol, but we'll see.
return decorator 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'; import dayjs from 'dayjs';
export default class Logger { export default class Logger {
static logTime() { public static console =(logType:'log'|'error', prefix:string, message:any)=>console[logType](`[${dayjs().format('DD/MM/YY HH:mm:ss')}]`, `[${prefix}]`, message);
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);
}
} }

View File

@ -1,26 +1,17 @@
import Discord from 'discord.js'; import Discord from 'discord.js';
import ConfigHelper from './ConfigHelper.js'; import ConfigHelper from './ConfigHelper.js';
const config = ConfigHelper.readConfig(); const config = ConfigHelper.readConfig();
type RoleKeys = keyof typeof config.mainServer.roles; type RoleKeys = keyof typeof config.dcServer.roles;
export default class MessageTool { 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); const embed = new Discord.EmbedBuilder().setColor(color).setTitle(title);
if (description) embed.setDescription(description); if (description) embed.setDescription(description);
if (image) embed.setImage(image); if (image) embed.setImage(image);
return embed return embed
} }
static concatMessage(...messages:string[]){ static concatMessage =(...messages:string[])=>messages.join('\n');
return 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 formatMention(mention:string, type:'user'|'channel'|'role'){ 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.`);
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')}`);
}
} }
// 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)=>{ export default function(data: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; const pallets = data.vehicles.filter(x=>x.category === 'PALLETS');
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=>({ const counts = pallets.reduce((acc, name)=>{
[t]:{ acc[name.name] = (acc[name.name] ?? 0) + 1;
name: t.toLowerCase().slice(0,1).toUpperCase()+t.toLowerCase().slice(1), return acc;
size: getAmount(t.toUpperCase()) }, {} as {[key:string]:number});
}, return counts;
})).reduce((a,b)=>({...a,...b}));
return palletTypeName;
} }

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

View File

@ -1,6 +1,4 @@
export default class UsernameHelper { export default (text:string)=>{
static stripName(text: string){ const dirSlash = process.platform === 'linux' ? '\/' : '\\';
const dirSlash = process.platform === 'linux' ? '\/' : '\\'; return text?.replace(/(?<=\/Users\/|\/media\/)[^/]+/g, match=>'・'.repeat(match.length)).split(dirSlash).join(dirSlash);
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; const client = new TClient;
client.init(); client.init();
import Logger from './helpers/Logger.js'; import Logger from './helpers/Logger.js';
import YTModule from './funcs/YTModule.js'; import YTModule from './modules/YTModule.js';
import MPModule from './funcs/MPModule.js'; import MPModule, {refreshTimerSecs} from './modules/MPModule.js';
import UsernameHelper from './helpers/UsernameHelper.js'; import UsernameHelper from './helpers/UsernameHelper.js';
import {Punishment} from './typings/interfaces'; import {Punishment} from './interfaces';
import {writeFileSync, readFileSync} from 'node:fs'; import {readFileSync} from 'node:fs';
// Error handler // 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; if (JSON.parse(readFileSync('src/errorBlocklist.json', 'utf8')).includes(error.message)) return;
console.error(error); 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('unhandledRejection', (error: Error)=>_(error, 'unhandledRejection'));
process.on('uncaughtException', (error: Error)=>DZ(error, 'uncaughtException')); process.on('uncaughtException', (error: Error)=>_(error, 'uncaughtException'));
process.on('error', (error: Error)=>DZ(error, 'nodeError')); process.on('error', (error: Error)=>_(error, 'processError'));
client.on('error', (error: Error)=>DZ(error, 'clientError')); client.on('error', (error: Error)=>_(error, 'clientError'));
// YouTube Upload notification and MP loop // Interval timers for modules
if (client.config.botSwitches.mpstats) setInterval(async()=>{ setInterval(async()=>await MPModule(client), refreshTimerSecs); // Second param got moved to inside MPModule function to reduce the amount of failure rates.
const serverlake = (await client.MPServer.findInCache(client.config.mainServer.id)); setInterval(()=>YTModule(client), 180000); // 3 minutes
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
// Event loop for punishments and daily msgs // Event loop for punishments and daily msgs
setInterval(async()=>{ setInterval(async()=>{
const now = Date.now(); const now = Date.now();
const punishments = await client.punishments.findInCache(); const punishments = await client.punishments.findInCache();
punishments.filter((x:Punishment)=>x.endTime && x.endTime<= now && !x.expired).forEach(async (punishment:Punishment)=>{ 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.console('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!')); 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 formattedDate = Math.floor((now - client.config.LRSstart)/1000/60/60/24);
const dailyMsgs = JSON.parse(readFileSync('./src/database/dailyMsgs.json', 'utf8')) const dailyMsgs = await client.dailyMsgs.fetchDays();
if (client.config.botSwitches.dailyMsgsBackup && !dailyMsgs.some((x:Array<number>)=>x[0] === formattedDate)){ if (client.config.botSwitches.dailyMsgsBackup && !dailyMsgs.some(x=>x[0] === formattedDate)) {
client.userLevels.resetAllData(); // reset all data on 1st of January every year 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 // Send notification to #bot-logs that the data has been pushed to database.
const yesterday = dailyMsgs.find((x:Array<number>)=>x[0] === formattedDate - 1); const commands = await client.guilds.cache.get(client.config.dcServer.id)?.commands.fetch();
if (total < yesterday) total = yesterday // messages went down. if (commands) (client.channels.resolve(client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [
dailyMsgs.push([formattedDate, total]); new client.embed().setDescription(`Pushed the following\ndata to </rank leaderboard:${commands.find(x=>x.name === 'rank').id}>`).setFields(
writeFileSync('./src/database/dailyMsgs.json', JSON.stringify(dailyMsgs)) {name: 'Day', value: formattedDate.toString(), inline: true},
Logger.forwardToConsole('log', 'DailyMsgs', `Pushed [${formattedDate}, ${total}]`) {name: 'Messages', value: Intl.NumberFormat('en-us').format(total).toString(), inline: true}
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}>`)); ).setColor(client.config.embedColor)
(client.channels.resolve(client.config.mainServer.channels.thismeanswar) as Discord.TextChannel).send({files:['./src/database/dailyMsgs.json']}).catch(fileErr=>console.log(fileErr)) ]});
else Logger.console('log', 'DailyMsgs', 'Rank command not found, cannot send notification in channel')
}
} }
}, 5000) }, 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 { export interface Punishment {
_id: number; case_id: number;
type: string; type: string;
member: string; member: string;
moderator: string; moderator: string;
@ -27,9 +27,9 @@ export interface FSData {
slots: { slots: {
capacity: number, capacity: number,
used: number, used: number,
players: Array<FSPlayer> players: FSPlayer[]
}, },
vehicles: Array<FSVehicle> vehicles: FSVehicle[]
} }
interface FSVehicle { interface FSVehicle {
name: string, name: string,
@ -38,7 +38,7 @@ interface FSVehicle {
x: number, x: number,
y: number, y: number,
z: number, z: number,
fills: Array<FSVehicleFill>, fills: FSVehicleFill[],
controller: string controller: string
} }
interface FSVehicleFill { interface FSVehicleFill {
@ -46,7 +46,7 @@ interface FSVehicleFill {
level: number level: number
} }
export interface FSPlayer { export interface FSPlayer {
isUsed: boolean, isUsed?: boolean,
isAdmin: boolean, isAdmin: boolean,
uptime: number, uptime: number,
name: string name: string
@ -108,42 +108,31 @@ export interface FSCareerSavegame {
} }
export interface Config { export interface Config {
configName: string, configName: string,
embedColor: Discord.ColorResolvable, embedColor: ColorResolvable,
embedColorGreen: Discord.ColorResolvable, embedColorGreen: ColorResolvable,
embedColorOrange: Discord.ColorResolvable, embedColorYellow: ColorResolvable,
embedColorYellow: Discord.ColorResolvable, embedColorRed: ColorResolvable,
embedColorRed: Discord.ColorResolvable, embedColorInvis: ColorResolvable,
embedColorBCA: Discord.ColorResolvable, embedColorBCA: ColorResolvable,
embedColorXmas: Discord.ColorResolvable, embedColorXmas: ColorResolvable,
LRSstart: number, LRSstart: number,
whitelistedServers: Array<string>, whitelistedServers: string[],
MPStatsLocation: {
mainServer: {
channel: string
message: string
},
secondServer: {
channel: string
message: string
}
},
botSwitches: { botSwitches: {
dailyMsgsBackup: boolean, dailyMsgsBackup: boolean,
registerCommands: boolean, registerCommands: boolean,
commands: boolean, commands: boolean,
logs: boolean, logs: boolean,
mpSys: boolean,
buttonRoles: boolean, buttonRoles: boolean,
automod: boolean, automod: boolean,
mpstats: boolean,
autores: boolean autores: boolean
}, },
botPresence: Discord.PresenceData, botPresence: PresenceData,
eval: boolean, whitelist: string[],
whitelist: Array<string>, contribList: string[],
contribList: Array<string>, dcServer: {
mainServer: {
id: string, id: string,
staffRoles: Array<string>, staffRoles: string[],
roles: { roles: {
admin: string, admin: string,
bottech: string, bottech: string,
@ -158,7 +147,6 @@ export interface Config {
vtcmember: string vtcmember: string
}, },
channels: { channels: {
console: string,
errors: string, errors: string,
thismeanswar: string, thismeanswar: string,
bot_suggestions: string, bot_suggestions: string,
@ -167,10 +155,9 @@ export interface Config {
welcome: string, welcome: string,
botcommands: string, botcommands: string,
bankick_log: string, bankick_log: string,
fs_server_log: string,
punishment_log: string, punishment_log: string,
dcmod_chat: 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 DatabaseServer from '../components/DatabaseServer.js';
import mongoose from 'mongoose'; import {Model, DataTypes} from 'sequelize';
import CacheServer from '../funcs/CacheServer.js'; import CacheServer from '../components/CacheServer.js';
const Schema = mongoose.model('mpserver', new mongoose.Schema({ class MPServer extends Model {
_id: {type: String, required:true}, declare public serverName: string;
mainServer: {required:true, type: new mongoose.Schema({ declare public isActive: boolean;
ip: {type: String, required:true}, declare public ip: string;
code: {type: String, required:true} declare public code: string;
}, {versionKey: false})}, declare public playerData: number[];
secondServer: {required:true, type: new mongoose.Schema({ }
ip: {type: String, required:true},
code: {type: String, required:true}
}, {versionKey: false})},
}, {versionKey: false}));
export default class MPServer extends Schema { export interface IServer {
client: TClient; serverName: string
_content: typeof Schema; isActive: boolean
constructor(client:TClient){ ip: string
super(); code: string
this.client = client; playerData: number[]
this._content = Schema; }
}
async findInCache(query:any): Promise<any> { const cacheKey = 'MPServer';
const cacheKey = `MPServer:${query}`;
const cachedResult = await CacheServer.get(cacheKey); export class MPServerSvc {
let result; private model: typeof MPServer;
if (cachedResult) {
try { constructor() {
result = cachedResult; this.model = MPServer;
} catch (error) { this.model.init({
console.error('Error parsing cached result:', error); serverName: {
result = await this._content.findById(query); type: DataTypes.STRING,
CacheServer.set(cacheKey, result); allowNull: false,
CacheServer.expiry(cacheKey, 1800); 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 { } else {
result = await this._content.findById(query); await this.model.create({
CacheServer.set(cacheKey, result); serverName: serverName,
CacheServer.expiry(cacheKey, 1800); 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; 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 DatabaseServer from '../components/DatabaseServer.js';
import mongoose from 'mongoose'; import {Model, DataTypes} from 'sequelize';
const Schema = mongoose.model('bonkCount', new mongoose.Schema({ class bonkCount extends Model {
_id: {type: String, required:true}, declare public id: string;
value: {type: Number, required:true} declare public count: number;
}, {versionKey: false})); }
export default class bonkCount extends Schema { export class BonkCountSvc {
client: TClient; private model: typeof bonkCount;
_content: typeof Schema;
constructor(client:TClient){ constructor(){
super(); this.model = bonkCount;
this.client = client; this.model.init({
this._content = Schema; 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){ async hitCountIncremental(userId:string) {
const amount = await this._content.findById(userid) const getUser = await this.model.findByPk(userId);
if (amount) await this._content.findByIdAndUpdate(userid, {value: amount.value + 1}) if (getUser) getUser.increment('count');
else await this._content.create({_id: userid, value: 1}) else await this.model.create({id: userId, count: 1});
return this; 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 Discord from 'discord.js';
import TClient from '../client.js'; import TClient from '../client.js';
import mongoose from 'mongoose';
import CacheServer from '../funcs/CacheServer.js';
import ms from 'ms'; import ms from 'ms';
import FormatTime from '../helpers/FormatTime.js'; import {Punishment} from '../interfaces';
import {Punishment} from '../typings/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({ class punishments extends Model {
_id: {type: Number, required: true}, declare public case_id: number;
type: {type: String, required: true}, declare public type: string;
member: {type: String, required: true}, declare public member: string;
moderator: {type: String, required: true}, declare public moderator: string;
expired: {type: Boolean}, declare public expired: boolean;
time: {type: Number, required: true}, declare public time: number;
reason: {type: String, required: true}, declare public reason: string;
endTime: {type: Number}, declare public endTime: number;
cancels: {type: Number}, declare public cancels: number;
duration: {type: Number} declare public duration: number;
}, {versionKey: false})); }
export default class punishments extends Schema { export class PunishmentsSvc {
client: TClient; private client: TClient;
_content: typeof Schema; private model: typeof punishments;
constructor(client:TClient){
super(); constructor(client:TClient) {
this.client = client; this.client = client;
this._content = Schema; this.model = punishments;
} this.model.init({
async findInCache(): Promise<any> { case_id: {
const cacheKey = 'punishments'; type: DataTypes.INTEGER,
const cachedResult = await CacheServer.get(cacheKey); unique: true,
let result; primaryKey: true
if (cachedResult) { },
try { type: {
result = cachedResult; type: DataTypes.STRING,
} catch (error) { allowNull: false
console.error('Error parsing cached result:', error); },
result = await this._content.find(); member: {
CacheServer.set(cacheKey, result); type: DataTypes.STRING,
CacheServer.expiry(cacheKey, 15); 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(); tableName: 'punishments',
CacheServer.set(cacheKey, result); createdAt: false,
CacheServer.expiry(cacheKey, 15); 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; return result;
} }
createId = async()=>Math.max(...(await this._content.find()).map(x=>x.id), 0) + 1; async createModlog(punishment:Punishment) {
async makeModlogEntry(punishment:Punishment){ const channel = ['kick', 'ban', 'softban'].includes(punishment.type) ? this.client.config.dcServer.channels.bankick_log : this.client.config.dcServer.channels.logs;
// Format data into an embed const embed = new this.client.embed()
const channel = ['kick', 'ban'].includes(punishment.type) ? this.client.config.mainServer.channels.bankick_log : this.client.config.mainServer.channels.logs; .setColor(this.client.config.embedColor)
const embed = new this.client.embed().setTitle(`${punishment.type[0].toUpperCase() + punishment.type.slice(1)} | Case #${punishment._id}`) .setTitle(`${punishment.type[0].toUpperCase() + punishment.type.slice(1)} | Case #${punishment.case_id}`)
.addFields( .addFields(
{name: '🔹 User', value: `<@${punishment.member}>\n\`${punishment.member}\``, inline: true}, {name: '🔹 User', value: `<@${punishment.member}>\n\`${punishment.member}\``, inline: true},
{name: '🔹 Moderator', value: `<@${punishment.moderator}>\n\`${punishment.moderator}\``, inline: true}, {name: '🔹 Moderator', value: `<@${punishment.moderator}>\n\`${punishment.moderator}\``, inline: true},
{name: '\u200b', value: '\u200b', inline: true}, {name: '\u200b', value: '\u200b', inline: true},
{name: '🔹 Reason', value: `\`${punishment.reason}\``, inline: true}) {name: '🔹 Reason', value: `\`${punishment.reason}\``, inline: true}
.setColor(this.client.config.embedColor).setTimestamp(punishment.time) ).setTimestamp(punishment.time);
if (punishment.duration) embed.addFields({name: '🔹 Duration', value: `${FormatTime(punishment.duration, 100)}`, inline: true}, {name: '\u200b', value: '\u200b', inline: true}) 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) { if (punishment.cancels) {
const cancels = await this._content.findById(punishment.cancels); const cancels = await this.model.findOne({where: {case_id: punishment.cancels}})
embed.addFields({name: '🔹 Overwrites', value: `This case overwrites Case #${cancels.id}\n\`${cancels.reason}\``}) 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 { return {
ban: 'banned', ban: 'banned',
softban: 'softbanned', softban: 'softbanned',
kick: 'kicked', kick: 'kicked',
mute: 'muted', mute: 'muted',
warn: 'warned' 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){ 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 {time, interaction} = options;
const now = Date.now(); const now = Date.now();
const guild = this.client.guilds.cache.get(this.client.config.mainServer.id) as Discord.Guild; const guild = this.client.guilds.cache.get(this.client.config.dcServer.id) as Discord.Guild;
const punData:Punishment={type, _id: await this.createId(), member:User.id, reason, moderator, time:now} 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 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() const embed = new this.client.embed()
.setColor(this.client.config.embedColor) .setColor(this.client.config.embedColor)
.setTitle(`Case #${punData._id}: ${type[0].toUpperCase()+type.slice(1)}`) .setTitle(`${type[0].toUpperCase() + type.slice(1)} | Case #${punishment.case_id}`)
.setDescription(`${User.username}\n<@${User.id}>\n(\`${User.id}\`)`) .setDescription(`${user.username}\n<@${user.id}>\n\`${user.id}\``)
.addFields({name: 'Reason', value: reason}) .addFields({name: 'Reason', value: `\`${reason}\``});
let punResult; let punishmentResult:any;
let timeInMillis; let millisecondTime:number;
let DM;
if (type == 'mute') timeInMillis = time ? ms(time) : 2419140000; // Timeouts have a limit of 4 weeks if (type === 'mute') millisecondTime = time ? ms(time) : 2419200000; // Timeouts have a maximum duration of 4 weeks (28 days)
else timeInMillis = time ? ms(time) : null; else millisecondTime = time ? ms(time) : null;
const durationText = timeInMillis ? ` for ${FormatTime(timeInMillis, 4, {longNames:true,commas:true})}` : ''; const durText = millisecondTime ? ` for ${Formatters.timeFormat(millisecondTime, 4, {longNames: true, commas: true})}` : '';
if (time) embed.addFields({name: 'Duration', value: durationText}); if (time) embed.addFields({name: 'Duration', value: durText});
if (GuildMember){ if (guildUser) {
try{ try {
DM=await GuildMember.send(`You've been ${this.getTense(type)} ${inOrFromBoolean} ${guild.name}${durationText} for \`${reason}\` (Case #${punData._id})`); await guildUser.send(`You've been ${this.getPastTense(type)} ${inOrFromBoolean} **${guild.name}**${durText}\n\`${reason}\` (Case #${punishment.case_id})`)
}catch(err){ } catch {
embed.setFooter({text: 'Failed to DM a member.'}) embed.setFooter({text: 'Unable to DM a member'})
} }
} }
if (['ban', 'softban'].includes(type)){ if (['ban', 'softban'].includes(type)) {
const banned = await guild.bans.fetch(User.id).catch(()=>undefined); const alreadyBanned = await guild.bans.fetch(user.id).catch(()=>null); // 172800 seconds is 48 hours, just for future reference
if (!banned) punResult = await guild.bans.create(User.id, {reason: auditLogReason, deleteMessageSeconds: 172800}).catch((err:Error)=>err.message) if (!alreadyBanned) punishmentResult = await guild.bans.create(user.id, {reason: auditLogReason, deleteMessageSeconds: 172800}).catch((err:Error)=>err.message);
else punResult = 'User is already banned.'; 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 == 'kick') punResult = await GuildMember?.kick(auditLogReason).catch((err:Error)=>err.message); else if (type === 'mute') punishmentResult = await guildUser?.timeout(millisecondTime, auditLogReason).catch((err:Error)=>err.message);
else if (type == 'mute') punResult = await GuildMember?.timeout(timeInMillis, auditLogReason).catch((err:Error)=>err.message);
if (type == 'softban' && typeof punResult != 'string') punResult = await guild.bans.remove(User.id, auditLogReason).catch((err:Error)=>err.message);
if (timeInMillis && ['mute','ban'].includes(type)){ if (type === 'softban' && typeof punishmentResult !== 'string') punishmentResult = await guild.bans.remove(user.id, auditLogReason).catch((err:Error)=>err.message);
punData.endTime = now + timeInMillis;
punData.duration = timeInMillis; if (millisecondTime && ['ban', 'mute'].includes(type)) {
punishment.endTime = now + millisecondTime;
punishment.duration = millisecondTime;
} }
if (typeof punResult == 'string'){// Unsuccessful punishment if (typeof punishmentResult === 'string') { // Punishment was unsuccessful
if (DM) DM.delete(); if (interaction) return interaction.editReply(punishmentResult);
if (interaction) return interaction.editReply(punResult); else return punishmentResult;
else return punResult;
} else { } else {
await this.makeModlogEntry(punData); const checkIfExists = await this.model.findOne({where: {case_id: punishment.case_id}});
await this._content.create(punData); 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]}); if (interaction) return interaction.editReply({embeds: [embed]});
else return punResult; 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 now = Date.now();
const _id = await this.createId(); const ID = await this.generateCaseId();
const punishment = await this._content.findById(caseId); const punishment = await this.model.findByPk(caseId);
if (!punishment) return 'Punishment not found.'; if (!punishment) return 'Case not found in database.';
const guild = this.client.guilds.cache.get(this.client.config.mainServer.id) as Discord.Guild; const guild = this.client.guilds.cache.get(this.client.config.dcServer.id) as Discord.Guild;
const auditLogReason = `${reason || 'Reason unspecified'} | Case #${punishment.id}`; const auditLogReason = `${reason ?? 'Reason unspecified'} | Case #${ID}`;
const User = await this.client.users.fetch(punishment.member); const user = await this.client.users.fetch(punishment.member);
const GuildMember = await guild.members.fetch(punishment.member).catch(()=>null); 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 removePunishmentData:Punishment = {type: `un${punishment.type}`, case_id: ID, cancels: punishment.case_id, member: punishment.member, reason, moderator, time: now};
let removePunishmentResult; let removePunishmentResult:any;
if (punishment.type == 'ban') removePunishmentResult = guild.bans.remove(punishment.member, auditLogReason).catch((err:Error)=>err.message); if (punishment.type === 'ban') removePunishmentResult = await guild.bans.remove(punishment.member, auditLogReason).catch((err:Error)=>err.message);
else if (punishment.type == 'mute'){ else if (punishment.type === 'mute') {
if (GuildMember){ if (guildUser) {
removePunishmentResult = GuildMember.timeout(null, auditLogReason).catch((err:Error)=>err.message); removePunishmentResult = await guildUser.timeout(null, auditLogReason).catch((err:Error)=>err.message);
GuildMember.send(`You've been unmuted in ${guild.name}.`).catch((err:Error)=>console.log(err.message)); guildUser.send(`You've been unmuted in **${guild.name}**.`).catch(()=>null);
} else await this._content.findByIdAndUpdate(caseId,{expired:true},{new:true}); } else this.model.update({expired: true}, {where: {case_id: caseId}});
} else removePunishmentData.type = 'punishmentOverride'; } else removePunishmentData.type = 'punishmentOverride';
if (typeof removePunishmentResult == 'string'){//Unsuccessful punishment if (typeof removePunishmentResult === 'string') {// Punishment was unsuccessful
if (interaction) return interaction.reply(removePunishmentResult); if (interaction) return interaction.editReply(removePunishmentResult);
else return removePunishmentResult; else return removePunishmentResult;
} else { } else {
await this._content.findByIdAndUpdate(caseId,{expired:true},{new:true}); this.model.update({expired: true}, {where: {case_id: caseId}}).then(()=>
await this._content.create(removePunishmentData); this.model.create({
await this.makeModlogEntry(removePunishmentData); 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) if (interaction) return interaction.reply({embeds: [new this.client.embed()
.setTitle(`Case #${removePunishmentData._id}: ${removePunishmentData.type[0].toUpperCase()+removePunishmentData.type.slice(1)}`) .setColor(this.client.config.embedColor)
.setDescription(`${User.username}\n<@${User.id}>\n(\`${User.id}\`)`) .setTitle(`${removePunishmentData.type[0].toUpperCase() + removePunishmentData.type.slice(1)} | Case #${removePunishmentData.case_id}`)
.addFields({name: 'Reason', value: reason},{name: 'Overwrites', value: `Case #${punishment.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 TClient from '../client.js';
import mongoose from 'mongoose'; import MessageTool from '../helpers/MessageTool.js';
import CacheServer from '../funcs/CacheServer.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({ class tagsystem extends Model {
_id: {type: String, required:true}, declare public tagname: string;
message: {type: String, required:true}, declare public message: string;
embedBool: {type: Boolean, required:true}, declare public embedFlag: boolean;
user: {required:true, type: new mongoose.Schema({ declare public userid: string;
name: {type: String, required:true}, }
_id: {type: String, required:true}
}, {versionKey: false})}
}, {versionKey: false}));
export default class tags extends Schema { interface Tags {
client: TClient; tagname: string;
_content: typeof Schema; message: string;
constructor(client:TClient){ embedFlag: boolean;
super(); userid: string;
this.client = client; }
this._content = Schema;
} export class TagSystemSvc {
async findInCache(): Promise<any> { private model: typeof tagsystem;
const cacheKey = 'Tags';
const cachedResult = await CacheServer.get(cacheKey); constructor() {
let result; this.model = tagsystem;
if (cachedResult) { this.model.init({
try { tagname: {
result = cachedResult; type: DataTypes.TEXT,
} catch (error) { unique: true,
console.error('Error parsing cached result:', error); allowNull: false
result = await this._content.find(); },
CacheServer.set(cacheKey, result); message: {
CacheServer.expiry(cacheKey, 240); type: DataTypes.TEXT,
allowNull: false
},
embedFlag: {
type: DataTypes.BOOLEAN,
allowNull: false
},
userid: {
type: DataTypes.STRING,
allowNull: false
} }
} else { }, {
result = await this._content.find(); tableName: 'tags',
CacheServer.set(cacheKey, result); createdAt: false,
CacheServer.expiry(cacheKey, 240); 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; 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 Discord from 'discord.js';
import TClient from '../client.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 cron from 'node-cron';
import {writeFileSync, readFileSync} from 'node:fs';
import Logger from '../helpers/Logger.js'; import Logger from '../helpers/Logger.js';
const Schema = mongoose.model('userLevels', new mongoose.Schema({ class userLevels extends Model {
_id: {type: String}, declare public id: string;
messages: {type: Number, required: true}, declare public messages: number;
level: {type: Number, required: true}, declare public level: number;
notificationPing: {type: Boolean} declare public pingToggle: boolean;
}, {versionKey: false})); }
export default class userLevels extends Schema { export class UserLevelsSvc {
client: TClient; private client: TClient;
_content: typeof Schema; private model: typeof userLevels;
constructor(client:TClient){
super(); constructor(client:TClient) {
this.client = client; 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(){ async fetchEveryone() {
// Every 1st of January at 11:00 (Midnight in London, 11AM in Sydney) 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()=>{ cron.schedule('0 11 1 1 *', async()=>{
Logger.forwardToConsole('log', 'Cron', 'Running job "resetAllData", this is activated every 1st of January'); Logger.console('log', 'Cron', 'Running job "resetAllData", this is activated every 1st of January');
const countDataBeforeReset = await this._content.countDocuments(); const performCountBeforeReset = await this.model.count();
Logger.forwardToConsole('log', 'Cron:resetAllData', `Counted ${countDataBeforeReset.toLocaleString()} documents before reset`); Logger.console('log', 'Cron:resetAllData', `Counted ${performCountBeforeReset.toLocaleString()} documents before reset`);
await this._content.deleteMany(); await this.client.dailyMsgs.nukeDays();
Logger.forwardToConsole('log', 'Cron:resetAllData', 'Deleted all documents, now resetting dailyMsgs'); await this.model.drop().then(async()=>await this.model.sync());
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()]});
// 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(); const newEpoch = new Date().getTime();
this.client.config.LRSstart = newEpoch; this.client.config.LRSstart = newEpoch;
const logText = `Resetting LRSstart to \`${newEpoch}\`, saved to config file`; const logText = `Resetting LRSstart to \`${newEpoch}\`, saved to config file`;
Logger.forwardToConsole('log', 'DailyMsgs', logText); Logger.console('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)); (this.client.channels.resolve(this.client.config.dcServer.channels.logs) as Discord.TextChannel).send({embeds: [new this.client.embed()
writeFileSync('./src/config.json', JSON.stringify(this.client.config, null, 2)); .setColor(this.client.config.embedColorXmas).setTitle('Happy New Years! Level System is clean!')
Logger.forwardToConsole('log', 'Cron:resetAllData', 'Job completed'); .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){ algorithm = (level:number)=>level*level*18;
const userData = await this._content.findById(userid) // Algorithm for determining levels. If adjusting, recommended to only change the integer at the end of equation.
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.
} }

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