mirror of
https://github.com/keanuplayz/TravBot-v3.git
synced 2024-08-15 02:33:12 +00:00
Made radical changes and setup foundation for SQLite database
This commit is contained in:
parent
69a8452574
commit
16e42be58d
22 changed files with 715 additions and 639 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,6 +8,7 @@ test*
|
||||||
!test/
|
!test/
|
||||||
*.bat
|
*.bat
|
||||||
desktop.ini
|
desktop.ini
|
||||||
|
*.db
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
- [Structure](#structure)
|
- [Structure](#structure)
|
||||||
- [Version Numbers](#version-numbers)
|
- [Version Numbers](#version-numbers)
|
||||||
|
- [Environment Variables](#environment-variables)
|
||||||
- [Utility Functions](#utility-functions)
|
- [Utility Functions](#utility-functions)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
|
|
||||||
|
@ -34,6 +35,17 @@ Because versions are assigned to batches of changes rather than single changes (
|
||||||
|
|
||||||
*Note: This system doesn't retroactively apply to TravBot-v2, which is why this version naming system won't make sense for v2's changelog.*
|
*Note: This system doesn't retroactively apply to TravBot-v2, which is why this version naming system won't make sense for v2's changelog.*
|
||||||
|
|
||||||
|
# Environment Variables
|
||||||
|
|
||||||
|
Certain variables are set via `.env` at the project root. These are for system configuration and should never change dynamically within the program, essentially read-only variables.
|
||||||
|
- `TOKEN`: Your bot's token
|
||||||
|
- `PREFIX`: Your bot's prefix
|
||||||
|
- `OWNER`: The ID of the owner
|
||||||
|
- `ADMINS`: A comma-separated (with a space) list of bot admin IDs
|
||||||
|
- `SUPPORT`: A comma-separated (with a space) list of bot support IDs
|
||||||
|
- `WOLFRAM_API_KEY`: Used for `commands/utility/calc`
|
||||||
|
- `DEV`: Enables dev mode as long as it isn't a falsy value (`DEV=1` works for example)
|
||||||
|
|
||||||
# Utility Functions
|
# Utility Functions
|
||||||
|
|
||||||
## [src/lib](../src/lib.ts) - General utility functions
|
## [src/lib](../src/lib.ts) - General utility functions
|
||||||
|
|
|
@ -52,7 +52,7 @@ Rather than have an `events` folder which contains dynamically loaded events, yo
|
||||||
```ts
|
```ts
|
||||||
import {client} from "..";
|
import {client} from "..";
|
||||||
|
|
||||||
client.on("message", (message) => {
|
client.on("messageCreate", (message) => {
|
||||||
//...
|
//...
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
1002
package-lock.json
generated
1002
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
@ -4,23 +4,20 @@
|
||||||
"description": "TravBot Discord bot.",
|
"description": "TravBot Discord bot.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"start": "node -r dotenv/config .",
|
||||||
"build": "rimraf dist && tsc --project tsconfig.prod.json && npm prune --production",
|
"build": "rimraf dist && tsc --project tsconfig.prod.json && npm prune --production",
|
||||||
"start": "node .",
|
"dev": "tsc-watch --onSuccess \"npm start\"",
|
||||||
"once": "tsc && npm start",
|
|
||||||
"dev": "tsc-watch --onSuccess \"npm run dev-instance\"",
|
|
||||||
"dev-fast": "tsc-watch --onSuccess \"node . dev\"",
|
|
||||||
"dev-instance": "rimraf dist && tsc && node . dev",
|
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"format": "prettier --write **/*",
|
"format": "prettier --write **/*",
|
||||||
"postinstall": "husky install"
|
"postinstall": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"better-sqlite3": "^7.4.5",
|
||||||
"canvas": "^2.8.0",
|
"canvas": "^2.8.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"discord.js": "^13.3.0",
|
"discord.js": "^13.3.0",
|
||||||
|
"dotenv": "^10.0.0",
|
||||||
"figlet": "^1.5.2",
|
"figlet": "^1.5.2",
|
||||||
"glob": "^7.2.0",
|
|
||||||
"inquirer": "^8.2.0",
|
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
"node-wolfram-alpha": "^1.2.5",
|
"node-wolfram-alpha": "^1.2.5",
|
||||||
|
@ -30,9 +27,8 @@
|
||||||
"weather-js": "^2.0.0"
|
"weather-js": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.4.1",
|
||||||
"@types/figlet": "^1.5.4",
|
"@types/figlet": "^1.5.4",
|
||||||
"@types/glob": "^7.2.0",
|
|
||||||
"@types/inquirer": "^8.1.3",
|
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^27.0.2",
|
||||||
"@types/mathjs": "^9.4.1",
|
"@types/mathjs": "^9.4.1",
|
||||||
"@types/ms": "^0.7.31",
|
"@types/ms": "^0.7.31",
|
||||||
|
|
|
@ -8,7 +8,7 @@ module.exports = {
|
||||||
jsxSingleQuote: false,
|
jsxSingleQuote: false,
|
||||||
trailingComma: "none",
|
trailingComma: "none",
|
||||||
bracketSpacing: false,
|
bracketSpacing: false,
|
||||||
jsxBracketSameLine: false,
|
bracketSameLine: false,
|
||||||
arrowParens: "always",
|
arrowParens: "always",
|
||||||
endOfLine: "auto" // Apparently, the GitHub repository still uses CRLF. I don't know how to force it to use LF, and until someone figures that out, I'm changing this to auto because I don't want more than one line ending commit.
|
endOfLine: "auto" // Apparently, the GitHub repository still uses CRLF. I don't know how to force it to use LF, and until someone figures that out, I'm changing this to auto because I don't want more than one line ending commit.
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ export const BetCommand = new NamedCommand({
|
||||||
|
|
||||||
// handle invalid target
|
// handle invalid target
|
||||||
if (target.id == author.id) return send("You can't bet Mons with yourself!");
|
if (target.id == author.id) return send("You can't bet Mons with yourself!");
|
||||||
else if (target.bot && process.argv[2] !== "dev") return send("You can't bet Mons with a bot!");
|
else if (target.bot && !process.env.DEV) return send("You can't bet Mons with a bot!");
|
||||||
|
|
||||||
return send("How much are you betting?");
|
return send("How much are you betting?");
|
||||||
} else return;
|
} else return;
|
||||||
|
@ -34,7 +34,7 @@ export const BetCommand = new NamedCommand({
|
||||||
|
|
||||||
// handle invalid target
|
// handle invalid target
|
||||||
if (target.id == author.id) return send("You can't bet Mons with yourself!");
|
if (target.id == author.id) return send("You can't bet Mons with yourself!");
|
||||||
else if (target.bot && process.argv[2] !== "dev") return send("You can't bet Mons with a bot!");
|
else if (target.bot && !process.env.DEV) return send("You can't bet Mons with a bot!");
|
||||||
|
|
||||||
// handle invalid amount
|
// handle invalid amount
|
||||||
if (amount <= 0) return send("You must bet at least one Mon!");
|
if (amount <= 0) return send("You must bet at least one Mon!");
|
||||||
|
@ -68,7 +68,7 @@ export const BetCommand = new NamedCommand({
|
||||||
|
|
||||||
// handle invalid target
|
// handle invalid target
|
||||||
if (target.id == author.id) return send("You can't bet Mons with yourself!");
|
if (target.id == author.id) return send("You can't bet Mons with yourself!");
|
||||||
else if (target.bot && !IS_DEV_MODE) return send("You can't bet Mons with a bot!");
|
else if (target.bot && !!process.env.DEV) return send("You can't bet Mons with a bot!");
|
||||||
|
|
||||||
// handle invalid amount
|
// handle invalid amount
|
||||||
if (amount <= 0) return send("You must bet at least one Mon!");
|
if (amount <= 0) return send("You must bet at least one Mon!");
|
||||||
|
|
|
@ -143,7 +143,7 @@ export const PayCommand = new NamedCommand({
|
||||||
embeds: [getMoneyEmbed(author, true)]
|
embeds: [getMoneyEmbed(author, true)]
|
||||||
});
|
});
|
||||||
else if (target.id === author.id) return send("You can't send Mons to yourself!");
|
else if (target.id === author.id) return send("You can't send Mons to yourself!");
|
||||||
else if (target.bot && !IS_DEV_MODE) return send("You can't send Mons to a bot!");
|
else if (target.bot && !process.env.DEV) return send("You can't send Mons to a bot!");
|
||||||
|
|
||||||
sender.money -= amount;
|
sender.money -= amount;
|
||||||
receiver.money += amount;
|
receiver.money += amount;
|
||||||
|
@ -179,7 +179,7 @@ export const PayCommand = new NamedCommand({
|
||||||
const user = await getUserByNickname(args.join(" "), guild);
|
const user = await getUserByNickname(args.join(" "), guild);
|
||||||
if (typeof user === "string") return send(user);
|
if (typeof user === "string") return send(user);
|
||||||
else if (user.id === author.id) return send("You can't send Mons to yourself!");
|
else if (user.id === author.id) return send("You can't send Mons to yourself!");
|
||||||
else if (user.bot && !IS_DEV_MODE) return send("You can't send Mons to a bot!");
|
else if (user.bot && !process.env.DEV) return send("You can't send Mons to a bot!");
|
||||||
|
|
||||||
const confirmed = await confirm(
|
const confirmed = await confirm(
|
||||||
await send({
|
await send({
|
||||||
|
|
|
@ -42,7 +42,7 @@ export const AwardCommand = new NamedCommand({
|
||||||
run: "You need to specify a user!",
|
run: "You need to specify a user!",
|
||||||
user: new Command({
|
user: new Command({
|
||||||
async run({send, author, args}) {
|
async run({send, author, args}) {
|
||||||
if (author.id === "394808963356688394" || IS_DEV_MODE) {
|
if (author.id === "394808963356688394" || process.env.DEV) {
|
||||||
const target = args[0] as User;
|
const target = args[0] as User;
|
||||||
const user = Storage.getUser(target.id);
|
const user = Storage.getUser(target.id);
|
||||||
user.money++;
|
user.money++;
|
||||||
|
@ -54,7 +54,7 @@ export const AwardCommand = new NamedCommand({
|
||||||
},
|
},
|
||||||
number: new Command({
|
number: new Command({
|
||||||
async run({send, author, args}) {
|
async run({send, author, args}) {
|
||||||
if (author.id === "394808963356688394" || IS_DEV_MODE) {
|
if (author.id === "394808963356688394" || process.env.DEV) {
|
||||||
const target = args[0] as User;
|
const target = args[0] as User;
|
||||||
const amount = Math.floor(args[1]);
|
const amount = Math.floor(args[1]);
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ export function getSendEmbed(sender: User, receiver: User, amount: number): obje
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAuthorized(guild: Guild | null, channel: TextBasedChannels): boolean {
|
export function isAuthorized(guild: Guild | null, channel: TextBasedChannels): boolean {
|
||||||
if (IS_DEV_MODE) {
|
if (process.env.DEV) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {Command, NamedCommand, getPermissionLevel, getPermissionName, CHANNEL_TYPE, RestCommand} from "onion-lasers";
|
import {Command, NamedCommand, getPermissionLevel, getPermissionName, CHANNEL_TYPE, RestCommand} from "onion-lasers";
|
||||||
import {Config, Storage} from "../../structures";
|
import {Config, Storage, getPrefix} from "../../structures";
|
||||||
import {Permissions, TextChannel, User, Role, Channel, Util} from "discord.js";
|
import {Permissions, TextChannel, User, Role, Channel, Util} from "discord.js";
|
||||||
import {logs} from "../../modules/globals";
|
import {logs} from "../../modules/logger";
|
||||||
|
|
||||||
function getLogBuffer(type: string) {
|
function getLogBuffer(type: string) {
|
||||||
return {
|
return {
|
||||||
|
@ -38,7 +38,7 @@ export default new NamedCommand({
|
||||||
Storage.getGuild(guild!.id).prefix = null;
|
Storage.getGuild(guild!.id).prefix = null;
|
||||||
Storage.save();
|
Storage.save();
|
||||||
send(
|
send(
|
||||||
`The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.`
|
`The custom prefix for this guild has been removed. My prefix is now back to \`${getPrefix()}\`.`
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
any: new Command({
|
any: new Command({
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import {NamedCommand, RestCommand} from "onion-lasers";
|
import {NamedCommand, RestCommand} from "onion-lasers";
|
||||||
import {WolframClient} from "node-wolfram-alpha";
|
import {WolframClient} from "node-wolfram-alpha";
|
||||||
import {MessageEmbed} from "discord.js";
|
import {MessageEmbed} from "discord.js";
|
||||||
import {Config} from "../../structures";
|
|
||||||
|
|
||||||
export default new NamedCommand({
|
export default new NamedCommand({
|
||||||
description: "Calculates a specified math expression.",
|
description: "Calculates a specified math expression.",
|
||||||
run: "Please provide a calculation.",
|
run: "Please provide a calculation.",
|
||||||
any: new RestCommand({
|
any: new RestCommand({
|
||||||
async run({send, combined}) {
|
async run({send, combined}) {
|
||||||
if (Config.wolfram === null) return send("There's no Wolfram token in the config.");
|
if (!process.env.WOLFRAM_API_KEY) return send("There's no Wolfram API key in `.env`.");
|
||||||
|
|
||||||
const wClient = new WolframClient(Config.wolfram);
|
const wClient = new WolframClient(process.env.WOLFRAM_API_KEY);
|
||||||
let resp;
|
let resp;
|
||||||
try {
|
try {
|
||||||
resp = await wClient.query(combined);
|
resp = await wClient.query(combined);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {MessageEmbed, version as djsversion, Guild, User, GuildMember, TextChannel, VoiceChannel} from "discord.js";
|
import {MessageEmbed, version, Guild, User, GuildMember, TextChannel, VoiceChannel} from "discord.js";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import {Command, NamedCommand, getUserByNickname, CHANNEL_TYPE, getGuildByName, RestCommand} from "onion-lasers";
|
import {Command, NamedCommand, getUserByNickname, CHANNEL_TYPE, getGuildByName, RestCommand} from "onion-lasers";
|
||||||
|
@ -68,8 +68,12 @@ export default new NamedCommand({
|
||||||
"Do MMMM YYYY HH:mm:ss"
|
"Do MMMM YYYY HH:mm:ss"
|
||||||
)}`,
|
)}`,
|
||||||
`**❯ Node.JS:** ${process.version}`,
|
`**❯ Node.JS:** ${process.version}`,
|
||||||
`**❯ Version:** v${process.env.npm_package_version}`,
|
`**❯ Version:** ${
|
||||||
`**❯ Discord.JS:** v${djsversion}`,
|
process.env.npm_package_version
|
||||||
|
? `v${process.env.npm_package_version}`
|
||||||
|
: "*Unable to fetch version, make sure to start the project via `npm start`, not `node`!*"
|
||||||
|
}`,
|
||||||
|
`**❯ Discord.JS:** v${version}`,
|
||||||
"\u200b"
|
"\u200b"
|
||||||
].join("\n")
|
].join("\n")
|
||||||
)
|
)
|
||||||
|
|
19
src/index.ts
19
src/index.ts
|
@ -1,6 +1,5 @@
|
||||||
import "./modules/globals";
|
import "./modules/logger";
|
||||||
import {Client, Permissions, Intents} from "discord.js";
|
import {Client, Permissions, Intents} from "discord.js";
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
// This is here in order to make it much less of a headache to access the client from other files.
|
// 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.
|
// This of course won't actually do anything until the setup process is complete and it logs in.
|
||||||
|
@ -17,18 +16,16 @@ export const client = new Client({
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
import {join} from "path";
|
||||||
import {launch} from "onion-lasers";
|
import {launch} from "onion-lasers";
|
||||||
import setup from "./modules/setup";
|
import {getPrefix} from "./structures";
|
||||||
import {Config, getPrefix} from "./structures";
|
|
||||||
import {toTitleCase} from "./lib";
|
import {toTitleCase} from "./lib";
|
||||||
|
|
||||||
// Send the login request to Discord's API and then load modules while waiting for it.
|
// Send the login request to Discord's API and then load modules while waiting for it.
|
||||||
setup.init().then(() => {
|
client.login(process.env.TOKEN).catch(console.error);
|
||||||
client.login(Config.token).catch(setup.again);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup the command handler.
|
// Setup the command handler.
|
||||||
launch(client, path.join(__dirname, "commands"), {
|
launch(client, join(__dirname, "commands"), {
|
||||||
getPrefix,
|
getPrefix,
|
||||||
categoryTransformer: toTitleCase,
|
categoryTransformer: toTitleCase,
|
||||||
permissionLevels: [
|
permissionLevels: [
|
||||||
|
@ -60,17 +57,17 @@ launch(client, path.join(__dirname, "commands"), {
|
||||||
{
|
{
|
||||||
// BOT_SUPPORT //
|
// BOT_SUPPORT //
|
||||||
name: "Bot Support",
|
name: "Bot Support",
|
||||||
check: (user) => Config.support.includes(user.id)
|
check: (user) => !!process.env.SUPPORT && process.env.SUPPORT.split(", ").includes(user.id)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// BOT_ADMIN //
|
// BOT_ADMIN //
|
||||||
name: "Bot Admin",
|
name: "Bot Admin",
|
||||||
check: (user) => Config.admins.includes(user.id)
|
check: (user) => !!process.env.ADMINS && process.env.ADMINS.split(", ").includes(user.id)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// BOT_OWNER //
|
// BOT_OWNER //
|
||||||
name: "Bot Owner",
|
name: "Bot Owner",
|
||||||
check: (user) => Config.owner === user.id
|
check: (user) => process.env.OWNER === user.id
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
145
src/modules/database.ts
Normal file
145
src/modules/database.ts
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import {existsSync} from "fs";
|
||||||
|
import {join} from "path";
|
||||||
|
|
||||||
|
// This section will serve as the documentation for the database, because in order to guarantee
|
||||||
|
// that a database created now will have the same structure as a database that has been migrated
|
||||||
|
// through different versions, a new database starts at version one and goes through the same
|
||||||
|
// migration process. Creating separate statements for migrations and creating a new database will
|
||||||
|
// allow for some dangerous out of sync definitions. For example, version 9 via migration might
|
||||||
|
// have a column that forgot to be dropped while version 9 via creation won't include that column,
|
||||||
|
// so when someone tries to use an INSERT statement, it'll throw an error because of discrepancies.
|
||||||
|
|
||||||
|
// -=[ Current Schema ]=-
|
||||||
|
// System: Version (INT UNIQUE)
|
||||||
|
// Users: ID, Money (INT), LastReceived (TIME), LastMonday (TIME), TimezoneOffset (INT NULLABLE), DaylightSavingsRegion (INT), EcoBetInsurance (INT)
|
||||||
|
// Guilds: ID, Prefix (TEXT NULLABLE), WelcomeType (INT), WelcomeChannel (TEXT NULLABLE), WelcomeMessage (TEXT NULLABLE), StreamingChannel (TEXT NULLABLE), HasMessageEmbeds (BOOL)
|
||||||
|
// Members: UserID, GuildID, StreamCategory (TEXT NULLABLE)
|
||||||
|
// Webhooks: ID, Token (TEXT)
|
||||||
|
// TodoLists: UserID, Timestamp (TIME), Entry (TEXT)
|
||||||
|
// StreamingRoles: GuildID, RoleID, Category (TEXT)
|
||||||
|
// DefaultChannelNames: GuildID, ChannelID, Name (TEXT)
|
||||||
|
// AutoRoles: GuildID, RoleID
|
||||||
|
|
||||||
|
// -=[ Notes ]=-
|
||||||
|
// - Unless otherwise directed above (NULLABLE), assume the "NOT NULL" constraint.
|
||||||
|
// - IDs use the "UNIQUE ON CONFLICT REPLACE" constraint to enable implicit UPSERT statements.
|
||||||
|
// - This way, you don't need to do INSERT INTO ... ON CONFLICT(...) DO UPDATE SET ...
|
||||||
|
// - For the sake of simplicity, any Discord ID will be stored and retrieved as a string.
|
||||||
|
// - Any datetime stuff (marked as TIME) will be stored as a UNIX timestamp in milliseconds (INT).
|
||||||
|
// - Booleans (marked as BOOL) will be stored as an integer, either 0 or 1 (though it just checks for 0).
|
||||||
|
|
||||||
|
// Calling migrations[2]() migrates the database from version 2 to version 3.
|
||||||
|
// NOTE: Once a migration is written, DO NOT change that migration or it'll break all future migrations.
|
||||||
|
const migrations: (() => void)[] = [
|
||||||
|
() => {
|
||||||
|
const hasLegacyData = existsSync(join("data", "config.json")) && existsSync(join("data", "storage.json"));
|
||||||
|
|
||||||
|
// Generate initial state
|
||||||
|
// Stuff like CREATE TABLE IF NOT EXISTS should be handled by the migration system.
|
||||||
|
generateSQLMigration([
|
||||||
|
`CREATE TABLE System (
|
||||||
|
Version INT NOT NULL UNIQUE
|
||||||
|
)`,
|
||||||
|
"INSERT INTO System VALUES (1)",
|
||||||
|
`CREATE TABLE Users (
|
||||||
|
ID TEXT NOT NULL UNIQUE ON CONFLICT REPLACE,
|
||||||
|
Money INT NOT NULL DEFAULT 0,
|
||||||
|
LastReceived INT NOT NULL DEFAULT -1,
|
||||||
|
LastMonday INT NOT NULL DEFAULT -1,
|
||||||
|
TimezoneOffset INT,
|
||||||
|
DaylightSavingsRegion INT NOT NULL DEFAULT 0,
|
||||||
|
EcoBetInsurance INT NOT NULL DEFAULT 0
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE Guilds (
|
||||||
|
ID TEXT NOT NULL UNIQUE ON CONFLICT REPLACE,
|
||||||
|
Prefix TEXT,
|
||||||
|
WelcomeType INT NOT NULL DEFAULT 0,
|
||||||
|
WelcomeChannel TEXT,
|
||||||
|
WelcomeMessage TEXT,
|
||||||
|
StreamingChannel TEXT,
|
||||||
|
HasMessageEmbeds INT NOT NULL CHECK(HasMessageEmbeds BETWEEN 0 AND 1) DEFAULT 1
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE Members (
|
||||||
|
UserID TEXT NOT NULL,
|
||||||
|
GuildID TEXT NOT NULL,
|
||||||
|
StreamCategory TEXT,
|
||||||
|
UNIQUE (UserID, GuildID) ON CONFLICT REPLACE
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE Webhooks (
|
||||||
|
ID TEXT NOT NULL UNIQUE ON CONFLICT REPLACE,
|
||||||
|
Token TEXT NOT NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE TodoLists (
|
||||||
|
UserID TEXT NOT NULL,
|
||||||
|
Timestamp INT NOT NULL,
|
||||||
|
Entry TEXT NOT NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE StreamingRoles (
|
||||||
|
GuildID TEXT NOT NULL,
|
||||||
|
RoleID TEXT NOT NULL UNIQUE ON CONFLICT REPLACE,
|
||||||
|
Category TEXT NOT NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE DefaultChannelNames (
|
||||||
|
GuildID TEXT NOT NULL,
|
||||||
|
ChannelID TEXT NOT NULL UNIQUE ON CONFLICT REPLACE,
|
||||||
|
Name TEXT NOT NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE AutoRoles (
|
||||||
|
GuildID TEXT NOT NULL,
|
||||||
|
RoleID TEXT NOT NULL UNIQUE ON CONFLICT REPLACE
|
||||||
|
)`
|
||||||
|
])();
|
||||||
|
|
||||||
|
// Load initial data if present
|
||||||
|
if (hasLegacyData) {
|
||||||
|
generateSQLMigration([])();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// "UPDATE System SET Version=2" when the time comes
|
||||||
|
];
|
||||||
|
|
||||||
|
const isExistingDatabase = existsSync(join("data", "main.db"));
|
||||||
|
export const db = new Database(join("data", "main.db"));
|
||||||
|
let version = -1;
|
||||||
|
|
||||||
|
// Get existing version if applicable and throw error if corrupt data.
|
||||||
|
// The data is considered corrupt if it exists and:
|
||||||
|
// - The System table doesn't exist (throws an error)
|
||||||
|
// - There isn't exactly one entry in System for Version
|
||||||
|
if (isExistingDatabase) {
|
||||||
|
try {
|
||||||
|
const {Version, Amount} = db.prepare("SELECT Version, Count(Version) AS Amount FROM System").get() as {
|
||||||
|
Version: number | null;
|
||||||
|
Amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!Version) {
|
||||||
|
console.error("No version entry in the System table.");
|
||||||
|
} else if (Amount === 1) {
|
||||||
|
version = Version;
|
||||||
|
} else {
|
||||||
|
console.error("More than one version entry in the System table.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
console.error("Invalid database, take a look at it manually.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
version = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then loop through all the versions
|
||||||
|
if (version !== -1) {
|
||||||
|
for (let v = version; v < migrations.length; v++) {
|
||||||
|
migrations[v]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSQLMigration(statements: string[]): () => void {
|
||||||
|
return () => {
|
||||||
|
for (const statement of statements) {
|
||||||
|
db.prepare(statement).run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import {client} from "../index";
|
||||||
// - "oil" will remain the same though, it's better that way (anything even remotely "oil"-related calls the image)
|
// - "oil" will remain the same though, it's better that way (anything even remotely "oil"-related calls the image)
|
||||||
// - Also uwu and owo penalties
|
// - Also uwu and owo penalties
|
||||||
|
|
||||||
client.on("message", (message) => {
|
client.on("messageCreate", (message) => {
|
||||||
if (message.content.toLowerCase().includes("remember to drink water")) {
|
if (message.content.toLowerCase().includes("remember to drink water")) {
|
||||||
message.react("🚱");
|
message.react("🚱");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var IS_DEV_MODE: boolean;
|
|
||||||
var PERMISSIONS: typeof PermissionsEnum;
|
var PERMISSIONS: typeof PermissionsEnum;
|
||||||
var BOT_VERSION: string;
|
|
||||||
|
|
||||||
interface Console {
|
interface Console {
|
||||||
ready: (...data: any[]) => void;
|
ready: (...data: any[]) => void;
|
||||||
|
@ -20,9 +18,7 @@ enum PermissionsEnum {
|
||||||
BOT_OWNER
|
BOT_OWNER
|
||||||
}
|
}
|
||||||
|
|
||||||
global.IS_DEV_MODE = process.argv[2] === "dev";
|
|
||||||
global.PERMISSIONS = PermissionsEnum;
|
global.PERMISSIONS = PermissionsEnum;
|
||||||
global.BOT_VERSION = "3.2.3";
|
|
||||||
|
|
||||||
const oldConsole = console;
|
const oldConsole = console;
|
||||||
|
|
||||||
|
@ -84,7 +80,8 @@ console = {
|
||||||
// $.debug(`core/lib::parseArgs("testing \"in progress\"") = ["testing", "in progress"]`) --> <path>/::(<object>.)<function>(<args>) = <value>
|
// $.debug(`core/lib::parseArgs("testing \"in progress\"") = ["testing", "in progress"]`) --> <path>/::(<object>.)<function>(<args>) = <value>
|
||||||
// Would probably be more suited for debugging program logic rather than function logic, which can be checked using unit tests.
|
// Would probably be more suited for debugging program logic rather than function logic, which can be checked using unit tests.
|
||||||
debug(...args: any[]) {
|
debug(...args: any[]) {
|
||||||
if (IS_DEV_MODE) oldConsole.debug(chalk.white.bgGray(formatTimestamp()), chalk.white.bgBlue("DEBUG"), ...args);
|
if (process.env.DEV)
|
||||||
|
oldConsole.debug(chalk.white.bgGray(formatTimestamp()), chalk.white.bgBlue("DEBUG"), ...args);
|
||||||
const text = `[${formatUTCTimestamp()}] [DEBUG] ${args.join(" ")}\n`;
|
const text = `[${formatUTCTimestamp()}] [DEBUG] ${args.join(" ")}\n`;
|
||||||
logs.verbose += text;
|
logs.verbose += text;
|
||||||
},
|
},
|
||||||
|
@ -96,5 +93,3 @@ console = {
|
||||||
logs.verbose += text;
|
logs.verbose += text;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Loading globals...");
|
|
|
@ -3,7 +3,7 @@ import {MessageEmbed} from "discord.js";
|
||||||
import {getPrefix} from "../structures";
|
import {getPrefix} from "../structures";
|
||||||
import {getMessageByID} from "onion-lasers";
|
import {getMessageByID} from "onion-lasers";
|
||||||
|
|
||||||
client.on("message", (message) => {
|
client.on("messageCreate", (message) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
// Only execute if the message is from a user and isn't a command.
|
// Only execute if the message is from a user and isn't a command.
|
||||||
if (message.content.startsWith(getPrefix(message.guild)) || message.author.bot) return;
|
if (message.content.startsWith(getPrefix(message.guild)) || message.author.bot) return;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {client} from "../index";
|
import {client} from "../index";
|
||||||
import {Config, Storage} from "../structures";
|
import {Storage, getPrefix} from "../structures";
|
||||||
|
|
||||||
client.once("ready", () => {
|
client.once("ready", () => {
|
||||||
if (client.user) {
|
if (client.user) {
|
||||||
|
@ -8,7 +8,7 @@ client.once("ready", () => {
|
||||||
);
|
);
|
||||||
client.user.setActivity({
|
client.user.setActivity({
|
||||||
type: "LISTENING",
|
type: "LISTENING",
|
||||||
name: `${Config.prefix}help`
|
name: `${getPrefix()}help`
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run this setup block once to restore eco bet money in case the bot went down. (And I guess search the client for those users to let them know too.)
|
// Run this setup block once to restore eco bet money in case the bot went down. (And I guess search the client for those users to let them know too.)
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
import {existsSync as exists} from "fs";
|
|
||||||
import inquirer from "inquirer";
|
|
||||||
import Storage from "./storage";
|
|
||||||
import {Config} from "../structures";
|
|
||||||
|
|
||||||
// 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: "owner",
|
|
||||||
message: "Enter the owner's user ID here."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "input",
|
|
||||||
name: "admins",
|
|
||||||
message: "Enter a list of bot admins (by their IDs) separated by spaces."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "input",
|
|
||||||
name: "support",
|
|
||||||
message: "Enter a list of bot troubleshooters (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;
|
|
||||||
Config.owner = answers.owner as string;
|
|
||||||
const admins = answers.admins as string;
|
|
||||||
Config.admins = admins !== "" ? admins.split(" ") : [];
|
|
||||||
const support = answers.support as string;
|
|
||||||
Config.support = support !== "" ? support.split(" ") : [];
|
|
||||||
Config.save(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/** Prompt the user to set their token again. */
|
|
||||||
async again() {
|
|
||||||
console.error("It seems that the token you provided is invalid.");
|
|
||||||
|
|
||||||
// Deactivate the console //
|
|
||||||
const oldConsole = console;
|
|
||||||
console = {
|
|
||||||
...oldConsole,
|
|
||||||
log() {},
|
|
||||||
warn() {},
|
|
||||||
error() {},
|
|
||||||
debug() {},
|
|
||||||
ready() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const answers = await inquirer.prompt(prompts.slice(0, 1));
|
|
||||||
Config.token = answers.token as string;
|
|
||||||
Config.save(false);
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -13,7 +13,9 @@ const Storage = {
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(file);
|
data = JSON.parse(file);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (process.argv[2] !== "dev") {
|
console.error(error, file);
|
||||||
|
|
||||||
|
if (!process.env.DEV) {
|
||||||
console.warn("[storage.read]", `Malformed JSON data (header: ${header}), backing it up.`, file);
|
console.warn("[storage.read]", `Malformed JSON data (header: ${header}), backing it up.`, file);
|
||||||
fs.writeFile(`${path}.backup`, file, (error) => {
|
fs.writeFile(`${path}.backup`, file, (error) => {
|
||||||
if (error) console.error("[storage.read]", error);
|
if (error) console.error("[storage.read]", error);
|
||||||
|
@ -30,7 +32,7 @@ const Storage = {
|
||||||
this.open("data");
|
this.open("data");
|
||||||
const path = `data/${header}.json`;
|
const path = `data/${header}.json`;
|
||||||
|
|
||||||
if (IS_DEV_MODE || header === "config") {
|
if (process.env.DEV || header === "config") {
|
||||||
const result = JSON.stringify(data, null, "\t");
|
const result = JSON.stringify(data, null, "\t");
|
||||||
|
|
||||||
if (asynchronous)
|
if (asynchronous)
|
||||||
|
|
|
@ -8,25 +8,11 @@ import {Guild as DiscordGuild, Snowflake} from "discord.js";
|
||||||
// And maybe use Collections/Maps instead of objects?
|
// And maybe use Collections/Maps instead of objects?
|
||||||
|
|
||||||
class ConfigStructure extends GenericStructure {
|
class ConfigStructure extends GenericStructure {
|
||||||
public token: string;
|
|
||||||
public prefix: string;
|
|
||||||
public owner: string;
|
|
||||||
public admins: string[];
|
|
||||||
public support: string[];
|
|
||||||
public lavalink: boolean | null;
|
|
||||||
public wolfram: string | null;
|
|
||||||
public systemLogsChannel: string | null;
|
public systemLogsChannel: string | null;
|
||||||
public webhooks: {[id: string]: string}; // id-token pairs
|
public webhooks: {[id: string]: string}; // id-token pairs
|
||||||
|
|
||||||
constructor(data: GenericJSON) {
|
constructor(data: GenericJSON) {
|
||||||
super("config");
|
super("config");
|
||||||
this.token = select(data.token, "<ENTER YOUR TOKEN HERE>", String);
|
|
||||||
this.prefix = select(data.prefix, "$", String);
|
|
||||||
this.owner = select(data.owner, "", String);
|
|
||||||
this.admins = select(data.admins, [], String, true);
|
|
||||||
this.support = select(data.support, [], String, true);
|
|
||||||
this.lavalink = select(data.lavalink, null, Boolean);
|
|
||||||
this.wolfram = select(data.wolfram, null, String);
|
|
||||||
this.systemLogsChannel = select(data.systemLogsChannel, null, String);
|
this.systemLogsChannel = select(data.systemLogsChannel, null, String);
|
||||||
this.webhooks = {};
|
this.webhooks = {};
|
||||||
|
|
||||||
|
@ -211,7 +197,7 @@ 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.
|
// 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.
|
// 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 (IS_DEV_MODE) {
|
if (process.env.DEV) {
|
||||||
watch("data", (_event, filename) => {
|
watch("data", (_event, filename) => {
|
||||||
const header = filename.substring(0, filename.indexOf(".json"));
|
const header = filename.substring(0, filename.indexOf(".json"));
|
||||||
|
|
||||||
|
@ -229,19 +215,17 @@ if (IS_DEV_MODE) {
|
||||||
/**
|
/**
|
||||||
* Get the current prefix of the guild or the bot's prefix if none is found.
|
* Get the current prefix of the guild or the bot's prefix if none is found.
|
||||||
*/
|
*/
|
||||||
export function getPrefix(guild: DiscordGuild | null): string {
|
export function getPrefix(guild?: DiscordGuild | null): string {
|
||||||
let prefix = Config.prefix;
|
|
||||||
|
|
||||||
if (guild) {
|
if (guild) {
|
||||||
const possibleGuildPrefix = Storage.getGuild(guild.id).prefix;
|
const possibleGuildPrefix = Storage.getGuild(guild.id).prefix;
|
||||||
|
|
||||||
// Here, lossy comparison works in our favor because you wouldn't want an empty string to trigger the prefix.
|
// Here, lossy comparison works in our favor because you wouldn't want an empty string to trigger the prefix.
|
||||||
if (possibleGuildPrefix) {
|
if (possibleGuildPrefix) {
|
||||||
prefix = possibleGuildPrefix;
|
return possibleGuildPrefix;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return prefix;
|
return process.env.PREFIX || "$";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmoteRegistryDumpEntry {
|
export interface EmoteRegistryDumpEntry {
|
||||||
|
|
Loading…
Reference in a new issue