Merge branch 'restructure' into dependabot/npm_and_yarn/lodash-4.17.19

This commit is contained in:
Keanu Timmermans 2020-07-25 18:16:09 +02:00 committed by GitHub
commit d796a40d37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1759 additions and 1607 deletions

View File

@ -1,3 +0,0 @@
{
"extends": "tesseract"
}

12
.gitignore vendored
View File

@ -1,13 +1,17 @@
# Specific to this repository
dist/
data/
tmp/
test*
*.bat
desktop.ini
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
config.json
config.js
data/
test.js
# Runtime data
pids

214
LICENSE
View File

@ -1,201 +1,21 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
MIT License
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Copyright (c) 2020 Keanu Timmermans
1. Definitions.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 Keanu Timmermans, Lexi Sother
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,5 +0,0 @@
{
"prefix": "!!", // Bot Prefix
"token": "<token>", // Bot Token
"owners": ["<id>"] // Array of bot owner IDs
}

0
docs/CHANGELOG.md Normal file
View File

33
docs/Specifications.md Normal file
View File

@ -0,0 +1,33 @@
# Structure
The top-level directory is reserved for files that have to be there for it to work as well as configuration files.
- `src`: Contains all the code for the bot itself. Code in this directory is for independent tasks keeping the initialization out of the subdirectories.
- `core`: This is where core structures and critical functions for the bot go.
- `modules`: This is where modules go that accomplish one specific purpose but isn't so necessary for the bot to function. The goal is to be able to safely remove these without too much trouble.
- `commands`: Here's the place to store commands. The file name determines the command name.
- `events`: Here's the place to store events. The file name determines the event type.
- `dist`: This is where the runnable code in `src` compiles to. (The directory structure mirrors `src`.)
- `data`: Holds all the dynamic data used by the bot. This is what you modify if you want to change stuff for just your instance of the bot.
- `standard`: Contains all the standard data to be used with the project itself. It's part of the code and will not be checked for inaccuracies because it's not meant to be easily modified.
- `docs`: Used for information about the design of the project.
# Specific Files
This list starts from `src`/`dist`.
- `index`: This is the entry point of the bot. Here is where all the initialization is done, because the idea is to keep repeatable code in separate modules while having code that runs only once here designating this is **the** starting point.
- `setup`: Used for the first time the bot is loaded, walking the user through setting up the bot.
- `core/lib`: Exports a function object which lets you wrap values letting you call special functions as well as calling utility functions common to all commands.
- `core/structures`: Contains all the structures that the dynamic data read from JSON files should follow. This exports instances of these classes.
- `core/command`: Contains the class used to instantiate commands.
- `core/event`: Contains the class used to instantiate events.
- `core/storage`: Exports an object which handles everything related to files.
- `core/wrappers`: Contains classes that wrap around values and provide extra functionality.
# Design Decisions
- All top-level files (relative to `src`/`dist`) should ideally be independent, one-time use scripts. This helps separate code that just initializes once and reusable code that forms the bulk of the main program itself. That's why all the file searching and loading commands/events will be done in `index`.
- Wrapper objects were designed with the idea of letting you assign functions directly to native objects [without the baggage of actually doing so](https://developer.mozilla.org/en-US/docs/Web/JavaScript/The_performance_hazards_of__%5B%5BPrototype%5D%5D_mutation).
- `test` should be a keyword for any file not tracked by git and generally be more flexible to play around with. It should also be automatically generated during initialization in `commands` so you can have templates ready for new commands.
- The storage module should not provide an auto-write feature. This would actually end up overcomplicating things especially when code isn't fully blocking.
- I think it's much easier to make a template system within the code itself. After all, the templates only change when the code changes to use new keys or remove old ones. You'll also be able to dedicate specific classes for the task rather than attaching meta tags to arrays and objects.
- I decided to forget about implementing dynamic events. I don't think it'll work with this setup. After all, there are only so many events you can use, whereas commands can have any number of ones, more suitable for dynamic loading. The main reasons were unsecure types and no easy way to access variables like the config or client.
- I want to make attaching subcommands more flexible, so you can either add subcommands in the constructor or by using a method. However, you have to add all other properties when instantiating a command.
- All commands should have only one parameter. This parameter is meant to be flexible so you can add properties without making a laundry list of parameters. It also has convenience functions too so you don't have to import the library for each command.
- The objects in `core/structures` are initialized into a special object once and then cached into memory automatically due to an import system. This means you don't have to keep loading JSON files over and over again even without the stack storage system. Because a JSON file resolves into an object, any extra keys are removed automatically (as it isn't initialized into the data) and any property that doesn't yet exist on the JSON object will be initialized into something. You can then have specific functions like `addUser` onto objects with a specific structure.

1052
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,23 +2,30 @@
"name": "d.js-v12-bot",
"version": "0.0.1",
"description": "A Discord bot built on Discord.JS v12",
"main": "src/index",
"main": "dist/index.js",
"dependencies": {
"chalk": "^4.1.0",
"discord.js": "^12.2.0",
"inquirer": "^7.3.1"
},
"devDependencies": {
"@types/inquirer": "^6.5.0",
"@types/node": "^14.0.22",
"@types/ws": "^7.2.6",
"tsc-watch": "^4.2.9",
"typescript": "^3.9.6"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "tsc",
"watch": "tsc --watch",
"autobuild": "tsc && npm start",
"start": "node dist/index.js",
"dev": "tsc-watch --onSuccess \"node dist/index.js dev\""
},
"keywords": [
"discord.js",
"bot"
],
"author": "Keanu Timmermans",
"license": "Apache-2.0",
"dependencies": {
"discord.js": "^12.2.0",
"moment": "^2.27.0",
"ms": "^2.1.2"
},
"devDependencies": {
"eslint": "^7.0.0",
"eslint-config-tesseract": "^0.0.2"
}
"license": "MIT"
}

View File

@ -1,16 +0,0 @@
/* eslint-disable no-unused-vars */
const Command = require('./../Structures/Command.js');
module.exports = class extends Command {
constructor(...args) {
super(...args, {
aliases: ['hallo']
});
}
async run(message, args) {
message.channel.send('Hello');
}
};

View File

@ -1,49 +0,0 @@
const Command = require('../../Structures/Command');
const { MessageEmbed, version: djsversion } = require('discord.js');
const { version } = require('../../../package.json');
const { utc } = require('moment');
const os = require('os');
const ms = require('ms');
module.exports = class extends Command {
constructor(...args) {
super(...args, {
aliases: ['info', 'bot', 'botinfo'],
category: 'Information'
});
}
run(message) {
const core = os.cpus()[0];
const embed = new MessageEmbed()
.setThumbnail(this.client.user.displayAvatarURL())
.setColor(message.guild.me.displayHexColor || 'BLUE')
.addField('General', [
`** Client:** ${this.client.user.tag} (${this.client.user.id})`,
`** Commands:** ${this.client.commands.size}`,
`** Servers:** ${this.client.guilds.cache.size.toLocaleString()}`,
`** Users:** ${this.client.guilds.cache.reduce((a, b) => a + b.memberCount, 0).toLocaleString()}`,
`** Channels:** ${this.client.channels.cache.size.toLocaleString()}`,
`** Creation Date:** ${utc(this.client.user.createdTimestamp).format('Do MMMM YYYY HH:mm:ss')}`,
`** Node.JS:** ${process.version}`,
`** Version:** v${version}`,
`** Discord.JS:** ${djsversion}`,
'\u200b'
])
.addField('System', [
`** Platform:** ${process.platform}`,
`** Uptime:** ${ms(os.uptime() * 1000, { long: true })}`,
`** CPU:**`,
`\u3000 • Cores: ${os.cpus().length}`,
`\u3000 • Model: ${core.model}`,
`\u3000 • Speed: ${core.speed}MHz`,
`** Memory:**`,
`\u3000 • Total: ${this.client.utils.formatBytes(process.memoryUsage().heapTotal)}`,
`\u3000 • Used: ${this.client.utils.formatBytes(process.memoryUsage().heapTotal)}`
])
.setTimestamp();
message.channel.send(embed);
}
};

View File

