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/ | ||||
| *.bat | ||||
| desktop.ini | ||||
| *.db | ||||
| 
 | ||||
| # Logs | ||||
| logs | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ | |||
| 
 | ||||
| - [Structure](#structure) | ||||
| - [Version Numbers](#version-numbers) | ||||
| - [Environment Variables](#environment-variables) | ||||
| - [Utility Functions](#utility-functions) | ||||
| - [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.* | ||||
| 
 | ||||
| # 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 | ||||
| 
 | ||||
| ## [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 | ||||
| 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.", | ||||
|     "main": "dist/index.js", | ||||
|     "scripts": { | ||||
|         "start": "node -r dotenv/config .", | ||||
|         "build": "rimraf dist && tsc --project tsconfig.prod.json && npm prune --production", | ||||
|         "start": "node .", | ||||
|         "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", | ||||
|         "dev": "tsc-watch --onSuccess \"npm start\"", | ||||
|         "test": "jest", | ||||
|         "format": "prettier --write **/*", | ||||
|         "postinstall": "husky install" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "better-sqlite3": "^7.4.5", | ||||
|         "canvas": "^2.8.0", | ||||
|         "chalk": "^4.1.2", | ||||
|         "discord.js": "^13.3.0", | ||||
|         "dotenv": "^10.0.0", | ||||
|         "figlet": "^1.5.2", | ||||
|         "glob": "^7.2.0", | ||||
|         "inquirer": "^8.2.0", | ||||
|         "moment": "^2.29.1", | ||||
|         "ms": "^2.1.3", | ||||
|         "node-wolfram-alpha": "^1.2.5", | ||||
|  | @ -30,9 +27,8 @@ | |||
|         "weather-js": "^2.0.0" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@types/better-sqlite3": "^7.4.1", | ||||
|         "@types/figlet": "^1.5.4", | ||||
|         "@types/glob": "^7.2.0", | ||||
|         "@types/inquirer": "^8.1.3", | ||||
|         "@types/jest": "^27.0.2", | ||||
|         "@types/mathjs": "^9.4.1", | ||||
|         "@types/ms": "^0.7.31", | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ module.exports = { | |||
|     jsxSingleQuote: false, | ||||
|     trailingComma: "none", | ||||
|     bracketSpacing: false, | ||||
|     jsxBracketSameLine: false, | ||||
|     bracketSameLine: false, | ||||
|     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.
 | ||||
| }; | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ export const BetCommand = new NamedCommand({ | |||
| 
 | ||||
|                 // handle invalid target
 | ||||
|                 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?"); | ||||
|             } else return; | ||||
|  | @ -34,7 +34,7 @@ export const BetCommand = new NamedCommand({ | |||
| 
 | ||||
|                     // handle invalid target
 | ||||
|                     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
 | ||||
|                     if (amount <= 0) return send("You must bet at least one Mon!"); | ||||
|  | @ -68,7 +68,7 @@ export const BetCommand = new NamedCommand({ | |||
| 
 | ||||
|                         // handle invalid target
 | ||||
|                         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
 | ||||
|                         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)] | ||||
|                         }); | ||||
|                     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; | ||||
|                     receiver.money += amount; | ||||
|  | @ -179,7 +179,7 @@ export const PayCommand = new NamedCommand({ | |||
|                 const user = await getUserByNickname(args.join(" "), guild); | ||||
|                 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.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( | ||||
|                     await send({ | ||||
|  |  | |||
|  | @ -42,7 +42,7 @@ export const AwardCommand = new NamedCommand({ | |||
|     run: "You need to specify a user!", | ||||
|     user: new Command({ | ||||
|         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 user = Storage.getUser(target.id); | ||||
|                 user.money++; | ||||
|  | @ -54,7 +54,7 @@ export const AwardCommand = new NamedCommand({ | |||
|         }, | ||||
|         number: new Command({ | ||||
|             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 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 { | ||||
|     if (IS_DEV_MODE) { | ||||
|     if (process.env.DEV) { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| 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 {logs} from "../../modules/globals"; | ||||
| import {logs} from "../../modules/logger"; | ||||
| 
 | ||||
| function getLogBuffer(type: string) { | ||||
|     return { | ||||
|  | @ -38,7 +38,7 @@ export default new NamedCommand({ | |||
|                         Storage.getGuild(guild!.id).prefix = null; | ||||
|                         Storage.save(); | ||||
|                         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({ | ||||
|  |  | |||
|  | @ -1,16 +1,15 @@ | |||
| import {NamedCommand, RestCommand} from "onion-lasers"; | ||||
| import {WolframClient} from "node-wolfram-alpha"; | ||||
| import {MessageEmbed} from "discord.js"; | ||||
| import {Config} from "../../structures"; | ||||
| 
 | ||||
| export default new NamedCommand({ | ||||
|     description: "Calculates a specified math expression.", | ||||
|     run: "Please provide a calculation.", | ||||
|     any: new RestCommand({ | ||||
|         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; | ||||
|             try { | ||||
|                 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 os from "os"; | ||||
| 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" | ||||
|                             )}`,
 | ||||
|                             `**❯ Node.JS:** ${process.version}`, | ||||
|                             `**❯ Version:** v${process.env.npm_package_version}`, | ||||
|                             `**❯ Discord.JS:** v${djsversion}`, | ||||
|                             `**❯ Version:** ${ | ||||
|                                 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" | ||||
|                         ].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 path from "path"; | ||||
| 
 | ||||
| // 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.
 | ||||
|  | @ -17,18 +16,16 @@ export const client = new Client({ | |||
|     ] | ||||
| }); | ||||
| 
 | ||||
| import {join} from "path"; | ||||
| import {launch} from "onion-lasers"; | ||||
| import setup from "./modules/setup"; | ||||
| import {Config, getPrefix} from "./structures"; | ||||
| import {getPrefix} from "./structures"; | ||||
| import {toTitleCase} from "./lib"; | ||||
| 
 | ||||
| // Send the login request to Discord's API and then load modules while waiting for it.
 | ||||
| setup.init().then(() => { | ||||
|     client.login(Config.token).catch(setup.again); | ||||
| }); | ||||
| client.login(process.env.TOKEN).catch(console.error); | ||||
| 
 | ||||
| // Setup the command handler.
 | ||||
| launch(client, path.join(__dirname, "commands"), { | ||||
| launch(client, join(__dirname, "commands"), { | ||||
|     getPrefix, | ||||
|     categoryTransformer: toTitleCase, | ||||
|     permissionLevels: [ | ||||
|  | @ -60,17 +57,17 @@ launch(client, path.join(__dirname, "commands"), { | |||
|         { | ||||
|             // 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 //
 | ||||
|             name: "Bot Admin", | ||||
|             check: (user) => Config.admins.includes(user.id) | ||||
|             check: (user) => !!process.env.ADMINS && process.env.ADMINS.split(", ").includes(user.id) | ||||
|         }, | ||||
|         { | ||||
|             // 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)
 | ||||
| // - Also uwu and owo penalties
 | ||||
| 
 | ||||
| client.on("message", (message) => { | ||||
| client.on("messageCreate", (message) => { | ||||
|     if (message.content.toLowerCase().includes("remember to drink water")) { | ||||
|         message.react("🚱"); | ||||
|     } | ||||
|  |  | |||
|  | @ -1,9 +1,7 @@ | |||
| import chalk from "chalk"; | ||||
| 
 | ||||
| declare global { | ||||
|     var IS_DEV_MODE: boolean; | ||||
|     var PERMISSIONS: typeof PermissionsEnum; | ||||
|     var BOT_VERSION: string; | ||||
| 
 | ||||
|     interface Console { | ||||
|         ready: (...data: any[]) => void; | ||||
|  | @ -20,9 +18,7 @@ enum PermissionsEnum { | |||
|     BOT_OWNER | ||||
| } | ||||
| 
 | ||||
| global.IS_DEV_MODE = process.argv[2] === "dev"; | ||||
| global.PERMISSIONS = PermissionsEnum; | ||||
| global.BOT_VERSION = "3.2.3"; | ||||
| 
 | ||||
| const oldConsole = console; | ||||
| 
 | ||||
|  | @ -84,7 +80,8 @@ console = { | |||
|     // $.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.
 | ||||
|     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`; | ||||
|         logs.verbose += text; | ||||
|     }, | ||||
|  | @ -96,5 +93,3 @@ console = { | |||
|         logs.verbose += text; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| console.log("Loading globals..."); | ||||
|  | @ -3,7 +3,7 @@ import {MessageEmbed} from "discord.js"; | |||
| import {getPrefix} from "../structures"; | ||||
| import {getMessageByID} from "onion-lasers"; | ||||
| 
 | ||||
| client.on("message", (message) => { | ||||
| client.on("messageCreate", (message) => { | ||||
|     (async () => { | ||||
|         // 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; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import {client} from "../index"; | ||||
| import {Config, Storage} from "../structures"; | ||||
| import {Storage, getPrefix} from "../structures"; | ||||
| 
 | ||||
| client.once("ready", () => { | ||||
|     if (client.user) { | ||||
|  | @ -8,7 +8,7 @@ client.once("ready", () => { | |||
|         ); | ||||
|         client.user.setActivity({ | ||||
|             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.)
 | ||||
|  |  | |||
|  | @ -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 { | ||||
|                 data = JSON.parse(file); | ||||
|             } 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); | ||||
|                     fs.writeFile(`${path}.backup`, file, (error) => { | ||||
|                         if (error) console.error("[storage.read]", error); | ||||
|  | @ -30,7 +32,7 @@ const Storage = { | |||
|         this.open("data"); | ||||
|         const path = `data/${header}.json`; | ||||
| 
 | ||||
|         if (IS_DEV_MODE || header === "config") { | ||||
|         if (process.env.DEV || header === "config") { | ||||
|             const result = JSON.stringify(data, null, "\t"); | ||||
| 
 | ||||
|             if (asynchronous) | ||||
|  |  | |||
|  | @ -8,25 +8,11 @@ import {Guild as DiscordGuild, Snowflake} from "discord.js"; | |||
| // And maybe use Collections/Maps instead of objects?
 | ||||
| 
 | ||||
| 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 webhooks: {[id: string]: string}; // id-token pairs
 | ||||
| 
 | ||||
|     constructor(data: GenericJSON) { | ||||
|         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.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.
 | ||||
| // 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) => { | ||||
|         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. | ||||
|  */ | ||||
| export function getPrefix(guild: DiscordGuild | null): string { | ||||
|     let prefix = Config.prefix; | ||||
| 
 | ||||
| export function getPrefix(guild?: DiscordGuild | null): string { | ||||
|     if (guild) { | ||||
|         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.
 | ||||
|         if (possibleGuildPrefix) { | ||||
|             prefix = possibleGuildPrefix; | ||||
|             return possibleGuildPrefix; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return prefix; | ||||
|     return process.env.PREFIX || "$"; | ||||
| } | ||||
| 
 | ||||
| export interface EmoteRegistryDumpEntry { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue