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…
	
	Add table
		Add a link
		
	
		Reference in a new issue