@ -1,90 +0,0 @@
/* eslint-disable no-undef */
/* eslint-disable no-warning-comments */
const Command = require('../../Structures/Command');
const { MessageEmbed } = require('discord.js');
const moment = require('moment');
const filterLevels = {
DISABLED: 'Off',
MEMBERS_WITHOUT_ROLES: 'No Role',
ALL_MEMBERS: 'Everyone'
};
const verificationLevels = {
NONE: 'None',
LOW: 'Low',
MEDIUM: 'Medium',
HIGH: '(╯°□°)╯︵ ┻━┻',
VERY_HIGH: '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻'
};
const regions = {
brazil: 'Brazil',
europe: 'Europe',
hongkong: 'Hong Kong',
india: 'India',
japan: 'Japan',
russia: 'Russia',
singapore: 'Singapore',
southafrica: 'South Africa',
sydney: 'Sydney',
'us-central': 'US Central',
'us-east': 'US East',
'us-west': 'US West',
'us-south': 'US South'
};
module.exports = class extends Command {
constructor(...args) {
super(...args, {
aliases: ['server', 'guild', 'guildinfo'],
category: 'Information'
});
}
async run(message) {
const roles = message.guild.roles.cache.sort((a, b) => b.position - a.position).map(role => role.toString());
const members = message.guild.members.cache;
const channels = message.guild.channels.cache;
const emojis = message.guild.emojis.cache;
const embed = new MessageEmbed()
.setDescription(`**Guild information for __${message.guild.name}__**`)
.setColor('BLUE')
.setThumbnail(message.guild.iconURL({ dynamic: true }))
.addField('General', [
`** Name:** ${message.guild.name}`,
`** ID:** ${message.guild.id}`,
`** Owner:** ${message.guild.owner.user.tag} (${message.guild.ownerID})`,
`** Region:** ${regions[message.guild.region]}`,
`** Boost Tier:** ${message.guild.premiumTier ? `Tier ${message.guild.premiumTier}` : 'None'}`,
`** Explicit Filter:** ${filterLevels[message.guild.explicitContentFilter]}`,
`** Verification Level:** ${verificationLevels[message.guild.verificationLevel]}`,
`** Time Created:** ${moment(message.guild.createdTimestamp).format('LT')} ${moment(message.guild.createdTimestamp).format('LL')} ${moment(message.guild.createdTimestamp).fromNow()})`,
'\u200b'
])
.addField('Statistics', [
`** Role Count:** ${roles.length}`,
`** Emoji Count:** ${emojis.size}`,
`** Regular Emoji Count:** ${emojis.filter(emoji => !emoji.animated).size}`,
`** Animated Emoji Count:** ${emojis.filter(emoji => emoji.animated).size}`,
`** Member Count:** ${message.guild.memberCount}`,
`** Humans:** ${members.filter(member => !member.user.bot).size}`,
`** Bots:** ${members.filter(member => member.user.bot).size}`,
`** Text Channels:** ${channels.filter(channel => channel.type === 'text')}`,
`** Voice Channels:** ${channels.filter(channel => channel.type === 'voice')}`,
`** Boost Count:** ${message.guild.premiumSubscriptionCount || '0'}`,
`\u200b`
])
.addField('Presence', [
`** Online:** ${members.filter(member => member.presence.status === 'online').size}`,
`** Idle:** ${members.filter(member => member.presence.status === 'idle').size}`,
`** Do Not Disturb:** ${members.filter(member => member.presence.stats === 'dnd').size}`,
`** Offline:** ${members.filter(member => member.presence.status === 'offline').size}`,
'\u200b'
])
.addField(`Roles [${roles.length - 1}]`, roles.length < 10 ? roles.join(', ') : roles.length > 10 ? this.client.utils.trimArray(roles) : 'None')
.setTimestamp();
message.channel.send(embed);
}
};

View File

@ -1,60 +0,0 @@
const Command = require('../../Structures/Command');
const { MessageEmbed } = require('discord.js');
const moment = require('moment');
const flags = {
DISCORD_EMPLOYEE: 'Discord Employee',
DISCORD_PARTNER: 'Discord Partner',
BUGHUNTER_LEVEL_1: 'Bug Hunter (Level 1)',
BUGHUNTER_LEVEL_2: 'Bug Hunter (Level 2)',
HYPESQUAD_EVENTS: 'HypeSquad Events',
HOUSE_BRAVERY: 'House of Bravery',
HOUSE_BRILLIANCE: 'House of Brilliance',
HOUSE_BALANCE: 'House of Balance',
EARLY_SUPPORTER: 'Early Supporter',
TEAM_USER: 'Team User',
SYSTEM: 'System',
VERIFIED_BOT: 'Verified Bot',
VERIFIED_DEVELOPER: 'Verified Bot Developer'
};
module.exports = class extends Command {
constructor(...args) {
super(...args, {
aliases: ['user', 'ui'],
category: 'Information'
});
}
async run(message, [target]) {
const member = message.mentions.members.last() || message.guild.members.cache.get(target) || message.member;
const roles = member.roles.cache
.sort((a, b) => b.position - a.position)
.map(role => role.toString())
.slice(0, -1);
const userFlags = member.user.flags.toArray();
const embed = new MessageEmbed()
.setThumbnail(member.user.displayAvatarURL({ dynamic: true, size: 512 }))
.setColor(member.displayHexColor || 'BLUE')
.addField('User', [
`** Username:** ${member.user.username}`,
`** Discriminator:** ${member.user.discriminator}`,
`** ID:** ${member.id}`,
`** Flags:** ${userFlags.length ? userFlags.map(flag => flags[flag]).join(', ') : 'None'}`,
`** Avatar:** [Link to avatar](${member.user.displayAvatarURL({ dynamic: true })})`,
`** Time Created:** ${moment(member.user.createdTimestamp).format('LT')} ${moment(member.user.createdTimestamp).format('LL')} ${moment(member.user.createdTimestamp).fromNow()}`,
`** Status:** ${member.user.presence.status}`,
`** Game:** ${member.user.presence.game || 'Not playing a game.'}`
])
.addField('Member', [
`** Highest Role:** ${member.roles.highest.id === message.guild.id ? 'None' : member.roles.highest.name}`,
`** Server Join Date:** ${moment(member.joinedAt).format('LL LTS')}`,
`** Hoist Role:** ${member.roles.hoist ? member.roles.hoist.name : 'None'}`,
`** Roles:** [${roles.length}]: ${roles.length < 10 ? roles.join(', ') : roles.length > 10 ? this.client.utils.trimArray(roles) : 'None'}`,
]);
return message.channel.send(embed);
}
};

View File

@ -1,56 +0,0 @@
const Command = require('../../Structures/Command');
const { MessageEmbed } = require('discord.js');
module.exports = class extends Command {
constructor(...args) {
super(...args, {
aliases: ['help', 'halp'],
category: 'Utilities'
});
}
async run(message, [command]) {
const embed = new MessageEmbed()
.setColor('BLUE')
.setAuthor(`${message.guild.name} Help Menu`, message.guild.iconURL({ dynamic: true }))
.setThumbnail(this.client.user.displayAvatarURL())
.setFooter(`Requested by ${message.author.username}`, message.author.displayAvatarURL({ dynamic: true }))
.setTimestamp();
if (command) {
const cmd = this.client.commands.get(command) || this.client.command.get(this.aliases.get(command));
if (!cmd) return message.channel.send(`\`${command}\` is not a valid command.`);
embed.setAuthor(`${this.client.utils.capitalise(cmd.name)} Command Help`, this.client.user.displayAvatarURL());
embed.setDescription([
`** Aliases:** ${cmd.aliases.length ? cmd.aliases.map(alias => `\`${alias}\``).join(' ') : 'No Aliases'}`,
`** Description:** ${cmd.description}`,
`** Category:** ${cmd.category}`,
`** Usage:** ${cmd.usage}`
]);
return message.channel.send(embed);
} else {
embed.setDescription([
`These are the available commands for ${message.guild.name}`,
`This bot's prefix is: ${this.client.prefix}`,
`Command Parameters: \`<>\` is a strict & \`[]\` is optional`
]);
let categories;
if (!this.client.owners.includes(message.author.id)) {
categories = this.client.utils.removeDuplicates(this.client.commands.filter(cmd => cmd.category !== 'Owner').map(cmd => cmd.category));
} else {
categories = this.client.utils.removeDuplicates(this.client.commands.map(cmd => cmd.category));
}
for (const category of categories) {
embed.addField(`**${this.client.utils.capitalise(category)}**`, this.client.commands.filter(cmd =>
cmd.category === category).map(cmd => `\`${cmd.name}\``).join(' '));
}
return message.channel.send(embed);
}
}
};

View File

@ -1,22 +0,0 @@
const Command = require('../../Structures/Command');
module.exports = class extends Command {
constructor(...args) {
super(...args, {
aliases: ['pong'],
category: 'Utilities'
});
}
async run(message) {
const msg = await message.channel.send('Pinging...');
const latency = msg.createdTimestamp - message.createdTimestamp;
const choices = ['Is this really my ping?', "Is this okay? I can't look!", "I hope it isn't bad!"];
const response = choices[Math.floor(Math.random() * choices.length)];
msg.edit(`${response} - Bot Latency: \`${latency}ms\`, API Latency: \`${Math.round(this.client.ws.ping)}ms\``);
}
};

View File

@ -1,17 +0,0 @@
const Command = require('../../Structures/Command.js');
const ms = require('ms');
module.exports = class extends Command {
constructor(...args) {
super(...args, {
aliases: ['uptime'],
category: 'Utilities'
});
}
async run(message) {
message.channel.send(`My uptime is \`${ms(this.client.uptime, { long: true })}\``);
}
};

View File

@ -1,25 +0,0 @@
const Event = require('../../Structures/Event');
module.exports = class extends Event {
async run(message) {
const mentionRegex = RegExp(`^<@!${this.client.user.id}>$`);
const mentionRegexPrefix = RegExp(`^<@!${this.client.user.id}> `);
if (!message.guild || message.author.bot) return;
if (message.content.match(mentionRegex)) message.channel.send(`My prefix for ${message.guild.name} is \`${this.client.prefix}\`.`);
const prefix = message.content.match(mentionRegexPrefix) ?
message.content.match(mentionRegexPrefix)[0] : this.client.prefix;
// eslint-disable-next-line no-unused-vars
const [cmd, ...args] = message.content.slice(prefix.length).trim().split(/ +/g);
const command = this.client.commands.get(cmd.toLowerCase()) || this.client.commands.get(this.client.aliases.get(cmd.toLowerCase()));
if (command) {
command.run(message, args);
}
}
};

View File

@ -1,19 +0,0 @@
const Event = require('../Structures/Event');
module.exports = class extends Event {
constructor(...args) {
super(...args, {
once: true
});
}
run() {
console.log([
`Logged in as ${this.client.user.tag}`,
`Loaded ${this.client.commands.size} commands.`,
`Loaded ${this.client.events.size} events.`
].join('\n'));
}
};

View File

@ -1,40 +0,0 @@
const { Client, Collection } = require('discord.js');
const Util = require('./Util.js');
module.exports = class BotClient extends Client {
constructor(options = {}) {
super({
disableMentions: 'everyone'
});
this.validate(options);
this.commands = new Collection();
this.events = new Collection();
this.aliases = new Collection();
this.utils = new Util(this);
this.owners = options.owners;
}
validate(options) {
if (typeof options !== 'object') throw new TypeError('Options should be a type of Object.');
if (!options.token) throw new Error('You must pass a token for the client.');
this.token = options.token;
if (!options.prefix) throw new Error('You must pass a prefix for the client.');
if (typeof options.prefix !== 'string') throw new TypeError('Prefix should be a type of String.');
this.prefix = options.prefix;
}
async start(token = this.token) {
this.utils.loadCommands();
this.utils.loadEvents();
super.login(token);
}
};

View File

@ -1,17 +0,0 @@
module.exports = class Command {
constructor(client, name, options = {}) {
this.client = client;
this.name = options.name || name;
this.aliases = options.aliases || [];
this.description = options.description || 'No description provided.';
this.category = options.category || 'Miscellaneous';
this.usage = `${this.client.prefix}${this.name} ${options.usage || ''}`.trim();
}
// eslint-disable-next-line no-unused-vars
async run(message, args) {
throw new Error(`Command ${this.name} doesn't provide a run method.`);
}
};

View File

@ -1,15 +0,0 @@
/* eslint-disable no-unused-vars */
module.exports = class Event {
constructor(client, name, options = {}) {
this.name = name;
this.client = client;
this.type = options.once ? 'once' : 'on';
this.emitter = (typeof options.emitter === 'string' ? this.client[options.emitter] : options.emitter) || this.client;
}
async run(...args) {
throw new Error(`The run method has not been implemented in ${this.name}`);
}
};

View File

@ -1,81 +0,0 @@
const path = require('path');
const { promisify } = require('util');
const glob = promisify(require('glob'));
const Command = require('./Command.js');
const Event = require('./Event.js');
module.exports = class Util {
constructor(client) {
this.client = client;
}
isClass(input) {
return typeof input === 'function' &&
typeof input.prototype === 'object' &&
input.toString().substring(0, 5) === 'class';
}
get directory() {
return `${path.dirname(require.main.filename)}${path.sep}`;
}
trimArray(arr, maxLen = 10) {
if (arr.length > maxLen) {
const len = arr.length - maxLen;
arr = arr.slice(0, maxLen);
arr.push(`${len} more...`);
}
return arr;
}
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${parseFloat((bytes / Math.pow(1024, i)).toFixed(2))} ${sizes[i]}`;
}
removeDuplicates(arr) {
return [...new Set(arr)];
}
capitalise(string) {
return string.split(' ').map(str => str.slice(0, 1).toUpperCase() + str.slice(1)).join(' ');
}
async loadCommands() {
return glob(`${this.directory}commands/**/*.js`).then(commands => {
for (const commandFile of commands) {
delete require.cache[commandFile];
const { name } = path.parse(commandFile);
const File = require(commandFile);
if (!this.isClass(File)) throw new TypeError(`Command ${name} doesn't export a class.`);
const command = new File(this.client, name.toLowerCase());
if (!(command instanceof Command)) throw new TypeError(`Command ${name} doesn't belong in commands.`);
this.client.commands.set(command.name, command);
if (command.aliases.length) {
for (const alias of command.aliases) {
this.client.aliases.set(alias, command.name);
}
}
}
});
}
async loadEvents() {
return glob(`${this.directory}events/**/*.js`).then(events => {
for (const eventFile of events) {
delete require.cache[eventFile];
const { name } = path.parse(eventFile);
const File = require(eventFile);
if (!this.isClass(File)) throw new TypeError(`Event ${name} doesn't export a class!`);
const event = new File(this.client, name.toLowerCase());
if (!(event instanceof Event)) throw new TypeError(`Event ${name} doesn't belong in the Events Directory.`);
this.client.events.set(event.name, event);
event.emitter[event.type](name, (...args) => event.run(...args));
}
});
}
};

107
src/commands/admin.ts Normal file
View File

@ -0,0 +1,107 @@
import Command from "../core/command";
import {CommonLibrary, logs} from "../core/lib";
import {Config, Storage} from "../core/structures";
import {Permissions} from "discord.js";
function authenticate($: CommonLibrary, customMessage = ""): boolean
{
const hasAccess = Config.mechanics.includes($.author.id) || ($.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR) || false);
if(!hasAccess)
{
if(customMessage !== "")
$.channel.send(customMessage);
else
{
$.channel.send(`${$.author.toString()}, you are not a server admin or one of the bot's mechanics. If you have access to the server files, add yourself to it manually in \`data/config.json\`. Your user ID should now be logged in the console.`);
$.debug($.author.id);
}
}
return hasAccess;
}
function getLogBuffer(type: string)
{
return {files: [{
attachment: Buffer.alloc(logs[type].length, logs[type]),
name: `${Date.now()}.${type}.log`
}]};
}
export default new Command({
description: "An all-in-one command to do admin stuff. You need to be either an admin of the server or one of the bot's mechanics to use this command.",
async run($: CommonLibrary): Promise<any>
{
const admin = $.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR) || false;
const mechanic = Config.mechanics.includes($.author.id);
let status = "";
if(admin && mechanic)
status = "a server admin and one of the bot's mechanics";
else if(admin)
status = "a server admin";
else if(mechanic)
status = "one of the bot's mechanics";
if(authenticate($))
$.channel.send(`${$.author.toString()}, you are ${status}, meaning you can use this command.`);
},
subcommands:
{
set: new Command({
description: "Set different per-guild settings for the bot.",
run: "You have to specify the option you want to set.",
subcommands:
{
prefix: new Command({
description: "Set a custom prefix for your guild. Removes your custom prefix if none is provided.",
usage: "(<prefix>)",
async run($: CommonLibrary): Promise<any>
{
if(authenticate($))
{
Storage.getGuild($.guild?.id || "N/A").prefix = null;
Storage.save();
$.channel.send(`The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.`);
}
},
any: new Command({
async run($: CommonLibrary): Promise<any>
{
if(authenticate($))
{
Storage.getGuild($.guild?.id || "N/A").prefix = $.args[0];
Storage.save();
$.channel.send(`The custom prefix for this guild is now \`${$.args[0]}\`.`);
}
}
})
})
}
}),
diag: new Command({
description: "Requests a debug log with the \"info\" verbosity level.",
async run($: CommonLibrary): Promise<any>
{
if(authenticate($))
$.channel.send(getLogBuffer("info"));
},
any: new Command({
description: `Select a verbosity to listen to. Available levels: \`[${Object.keys(logs)}]\``,
async run($: CommonLibrary): Promise<any>
{
if(authenticate($))
{
const type = $.args[0];
if(type in logs)
$.channel.send(getLogBuffer(type));
else
$.channel.send(`Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys(logs)}]\`.`);
}
}
})
})
}
});

102
src/commands/help.ts Normal file
View File

@ -0,0 +1,102 @@
import Command from "../core/command";
import {CommonLibrary} from "../core/lib";
import FileManager from "../core/storage";
const types = ["user", "number", "any"];
export default new Command({
description: "Lists all commands. If a command is specified, their arguments are listed as well.",
usage: "([command, [subcommand/type], ...])",
async run($: CommonLibrary): Promise<any>
{
const commands = await FileManager.loadCommands();
const list: string[] = [];
for(const [header, command] of commands)
if(header !== "test")
list.push(`- \`${header}\` - ${command.description}`);
const outList = list.length > 0 ? `\n${list.join('\n')}` : " None";
$.channel.send(`Legend: \`<type>\`, \`[list/of/subcommands]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\`\nCommands:${outList}`, {split: true});
},
any: new Command({
async run($: CommonLibrary): Promise<any>
{
const commands = await FileManager.loadCommands();
let header = $.args.shift();
let command = commands.get(header);
if(!command || header === "test")
$.channel.send(`No command found by the name \`${header}\`!`);
else
{
let usage = command.usage;
for(const param of $.args)
{
header += ` ${param}`;
if(/<\w+>/g.test(param))
{
const type = param.match(/\w+/g)[0];
command = command[type];
if(types.includes(type) && command?.usage)
usage = command.usage;
else
{
command = undefined;
break;
}
}
else if(command?.subcommands?.[param])
{
command = command.subcommands[param];
if(command.usage !== "")
usage = command.usage;
}
else
{
command = undefined;
break;
}
}
if(!command)
return $.channel.send(`No command found by the name \`${header}\`!`);
let append = "";
if(usage === "")
{
const list: string[] = [];
for(const subtag in command.subcommands)
{
const subcmd = command.subcommands[subtag];
const customUsage = subcmd.usage ? ` ${subcmd.usage}` : "";
list.push(`- \`${header} ${subtag}${customUsage}\` - ${subcmd.description}`);
}
for(const type of types)
{
if(command[type])
{
const cmd = command[type];
const customUsage = cmd.usage ? ` ${cmd.usage}` : "";
list.push(`- \`${header} <${type}>${customUsage}\` - ${cmd.description}`);
}
}
append = "Usages:" + (list.length > 0 ? `\n${list.join('\n')}` : " None.");
}
else
append = `Usage: \`${header} ${usage}\``;
$.channel.send(`Command: \`${header}\`\nDescription: ${command.description}\n${append}`, {split: true});
}
}
})
});

241
src/commands/money.ts Normal file
View File

@ -0,0 +1,241 @@
import Command from "../core/command";
import $, {CommonLibrary} from "../core/lib";
import {Storage} from "../core/structures";
import {User} from "discord.js";
export function getMoneyEmbed(user: User): object
{
const profile = Storage.getUser(user.id);
return {embed: {
color: 0xFFFF00,
author:
{
name: user.username,
icon_url: user.displayAvatarURL({
format: "png",
dynamic: true
})
},
fields:
[
{
name: "Balance",
value: $(profile.money).pluralise("credit", "s")
}
]
}};
}
function getSendEmbed(sender: User, receiver: User, amount: number): object
{
return {embed: {
color: 0xFFFF00,
author:
{
name: sender.username,
icon_url: sender.displayAvatarURL({
format: "png",
dynamic: true
})
},
title: "Transaction",
description: `${sender.toString()} has sent ${$(amount).pluralise("credit", "s")} to ${receiver.toString()}!`,
fields:
[
{
name: `Sender: ${sender.username}#${sender.discriminator}`,
value: $(Storage.getUser(sender.id).money).pluralise("credit", "s")
},
{
name: `Receiver: ${receiver.username}#${receiver.discriminator}`,
value: $(Storage.getUser(receiver.id).money).pluralise("credit", "s")
}
],
footer:
{
text: receiver.username,
icon_url: receiver.displayAvatarURL({
format: "png",
dynamic: true
})
}
}};
}
export default new Command({
description: "See how much money you have. Also provides other commands related to money.",
async run($: CommonLibrary): Promise<any>
{
$.channel.send(getMoneyEmbed($.author));
},
subcommands:
{
get: new Command({
description: "Pick up your daily credits. The cooldown is per user and every 22 hours to allow for some leeway.",
async run($: CommonLibrary): Promise<any>
{
const user = Storage.getUser($.author.id);
const now = Date.now();
if(user.lastReceived === -1)
{
user.money = 100;
user.lastReceived = now;
Storage.save();
$.channel.send("Here's 100 credits to get started, the price of a sandwich in Rookie Harbor.", getMoneyEmbed($.author));
}
else if(now - user.lastReceived >= 79200000)
{
user.money += 25;
user.lastReceived = now;
Storage.save();
$.channel.send("Here's your daily 25 credits.", getMoneyEmbed($.author));
}
else
$.channel.send(`It's too soon to pick up your daily credits. You have about ${((user.lastReceived + 79200000 - now) / 3600000).toFixed(1)} hours to go.`);
}
}),
send: new Command({
description: "Send money to someone.",
usage: "<user> <amount>",
run: "Who are you sending this money to?",
user: new Command({
run: "You need to enter an amount you're sending!",
number: new Command({
async run($: CommonLibrary): Promise<any>
{
const amount = Math.floor($.args[1]);
const author = $.author;
const sender = Storage.getUser(author.id);
const target = $.args[0];
const receiver = Storage.getUser(target.id);
if(amount <= 0)
return $.channel.send("You must send at least one credit!");
else if(sender.money < amount)
return $.channel.send("You don't have enough money to do that!", getMoneyEmbed(author));
else if(target.id === author.id)
return $.channel.send("You can't send money to yourself!");
else if(target.bot && process.argv[2] !== "dev")
return $.channel.send("You can't send money to a bot!");
sender.money -= amount;
receiver.money += amount;
Storage.save();
$.channel.send(getSendEmbed(author, target, amount));
}
})
}),
number: new Command({
run: "You must use the format `money send <user> <amount>`!"
}),
any: new Command({
async run($: CommonLibrary): Promise<any>
{
const last = $.args.pop();
if(!/\d+/g.test(last) && $.args.length === 0)
return $.channel.send("You need to enter an amount you're sending!");
const amount = Math.floor(last);
const author = $.author;
const sender = Storage.getUser(author.id);
if(amount <= 0)
return $.channel.send("You must send at least one credit!");
else if(sender.money < amount)
return $.channel.send("You don't have enough money to do that!", getMoneyEmbed(author));
else if(!$.guild)
return $.channel.send("You have to use this in a server if you want to send money with a username!");
const username = $.args.join(" ");
const member = (await $.guild.members.fetch({
query: username,
limit: 1
})).first();
if(!member)
return $.channel.send(`Couldn't find a user by the name of \`${username}\`! If you want to send money to someone in a different server, you have to use their user ID!`);
else if(member.user.id === author.id)
return $.channel.send("You can't send money to yourself!");
else if(member.user.bot && process.argv[2] !== "dev")
return $.channel.send("You can't send money to a bot!");
const target = member.user;
$.prompt(await $.channel.send(`Are you sure you want to send ${$(amount).pluralise("credit", "s")} to this person?\n*(This message will automatically be deleted after 10 seconds.)*`, {embed: {
color: "#ffff00",
author:
{
name: `${target.username}#${target.discriminator}`,
icon_url: target.displayAvatarURL({
format: "png",
dynamic: true
})
}
}}), $.author.id, () => {
const receiver = Storage.getUser(target.id);
sender.money -= amount;
receiver.money += amount;
Storage.save();
$.channel.send(getSendEmbed(author, target, amount));
});
}
})
}),
leaderboard: new Command({
description: "See the richest players tracked by this bot (across servers).",
async run($: CommonLibrary): Promise<any>
{
const users = Storage.users;
const ids = Object.keys(users);
ids.sort((a, b) => users[b].money - users[a].money);
const fields = [];
for(let i = 0, limit = Math.min(10, ids.length); i < limit; i++)
{
const id = ids[i];
const user = await $.client.users.fetch(id);
fields.push({
name: `#${i+1}. ${user.username}#${user.discriminator}`,
value: $(users[id].money).pluralise("credit", "s")
});
}
$.channel.send({embed: {
title: "Top 10 Richest Players",
color: "#ffff00",
fields: fields
}});
}
})
},
user: new Command({
description: "See how much money someone else has by using their user ID or pinging them.",
async run($: CommonLibrary): Promise<any>
{
$.channel.send(getMoneyEmbed($.args[0]));
}
}),
any: new Command({
description: "See how much money someone else has by using their username.",
async run($: CommonLibrary): Promise<any>
{
if($.guild)
{
const username = $.args.join(" ");
const member = (await $.guild.members.fetch({
query: username,
limit: 1
})).first();
if(member)
$.channel.send(getMoneyEmbed(member.user));
else
$.channel.send(`Couldn't find a user by the name of \`${username}\`!`);
}
}
})
});

145
src/core/command.ts Normal file
View File

@ -0,0 +1,145 @@
import {isType, parseVars, CommonLibrary} from "./lib";
// Permission levels starting from zero then increasing, allowing for numerical comparisons.
// Note: For my bot, there really isn't much purpose to doing so, as it's just one command. And plus, if you're doing stuff like moderation commands, it's probably better to make a permissions system that allows for you to separate permissions into different trees. After all, it'd be a really bad idea to allow a bot mechanic to ban users.
//enum PERMISSIONS {NONE, ADMIN, MECHANIC}
interface CommandOptions
{
description?: string;
endpoint?: boolean;
usage?: string;
//permissions?: number;
run?: Function|string;
subcommands?: {[key: string]: Command};
user?: Command;
number?: Command;
any?: Command;
}
export default class Command
{
public readonly description: string;
public readonly endpoint: boolean;
public readonly usage: string;
//public readonly permissions: number;
private run: Function|string;
public subcommands: {[key: string]: Command}|null;
public user: Command|null;
public number: Command|null;
public any: Command|null;
//public static readonly PERMISSIONS = PERMISSIONS;
[key: string]: any; // Allow for dynamic indexing. The CommandOptions interface will still prevent users from adding unused properties though.
constructor(options?: CommandOptions)
{
this.description = options?.description || "No description.";
this.endpoint = options?.endpoint || false;
this.usage = options?.usage || "";
//this.permissions = options?.permissions || Command.PERMISSIONS.NONE;
this.run = options?.run || "No action was set on this command!";
this.subcommands = options?.subcommands || null;
this.user = options?.user || null;
this.number = options?.number || null;
this.any = options?.any || null;
}
public execute($: CommonLibrary)
{
if(isType(this.run, String))
{
$.channel.send(parseVars(this.run as string, {
author: $.author.toString()
}, "???"));
}
else
(this.run as Function)($).catch($.handler.bind($));
}
/**
* Set what happens when the command is called.
* - If the command is a function, run it with one argument (the common library).
* - If the command is a string, it'll be sent as a message with %variables% replaced.
*/
public set(run: Function|string)
{
this.run = run;
}
/** The safe way to attach a named subcommand. */
public attach(key: string, command: Command)
{
if(!this.subcommands)
this.subcommands = {};
this.subcommands[key] = command;
}
/** See if a subcommand exists for the command. */
/*public has(type: string): boolean
{
return this.subcommands && (type in this.subcommands) || false;
}*/
/** Get the requested subcommand if it exists. */
/*public get(type: string): Command|null
{
return this.subcommands && this.subcommands[type] || null;
}*/
}
/*export function hasPermission(member: GuildMember, permission: number): boolean
{
const length = Object.keys(PERMISSIONS).length / 2;
console.log(member, permission, length);
return true;
}*/
// The template should be built with a reductionist mentality.
// Provide everything the user needs and then let them remove whatever they want.
// That way, they aren't focusing on what's missing, but rather what they need for their command.
export const template =
`import Command from '../core/command';
import {CommonLibrary} from '../core/lib';
export default new Command({
description: "This is a template/testing command providing common functionality. Remove what you don't need, and rename/delete this file to generate a fresh command file here. This command should be automatically excluded from the help command. The \\"usage\\" parameter (string) overrides the default usage for the help command. The \\"endpoint\\" parameter (boolean) prevents further arguments from being passed. Also, as long as you keep the run function async, it'll return a promise allowing the program to automatically catch any synchronous errors. However, you'll have to do manual error handling if you go the then and catch route.",
endpoint: false,
usage: '',
async run($: CommonLibrary): Promise<any> {
},
subcommands: {
layer: new Command({
description: "This is a named subcommand, meaning that the key name is what determines the keyword to use. With default settings for example, \\"$test layer\\".",
endpoint: false,
usage: '',
async run($: CommonLibrary): Promise<any> {
}
})
},
user: new Command({
description: "This is the subcommand for getting users by pinging them or copying their ID. With default settings for example, \\"$test 237359961842253835\\". The argument will be a user object and won't run if no user is found by that ID.",
endpoint: false,
usage: '',
async run($: CommonLibrary): Promise<any> {
}
}),
number: new Command({
description: "This is a numeric subcommand, meaning that any type of number (excluding Infinity/NaN) will route to this command if present. With default settings for example, \\"$test -5.2\\". The argument with the number is already parsed so you can just use it without converting it.",
endpoint: false,
usage: '',
async run($: CommonLibrary): Promise<any> {
}
}),
any: new Command({
description: "This is a generic subcommand, meaning that if there isn't a more specific subcommand that's called, it falls to this. With default settings for example, \\"$test reeee\\".",
endpoint: false,
usage: '',
async run($: CommonLibrary): Promise<any> {
}
})
});`;

31
src/core/event.ts Normal file
View File

@ -0,0 +1,31 @@
import {Client} from "discord.js";
// Last Updated: Discord.js v12.2.0
export const EVENTS = ["channelCreate", "channelDelete", "channelPinsUpdate", "channelUpdate", "debug", "warn", "disconnect", "emojiCreate", "emojiDelete", "emojiUpdate", "error", "guildBanAdd", "guildBanRemove", "guildCreate", "guildDelete", "guildUnavailable", "guildIntegrationsUpdate", "guildMemberAdd", "guildMemberAvailable", "guildMemberRemove", "guildMembersChunk", "guildMemberSpeaking", "guildMemberUpdate", "guildUpdate", "inviteCreate", "inviteDelete", "message", "messageDelete", "messageReactionRemoveAll", "messageReactionRemoveEmoji", "messageDeleteBulk", "messageReactionAdd", "messageReactionRemove", "messageUpdate", "presenceUpdate", "rateLimit", "ready", "invalidated", "roleCreate", "roleDelete", "roleUpdate", "typingStart", "userUpdate", "voiceStateUpdate", "webhookUpdate", "shardDisconnect", "shardError", "shardReady", "shardReconnecting", "shardResume"];
interface EventOptions
{
readonly on?: Function;
readonly once?: Function;
}
export default class Event
{
private readonly on: Function|null;
private readonly once: Function|null;
constructor(options: EventOptions)
{
this.on = options.on || null;
this.once = options.once || null;
}
// For this function, I'm going to assume that the event is used with the correct arguments and that the event tag is checked in "storage.ts".
public attach(client: Client, event: string)
{
if(this.on)
client.on(event as any, this.on as any);
if(this.once)
client.once(event as any, this.once as any);
}
}

372
src/core/lib.ts Normal file
View File

@ -0,0 +1,372 @@
import {GenericWrapper, NumberWrapper, ArrayWrapper} from "./wrappers";
import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember} from "discord.js";
import chalk from "chalk";
import FileManager from "./storage";
import {eventListeners} from "../events/messageReactionRemove";
/** A type that describes what the library module does. */
export interface CommonLibrary
{
// Wrapper Object //
/** Wraps the value you enter with an object that provides extra functionality and provides common utility functions. */
(value: number): NumberWrapper;
<T>(value: T[]): ArrayWrapper<T>;
<T>(value: T): GenericWrapper<T>;
// Common Library Functions //
/** <Promise>.catch($.handler.bind($)) or <Promise>.catch(error => $.handler(error)) */
handler: (error: Error) => void;
log: (...args: any[]) => void;
warn: (...args: any[]) => void;
error: (...args: any[]) => void;
debug: (...args: any[]) => void;
ready: (...args: any[]) => void;
paginate: (message: Message, senderID: string, total: number, callback: (page: number) => void, duration?: number) => void;
prompt: (message: Message, senderID: string, onConfirm: () => void, duration?: number) => void;
// Dynamic Properties //
args: any[];
client: Client;
message: Message;
channel: TextChannel|DMChannel|NewsChannel;
guild: Guild|null;
author: User;
member: GuildMember|null;
}
export default function $(value: number): NumberWrapper;
export default function $<T>(value: T[]): ArrayWrapper<T>;
export default function $<T>(value: T): GenericWrapper<T>;
export default function $(value: any)
{
if(isType(value, Number))
return new NumberWrapper(value);
else if(isType(value, Array))
return new ArrayWrapper(value);
else
return new GenericWrapper(value);
}
// If you use promises, use this function to display the error in chat.
// Case #1: await $.channel.send(""); --> Automatically caught by Command.execute().
// Case #2: $.channel.send("").catch($.handler.bind($)); --> Manually caught by the user.
$.handler = function(this: CommonLibrary, error: Error)
{
if(this)
this.channel.send(`There was an error while trying to execute that command!\`\`\`${error.stack ?? error}\`\`\``);
else
$.warn("No context was attached to $.handler! Make sure to use .catch($.handler.bind($)) or .catch(error => $.handler(error)) instead!");
$.error(error);
};
// Logs with different levels of verbosity.
export const logs: {[type: string]: string} = {
error: "",
warn: "",
info: "",
verbose: ""
};
let enabled = true;
export function setConsoleActivated(activated: boolean) {enabled = activated};
// The custom console. In order of verbosity, error, warn, log, and debug. Ready is a variation of log.
// General Purpose Logger
$.log = (...args: any[]) => {
if(enabled)
console.log(chalk.white.bgGray(formatTimestamp()), chalk.black.bgWhite("INFO"), ...args);
const text = `[${formatUTCTimestamp()}] [INFO] ${args.join(" ")}\n`;
logs.info += text;
logs.verbose += text;
};
// "It'll still work, but you should really check up on this."
$.warn = (...args: any[]) => {
if(enabled)
console.warn(chalk.white.bgGray(formatTimestamp()), chalk.black.bgYellow("WARN"), ...args);
const text = `[${formatUTCTimestamp()}] [WARN] ${args.join(" ")}\n`;
logs.warn += text;
logs.info += text;
logs.verbose += text;
};
// Used for anything which prevents the program from actually running.
$.error = (...args: any[]) => {
if(enabled)
console.error(chalk.white.bgGray(formatTimestamp()), chalk.white.bgRed("ERROR"), ...args);
const text = `[${formatUTCTimestamp()}] [ERROR] ${args.join(" ")}\n`;
logs.error += text;
logs.warn += text;
logs.info += text;
logs.verbose += text;
};
// Be as verbose as possible. If anything might help when debugging an error, then include it. This only shows in your console if you run this with "dev", but you can still get it from "logs.verbose".
$.debug = (...args: any[]) => {
if(process.argv[2] === "dev" && enabled)
console.debug(chalk.white.bgGray(formatTimestamp()), chalk.white.bgBlue("DEBUG"), ...args);
const text = `[${formatUTCTimestamp()}] [DEBUG] ${args.join(" ")}\n`;
logs.verbose += text;
};
// Used once at the start of the program when the bot loads.
$.ready = (...args: any[]) => {
if(enabled)
console.log(chalk.white.bgGray(formatTimestamp()), chalk.black.bgGreen("READY"), ...args);
const text = `[${formatUTCTimestamp()}] [READY] ${args.join(" ")}\n`;
logs.info += text;
logs.verbose += text;
};
export function formatTimestamp(now = new Date())
{
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const hour = now.getHours().toString().padStart(2, '0');
const minute = now.getMinutes().toString().padStart(2, '0');
const second = now.getSeconds().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
export function formatUTCTimestamp(now = new Date())
{
const year = now.getUTCFullYear();
const month = (now.getUTCMonth() + 1).toString().padStart(2, '0');
const day = now.getUTCDate().toString().padStart(2, '0');
const hour = now.getUTCHours().toString().padStart(2, '0');
const minute = now.getUTCMinutes().toString().padStart(2, '0');
const second = now.getUTCSeconds().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
// Pagination function that allows for customization via a callback.
// Define your own pages outside the function because this only manages the actual turning of pages.
$.paginate = async(message: Message, senderID: string, total: number, callback: (page: number) => void, duration = 60000) => {
let page = 0;
const turn = (amount: number) => {
page += amount;
if(page < 0)
page += total;
else if(page >= total)
page -= total;
callback(page);
}
const handle = (emote: string, reacterID: string) => {
if(reacterID === senderID)
{
switch(emote)
{
case '⬅️': turn(-1); break;
case '➡️': turn(1); break;
}
}
};
// Listen for reactions and call the handler.
await message.react('⬅️');
await message.react('➡️');
eventListeners.set(message.id, handle);
await message.awaitReactions((reaction, user) => {
handle(reaction.emoji.name, user.id);
return false;
}, {time: duration});
// When time's up, remove the bot's own reactions.
eventListeners.delete(message.id);
message.reactions.cache.get('⬅️')?.users.remove(message.author);
message.reactions.cache.get('➡️')?.users.remove(message.author);
};
// Waits for the sender to either confirm an action or let it pass (and delete the message).
$.prompt = async(message: Message, senderID: string, onConfirm: () => void, duration = 10000) => {
let isDeleted = false;
message.react('✅');
await message.awaitReactions((reaction, user) => {
if(user.id === senderID)
{
if(reaction.emoji.name === '✅')
onConfirm();
isDeleted = true;
message.delete();
}
// CollectorFilter requires a boolean to be returned.
// My guess is that the return value of awaitReactions can be altered by making a boolean filter.
// However, because that's not my concern with this command, I don't have to worry about it.
// May as well just set it to false because I'm not concerned with collecting any reactions.
return false;
}, {time: duration});
if(!isDeleted)
message.delete();
};
/**
* Splits a command by spaces while accounting for quotes which capture string arguments.
* - `\"` = `"`
* - `\\` = `\`
*/
export function parseArgs(line: string): string[]
{
let result = [];
let selection = "";
let inString = false;
let isEscaped = false;
for(let c of line)
{
if(isEscaped)
{
if(['"', '\\'].includes(c))
selection += c;
else
selection += '\\' + c;
isEscaped = false;
}
else if(c === '\\')
isEscaped = true;
else if(c === '"')
inString = !inString;
else if(c === ' ' && !inString)
{
result.push(selection);
selection = "";
}
else
selection += c;
}
if(selection.length > 0)
result.push(selection)
return result;
}
/**
* Allows you to store a template string with variable markers and parse it later.
* - Use `%name%` for variables
* - `%%` = `%`
* - If the invalid token is null/undefined, nothing is changed.
*/
export function parseVars(line: string, definitions: {[key: string]: string}, invalid: string|null|undefined = ""): string
{
let result = "";
let inVariable = false;
let token = "";
for(const c of line)
{
if(c === '%')
{
if(inVariable)
{
if(token === "")
result += '%';
else
{
if(token in definitions)
result += definitions[token];
else if(invalid === undefined || invalid === null)
result += `%${token}%`;
else
result += invalid;
token = "";
}
}
inVariable = !inVariable;
}
else if(inVariable)
token += c;
else
result += c;
}
return result;
}
/**
* Split up an array into a specified length.
* [1,2,3,4,5,6,7,8,9,10] split by 3 = [[1,2,3],[4,5,6],[7,8,9],[10]]
*/
export function perforate<T>(list: T[], lengthOfEachSection: number): T[][]
{
const sections: T[][] = [];
const amountOfSections = Math.ceil(list.length / lengthOfEachSection);
for(let index = 0; index < amountOfSections; index++)
sections.push(list.slice(index * lengthOfEachSection, (index + 1) * lengthOfEachSection));
return sections;
}
export function isType(value: any, type: Function): boolean
{
if(value === undefined && type === undefined)
return true;
else if(value === null && type === null)
return true;
else
return value !== undefined && value !== null && value.constructor === type;
}
/**
* Checks a value to see if it matches the fallback's type, otherwise returns the fallback.
* For the purposes of the templates system, this function will only check array types, objects should be checked under their own type (as you'd do anyway with something like a User object).
* If at any point the value doesn't match the data structure provided, the fallback is returned.
* Warning: Type checking is based on the fallback's type. Be sure that the "type" parameter is accurate to this!
*/
export function select<T>(value: any, fallback: T, type: Function, isArray = false): T
{
if(isArray && isType(value, Array))
{
for(let item of value)
if(!isType(item, type))
return fallback;
return value;
}
else
{
if(isType(value, type))
return value;
else
return fallback;
}
}
export interface GenericJSON
{
[key: string]: any;
}
export abstract class GenericStructure
{
protected __meta__ = "generic";
constructor(tag?: string)
{
this.__meta__ = tag || this.__meta__;
}
public save(asynchronous = true)
{
const tag = this.__meta__;
delete this.__meta__;
FileManager.write(tag, this, asynchronous);
this.__meta__ = tag;
}
}
// A 50% chance would be "Math.random() < 0.5" because Math.random() can be [0, 1), so to make two equal ranges, you'd need [0, 0.5)U[0.5, 1).
// Similar logic would follow for any other percentage. Math.random() < 1 is always true (100% chance) and Math.random() < 0 is always false (0% chance).
export const Random = {
num: (min: number, max: number) => (Math.random() * (max - min)) + min,
int: (min: number, max: number) => Math.floor(Random.num(min, max)),
chance: (decimal: number) => Math.random() < decimal,
sign: (number = 1) => number * (Random.chance(0.5) ? -1 : 1),
deviation: (base: number, deviation: number) => Random.num(base - deviation, base + deviation)
};

126
src/core/storage.ts Normal file
View File

@ -0,0 +1,126 @@
import fs from "fs";
import $ from "./lib";
import {Collection, Client} from "discord.js";
import Command, {template} from "../core/command";
import {EVENTS} from "./event";
let commands: Collection<string, Command>|null = null;
const Storage = {
read(header: string): object
{
this.open("data");
const path = `data/${header}.json`;
let data = {};
if(fs.existsSync(path))
{
const file = fs.readFileSync(path, "utf-8");
try
{
data = JSON.parse(file);
}
catch(error)
{
if(process.argv[2] !== "dev")
{
$.warn(`Malformed JSON data (header: ${header}), backing it up.`, file);
fs.writeFile(`${path}.backup`, file, generateHandler(`Backup file of "${header}" successfully written as ${file}.`));
}
}
}
return data;
},
write(header: string, data: object, asynchronous = true)
{
this.open("data");
const path = `data/${header}.json`;
if(process.argv[2] === "dev" || header === "config")
{
const result = JSON.stringify(data, null, '\t');
if(asynchronous)
fs.writeFile(path, result, generateHandler(`"${header}" sucessfully spaced and written.`));
else
fs.writeFileSync(path, result);
}
else
{
const result = JSON.stringify(data);
if(asynchronous)
fs.writeFile(path, result, generateHandler(`"${header}" sucessfully written.`));
else
fs.writeFileSync(path, result);
}
},
open(path: string, filter?: (value: string, index: number, array: string[]) => unknown): string[]
{
if(!fs.existsSync(path))
fs.mkdirSync(path);
let directory = fs.readdirSync(path);
if(filter)
directory = directory.filter(filter);
return directory;
},
close(path: string)
{
if(fs.existsSync(path) && fs.readdirSync(path).length === 0)
fs.rmdir(path, generateHandler(`"${path}" successfully closed.`));
},
/** Returns the cache of the commands if it exists and searches the directory if not. */
async loadCommands(): Promise<Collection<string, Command>>
{
if(commands)
return commands;
if(process.argv[2] === "dev" && !fs.existsSync("src/commands/test.ts"))
fs.writeFile("src/commands/test.ts", template, generateHandler('"test.ts" (testing/template command) successfully generated.'));
commands = new Collection();
for(const file of Storage.open("dist/commands", (filename: string) => filename.endsWith(".js")))
{
const header = file.substring(0, file.indexOf(".js"));
const command = (await import(`../commands/${header}`)).default;
commands.set(header, command);
$.log(`Loading Command: ${header}`);
}
return commands;
},
async loadEvents(client: Client)
{
for(const file of Storage.open("dist/events", (filename: string) => filename.endsWith(".js")))
{
const header = file.substring(0, file.indexOf(".js"));
const event = (await import(`../events/${header}`)).default;
if(EVENTS.includes(header))
{
event.attach(client, header);
$.log(`Loading Event: ${header}`);
}
else
$.warn(`"${header}" is not a valid event type! Did you misspell it? (Note: If you fixed the issue, delete "dist" because the compiler won't automatically delete any extra files.)`);
}
}
};
function generateHandler(message: string)
{
return (error: Error|null) => {
if(error)
$.error(error);
else
$.debug(message);
};
};
export default Storage;

113
src/core/structures.ts Normal file
View File

@ -0,0 +1,113 @@
import FileManager from "./storage";
import $, {select, GenericJSON, GenericStructure} from "./lib";
import {watch} from "fs";
class ConfigStructure extends GenericStructure
{
public token: string;
public prefix: string;
public mechanics: string[];
constructor(data: GenericJSON)
{
super("config");
this.token = select(data.token, "<ENTER YOUR TOKEN HERE>", String);
this.prefix = select(data.prefix, "$", String);
this.mechanics = select(data.mechanics, [], String, true);
}
}
class User
{
public money: number;
public lastReceived: number;
constructor(data?: GenericJSON)
{
this.money = select(data?.money, 0, Number);
this.lastReceived = select(data?.lastReceived, -1, Number);
}
}
class Guild
{
public prefix: string|null;
constructor(data?: GenericJSON)
{
this.prefix = select(data?.prefix, null, String);
}
}
class StorageStructure extends GenericStructure
{
public users: {[id: string]: User};
public guilds: {[id: string]: Guild};
constructor(data: GenericJSON)
{
super("storage");
this.users = {};
this.guilds = {};
for(let id in data.users)
if(/\d{17,19}/g.test(id))
this.users[id] = new User(data.users[id]);
for(let id in data.guilds)
if(/\d{17,19}/g.test(id))
this.guilds[id] = new Guild(data.guilds[id]);
}
/** Gets a user's profile if they exist and generate one if not. */
public getUser(id: string): User
{
if(!/\d{17,19}/g.test(id))
$.warn(`"${id}" is not a valid user ID! It will be erased when the data loads again.`);
if(id in this.users)
return this.users[id];
else
{
const user = new User();
this.users[id] = user;
return user;
}
}
/** Gets a guild's settings if they exist and generate one if not. */
public getGuild(id: string): Guild
{
if(!/\d{17,19}/g.test(id))
$.warn(`"${id}" is not a valid guild ID! It will be erased when the data loads again.`);
if(id in this.guilds)
return this.guilds[id];
else
{
const guild = new Guild();
this.guilds[id] = guild;
return guild;
}
}
}
// Exports instances. Don't worry, importing it from different files will load the same instance.
export let Config = new ConfigStructure(FileManager.read("config"));
export let Storage = new StorageStructure(FileManager.read("storage"));
// This part will allow the user to manually edit any JSON files they want while the program is running which'll update the program's cache.
// However, fs.watch is a buggy mess that should be avoided in production. While it helps test out stuff for development, it's not a good idea to have it running outside of development as it causes all sorts of issues.
if(process.argv[2] === "dev")
{
watch("data", (event, filename) => {
$.debug("File Watcher:", event, filename);
const header = filename.substring(0, filename.indexOf(".json"));
switch(header)
{
case "config": Config = new ConfigStructure(FileManager.read("config")); break;
case "storage": Storage = new StorageStructure(FileManager.read("storage")); break;
}
});
}

51
src/core/wrappers.ts Normal file
View File

@ -0,0 +1,51 @@
export class GenericWrapper<T>
{
protected readonly value: T;
public constructor(value: T)
{
this.value = value;
}
}
export class NumberWrapper extends GenericWrapper<number>
{
/**
* Pluralises a word and chooses a suffix attached to the root provided.
* - pluralise("credit", "s") = credit/credits
* - pluralise("part", "ies", "y") = party/parties
* - pluralise("sheep") = sheep
*/
public pluralise(word: string, plural = "", singular = "", excludeNumber = false): string
{
let result = excludeNumber ? "" : `${this.value} `;
if(this.value === 1)
result += word + singular;
else
result += word + plural;
return result;
}
/**
* Pluralises a word for changes.
* - (-1).pluraliseSigned() = '-1 credits'
* - (0).pluraliseSigned() = '+0 credits'
* - (1).pluraliseSigned() = '+1 credit'
*/
public pluraliseSigned(word: string, plural = "", singular = "", excludeNumber = false): string
{
const sign = this.value >= 0 ? '+' : '';
return `${sign}${this.pluralise(word, plural, singular, excludeNumber)}`;
}
}
export class ArrayWrapper<T> extends GenericWrapper<T[]>
{
/** Returns a random element from this array. */
public random(): T
{
return this.value[Math.floor(Math.random() * this.value.length)];
}
}

103
src/events/message.ts Normal file
View File

@ -0,0 +1,103 @@
import Event from "../core/event";
import Command from "../core/command";
import $ from "../core/lib";
import {Message, Permissions, Collection} from "discord.js";
import FileManager from "../core/storage";
import {Config, Storage} from "../core/structures";
// It's a rather hacky solution, but since there's no top-level await, I just have to make the loading conditional.
let commands: Collection<string, Command>|null = null;
export default new Event({
async on(message: Message)
{
// Load commands if it hasn't already done so. Luckily, it's called once at most.
if(!commands)
commands = await FileManager.loadCommands();
// Message Setup //
if(message.author.bot)
return;
const prefix = Storage.getGuild(message.guild?.id || "N/A").prefix || Config.prefix;
if(!message.content.startsWith(prefix))
return;
const [header, ...args] = message.content.substring(prefix.length).split(/ +/);
if(!commands.has(header))
return;
if(message.channel.type === "text" && !message.channel.permissionsFor(message.client.user || "")?.has(Permissions.FLAGS.SEND_MESSAGES))
{
let status;
if(message.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR))
status = "Because you're a server admin, you have the ability to change that channel's permissions to match if that's what you intended.";
else
status = "Try using a different channel or contacting a server admin to change permissions of that channel if you think something's wrong.";
return message.author.send(`I don't have permission to send messages in ${message.channel.toString()}. ${status}`);
}
$.log(`${message.author.username}#${message.author.discriminator} executed the command "${header}" with arguments "${args}".`);
// Subcommand Recursion //
let command = commands.get(header);
if(!command) return $.warn(`Command "${header}" was called but for some reason it's still undefined!`);
const params: any[] = [];
let isEndpoint = false;
for(let param of args)
{
if(command.endpoint)
{
if(command.subcommands || command.user || command.number || command.any)
$.warn(`An endpoint cannot have subcommands! Check ${prefix}${header} again.`);
isEndpoint = true;
break;
}
if(command.subcommands?.[param])
command = command.subcommands[param];
// Any Discord ID format will automatically format to a user ID.
else if(command.user && (/\d{17,19}/.test(param)))
{
const id = param.match(/\d+/g)![0];
command = command.user;
try {params.push(await message.client.users.fetch(id))}
catch(error) {return message.channel.send(`No user found by the ID \`${id}\`!`)}
}
// Disallow infinity and allow for 0.
else if(command.number && (Number(param) || param === "0") && !param.includes("Infinity"))
{
command = command.number;
params.push(Number(param));
}
else if(command.any)
{
command = command.any;
params.push(param);
}
else
params.push(param);
}
if(isEndpoint)
return message.channel.send("Too many arguments!");
// Execute with dynamic library attached. //
// The purpose of using $.bind($) is to clone the function so as to not modify the original $.
// The cloned function doesn't copy the properties, so Object.assign() is used.
// Object.assign() modifies the first element and returns that, the second element applies its properties and the third element applies its own overriding the second one.
command.execute(Object.assign($.bind($), {
args: params,
author: message.author,
channel: message.channel,
client: message.client,
guild: message.guild,
member: message.member,
message: message
}, $));
}
});

View File

@ -0,0 +1,14 @@
import Event from "../core/event";
import {MessageReaction, User, PartialUser} from "discord.js";
// A list of message ID and callback pairs. You get the emote name and ID of the user reacting.
export const eventListeners: Map<string, (emote: string, id: string) => void> = new Map();
// Attached to the client, there can be one event listener attached to a message ID which is executed if present.
export default new Event({
on(reaction: MessageReaction, user: User|PartialUser)
{
const callback = eventListeners.get(reaction.message.id);
callback && callback(reaction.emoji.name, user.id);
}
});

18
src/events/ready.ts Normal file
View File

@ -0,0 +1,18 @@
import Event from "../core/event";
import {client} from "../index";
import $ from "../core/lib";
import {Config} from "../core/structures";
export default new Event({
once()
{
if(client.user)
{
$.ready(`Logged in as ${client.user.username}#${client.user.discriminator}.`);
client.user.setActivity({
type: "LISTENING",
name: `${Config.prefix}help`
});
}
}
});

View File

@ -1,5 +0,0 @@
const BotClient = require('./Structures/BotClient');
const config = require('../config.json');
const client = new BotClient(config);
client.start();

15
src/index.ts Normal file
View File

@ -0,0 +1,15 @@
import {Client} from "discord.js";
import setup from "./setup";
import FileManager from "./core/storage";
import {Config} from "./core/structures";
// This is here in order to make it much less of a headache to access the client from other files.
// This of course won't actually do anything until the setup process is complete and it logs in.
export const client = new Client();
// Begin the command loading here rather than when it's needed like in the message event.
setup.init().then(() => {
FileManager.loadCommands();
FileManager.loadEvents(client);
client.login(Config.token).catch(setup.again);
});

49
src/setup.ts Normal file
View File

@ -0,0 +1,49 @@
import {existsSync as exists} from "fs";
import inquirer from "inquirer";
import Storage from "./core/storage";
import {Config} from "./core/structures";
import $, {setConsoleActivated} from "./core/lib";
// This file is called (or at least should be called) automatically as long as a config file doesn't exist yet.
// And that file won't be written until the data is successfully initialized.
const prompts = [{
type: "password",
name: "token",
message: "What's your bot's token?",
mask: true
}, {
type: "input",
name: "prefix",
message: "What do you want your bot's prefix to be?",
default: "$"
}, {
type: "input",
name: "mechanics",
message: "Enter a list of bot mechanics (by their IDs) separated by spaces."
}];
export default {
async init()
{
while(!exists("data/config.json"))
{
const answers = await inquirer.prompt(prompts);
Storage.open("data");
Config.token = answers.token as string;
Config.prefix = answers.prefix as string;
const mechanics = (answers.mechanics as string);
Config.mechanics = mechanics !== "" ? mechanics.split(" ") : [];
Config.save();
}
},
/** Prompt the user to set their token again. */
async again()
{
$.error("It seems that the token you provided is invalid.");
setConsoleActivated(false);
const answers = await inquirer.prompt(prompts.slice(0, 1));
Config.token = answers.token as string;
Config.save(false);
process.exit();
}
};

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions":
{
"rootDir": "src",
"outDir": "dist",
"target": "ES6",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"removeComments": true
}
}