mirror of
				https://github.com/keanuplayz/TravBot-v3.git
				synced 2024-08-15 02:33:12 +00:00 
			
		
		
		
	Updated library functions
This commit is contained in:
		
							parent
							
								
									3798c27df9
								
							
						
					
					
						commit
						c980a182f8
					
				
					 14 changed files with 479 additions and 542 deletions
				
			
		|  | @ -75,27 +75,24 @@ Because versions are assigned to batches of changes rather than single changes ( | ||||||
| ```ts | ```ts | ||||||
| const pages = ["one", "two", "three"]; | const pages = ["one", "two", "three"]; | ||||||
| 
 | 
 | ||||||
| paginate(channel, author.id, pages.length, (page) => { | paginate(send, page => { | ||||||
| 	return { | 	return {content: pages[page]}; | ||||||
| 		content: pages[page] | }, pages.length, author.id); | ||||||
| 	}; |  | ||||||
| }); |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| `prompt()` | `confirm()` | ||||||
| ```ts | ```ts | ||||||
| const msg = await channel.send('Are you sure you want to delete this?'); | const result = await confirm(await send("Are you sure you want to delete this?"), author.id); // boolean | null | ||||||
| 
 |  | ||||||
| prompt(msg, author.id, () => { |  | ||||||
| 	//... |  | ||||||
| }); |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| `callMemberByUsername()` | `askMultipleChoice()` | ||||||
| ```ts | ```ts | ||||||
| callMemberByUsername(message, args.join(" "), (member) => { | const result = await askMultipleChoice(await send("Which of the following numbers is your favorite?"), author.id, 4, 10000); // number (0 to 3) | null | ||||||
| 	channel.send(`Your nickname is ${member.nickname}.`); | ``` | ||||||
| }); | 
 | ||||||
|  | `askForReply()` | ||||||
|  | ```ts | ||||||
|  | const reply = await askForReply(await send("What is your favorite thing to do?"), author.id, 10000); // Message | null | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## [src/lib](../src/lib.ts) - General utility functions | ## [src/lib](../src/lib.ts) - General utility functions | ||||||
|  |  | ||||||
|  | @ -4,7 +4,6 @@ import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./modu | ||||||
| import {BuyCommand, ShopCommand} from "./modules/eco-shop"; | import {BuyCommand, ShopCommand} from "./modules/eco-shop"; | ||||||
| import {MondayCommand, AwardCommand} from "./modules/eco-extras"; | import {MondayCommand, AwardCommand} from "./modules/eco-extras"; | ||||||
| import {BetCommand} from "./modules/eco-bet"; | import {BetCommand} from "./modules/eco-bet"; | ||||||
| import {GuildMember} from "discord.js"; |  | ||||||
| 
 | 
 | ||||||
| export default new NamedCommand({ | export default new NamedCommand({ | ||||||
|     description: "Economy command for Monika.", |     description: "Economy command for Monika.", | ||||||
|  | @ -38,7 +37,7 @@ export default new NamedCommand({ | ||||||
|         async run({send, guild, channel, args, message, combined}) { |         async run({send, guild, channel, args, message, combined}) { | ||||||
|             if (isAuthorized(guild, channel)) { |             if (isAuthorized(guild, channel)) { | ||||||
|                 const member = await getMemberByName(guild!, combined); |                 const member = await getMemberByName(guild!, combined); | ||||||
|                 if (member instanceof GuildMember) send(getMoneyEmbed(member.user)); |                 if (typeof member !== "string") send(getMoneyEmbed(member.user)); | ||||||
|                 else send(member); |                 else send(member); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import {Command, NamedCommand, askYesOrNo} from "../../../core"; | import {Command, NamedCommand, confirm} from "../../../core"; | ||||||
| import {pluralise} from "../../../lib"; | import {pluralise} from "../../../lib"; | ||||||
| import {Storage} from "../../../structures"; | import {Storage} from "../../../structures"; | ||||||
| import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils"; | import {isAuthorized, getMoneyEmbed} from "./eco-utils"; | ||||||
| import {User} from "discord.js"; | import {User} from "discord.js"; | ||||||
| 
 | 
 | ||||||
| export const BetCommand = new NamedCommand({ | export const BetCommand = new NamedCommand({ | ||||||
|  | @ -79,88 +79,89 @@ export const BetCommand = new NamedCommand({ | ||||||
|                             return send(`Bet duration is too long, maximum duration is ${durationBounds.max}`); |                             return send(`Bet duration is too long, maximum duration is ${durationBounds.max}`); | ||||||
| 
 | 
 | ||||||
|                         // Ask target whether or not they want to take the bet.
 |                         // Ask target whether or not they want to take the bet.
 | ||||||
|                         const takeBet = await askYesOrNo( |                         const takeBet = await confirm( | ||||||
|                             await send( |                             await send( | ||||||
|                                 `<@${target.id}>, do you want to take this bet of ${pluralise(amount, "Mon", "s")}` |                                 `<@${target.id}>, do you want to take this bet of ${pluralise(amount, "Mon", "s")}` | ||||||
|                             ), |                             ), | ||||||
|                             target.id |                             target.id | ||||||
|                         ); |                         ); | ||||||
| 
 | 
 | ||||||
|                         if (takeBet) { |                         if (!takeBet) return send(`<@${target.id}> has rejected your bet, <@${author.id}>`); | ||||||
|                             // [MEDIUM PRIORITY: bet persistence to prevent losses in case of shutdown.]
 |  | ||||||
|                             // Remove amount money from both parts at the start to avoid duplication of money.
 |  | ||||||
|                             sender.money -= amount; |  | ||||||
|                             receiver.money -= amount; |  | ||||||
|                             // Very hacky solution for persistence but better than no solution, backup returns runs during the bot's setup code.
 |  | ||||||
|                             sender.ecoBetInsurance += amount; |  | ||||||
|                             receiver.ecoBetInsurance += amount; |  | ||||||
|                             Storage.save(); |  | ||||||
| 
 | 
 | ||||||
|                             // Notify both users.
 |                         // [MEDIUM PRIORITY: bet persistence to prevent losses in case of shutdown.]
 | ||||||
|                             await send( |                         // Remove amount money from both parts at the start to avoid duplication of money.
 | ||||||
|                                 `<@${target.id}> has taken <@${author.id}>'s bet, the bet amount of ${pluralise( |                         sender.money -= amount; | ||||||
|                                     amount, |                         receiver.money -= amount; | ||||||
|                                     "Mon", |                         // Very hacky solution for persistence but better than no solution, backup returns runs during the bot's setup code.
 | ||||||
|                                     "s" |                         sender.ecoBetInsurance += amount; | ||||||
|                                 )} has been deducted from each of them.` |                         receiver.ecoBetInsurance += amount; | ||||||
|  |                         Storage.save(); | ||||||
|  | 
 | ||||||
|  |                         // Notify both users.
 | ||||||
|  |                         send( | ||||||
|  |                             `<@${target.id}> has taken <@${author.id}>'s bet, the bet amount of ${pluralise( | ||||||
|  |                                 amount, | ||||||
|  |                                 "Mon", | ||||||
|  |                                 "s" | ||||||
|  |                             )} has been deducted from each of them.` | ||||||
|  |                         ); | ||||||
|  | 
 | ||||||
|  |                         // Wait for the duration of the bet.
 | ||||||
|  |                         return client.setTimeout(async () => { | ||||||
|  |                             // In debug mode, saving the storage will break the references, so you have to redeclare sender and receiver for it to actually save.
 | ||||||
|  |                             const sender = Storage.getUser(author.id); | ||||||
|  |                             const receiver = Storage.getUser(target.id); | ||||||
|  |                             // [TODO: when D.JSv13 comes out, inline reply to clean up.]
 | ||||||
|  |                             // When bet is over, give a vote to ask people their thoughts.
 | ||||||
|  |                             const voteMsg = await send( | ||||||
|  |                                 `VOTE: do you think that <@${ | ||||||
|  |                                     target.id | ||||||
|  |                                 }> has won the bet?\nhttps://discord.com/channels/${guild!.id}/${channel.id}/${
 | ||||||
|  |                                     message.id | ||||||
|  |                                 }` | ||||||
|                             ); |                             ); | ||||||
|  |                             await voteMsg.react("✅"); | ||||||
|  |                             await voteMsg.react("❌"); | ||||||
| 
 | 
 | ||||||
|                             // Wait for the duration of the bet.
 |                             // Filter reactions to only collect the pertinent ones.
 | ||||||
|                             return client.setTimeout(async () => { |                             voteMsg | ||||||
|                                 // In debug mode, saving the storage will break the references, so you have to redeclare sender and receiver for it to actually save.
 |                                 .awaitReactions( | ||||||
|                                 const sender = Storage.getUser(author.id); |                                     (reaction, user) => { | ||||||
|                                 const receiver = Storage.getUser(target.id); |                                         return ["✅", "❌"].includes(reaction.emoji.name); | ||||||
|                                 // [TODO: when D.JSv13 comes out, inline reply to clean up.]
 |                                     }, | ||||||
|                                 // When bet is over, give a vote to ask people their thoughts.
 |                                     // [Pertinence to make configurable on the fly.]
 | ||||||
|                                 const voteMsg = await send( |                                     {time: parseDuration("2m")} | ||||||
|                                     `VOTE: do you think that <@${ |                                 ) | ||||||
|                                         target.id |                                 .then((reactions) => { | ||||||
|                                     }> has won the bet?\nhttps://discord.com/channels/${guild!.id}/${channel.id}/${
 |                                     // Count votes
 | ||||||
|                                         message.id |                                     const okReaction = reactions.get("✅"); | ||||||
|                                     }` |                                     const noReaction = reactions.get("❌"); | ||||||
|                                 ); |                                     const ok = okReaction ? (okReaction.count ?? 1) - 1 : 0; | ||||||
|                                 await voteMsg.react("✅"); |                                     const no = noReaction ? (noReaction.count ?? 1) - 1 : 0; | ||||||
|                                 await voteMsg.react("❌"); |  | ||||||
| 
 | 
 | ||||||
|                                 // Filter reactions to only collect the pertinent ones.
 |                                     if (ok > no) { | ||||||
|                                 voteMsg |                                         receiver.money += amount * 2; | ||||||
|                                     .awaitReactions( |                                         send( | ||||||
|                                         (reaction, user) => { |                                             `By the people's votes, <@${target.id}> has won the bet that <@${author.id}> had sent them.` | ||||||
|                                             return ["✅", "❌"].includes(reaction.emoji.name); |                                         ); | ||||||
|                                         }, |                                     } else if (ok < no) { | ||||||
|                                         // [Pertinence to make configurable on the fly.]
 |                                         sender.money += amount * 2; | ||||||
|                                         {time: parseDuration("2m")} |                                         send( | ||||||
|                                     ) |                                             `By the people's votes, <@${target.id}> has lost the bet that <@${author.id}> had sent them.` | ||||||
|                                     .then((reactions) => { |                                         ); | ||||||
|                                         // Count votes
 |                                     } else { | ||||||
|                                         const okReaction = reactions.get("✅"); |                                         sender.money += amount; | ||||||
|                                         const noReaction = reactions.get("❌"); |                                         receiver.money += amount; | ||||||
|                                         const ok = okReaction ? (okReaction.count ?? 1) - 1 : 0; |                                         send( | ||||||
|                                         const no = noReaction ? (noReaction.count ?? 1) - 1 : 0; |                                             `By the people's votes, <@${target.id}> couldn't be determined to have won or lost the bet that <@${author.id}> had sent them.` | ||||||
|  |                                         ); | ||||||
|  |                                     } | ||||||
| 
 | 
 | ||||||
|                                         if (ok > no) { |                                     sender.ecoBetInsurance -= amount; | ||||||
|                                             receiver.money += amount * 2; |                                     receiver.ecoBetInsurance -= amount; | ||||||
|                                             send( |                                     Storage.save(); | ||||||
|                                                 `By the people's votes, <@${target.id}> has won the bet that <@${author.id}> had sent them.` |                                 }); | ||||||
|                                             ); |                         }, duration); | ||||||
|                                         } else if (ok < no) { |  | ||||||
|                                             sender.money += amount * 2; |  | ||||||
|                                             send( |  | ||||||
|                                                 `By the people's votes, <@${target.id}> has lost the bet that <@${author.id}> had sent them.` |  | ||||||
|                                             ); |  | ||||||
|                                         } else { |  | ||||||
|                                             sender.money += amount; |  | ||||||
|                                             receiver.money += amount; |  | ||||||
|                                             send( |  | ||||||
|                                                 `By the people's votes, <@${target.id}> couldn't be determined to have won or lost the bet that <@${author.id}> had sent them.` |  | ||||||
|                                             ); |  | ||||||
|                                         } |  | ||||||
|                                         sender.ecoBetInsurance -= amount; |  | ||||||
|                                         receiver.ecoBetInsurance -= amount; |  | ||||||
|                                         Storage.save(); |  | ||||||
|                                     }); |  | ||||||
|                             }, duration); |  | ||||||
|                         } else return await send(`<@${target.id}> has rejected your bet, <@${author.id}>`); |  | ||||||
|                     } else return; |                     } else return; | ||||||
|                 } |                 } | ||||||
|             }) |             }) | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import {GuildMember} from "discord.js"; | import {Command, getMemberByName, NamedCommand, confirm, RestCommand} from "../../../core"; | ||||||
| import {Command, getMemberByName, NamedCommand, prompt, RestCommand} from "../../../core"; |  | ||||||
| import {pluralise} from "../../../lib"; | import {pluralise} from "../../../lib"; | ||||||
| import {Storage} from "../../../structures"; | import {Storage} from "../../../structures"; | ||||||
| import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils"; | import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils"; | ||||||
|  | @ -90,7 +89,7 @@ export const LeaderboardCommand = new NamedCommand({ | ||||||
|                 const user = await client.users.fetch(id); |                 const user = await client.users.fetch(id); | ||||||
| 
 | 
 | ||||||
|                 fields.push({ |                 fields.push({ | ||||||
|                     name: `#${i + 1}. ${user.username}#${user.discriminator}`, |                     name: `#${i + 1}. ${user.tag}`, | ||||||
|                     value: pluralise(users[id].money, "Mon", "s") |                     value: pluralise(users[id].money, "Mon", "s") | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|  | @ -158,42 +157,38 @@ export const PayCommand = new NamedCommand({ | ||||||
|                     return send("You have to use this in a server if you want to send Mons with a username!"); |                     return send("You have to use this in a server if you want to send Mons with a username!"); | ||||||
| 
 | 
 | ||||||
|                 const member = await getMemberByName(guild, combined); |                 const member = await getMemberByName(guild, combined); | ||||||
|                 if (!(member instanceof GuildMember)) return send(member); |                 if (typeof member === "string") return send(member); | ||||||
|                 else if (member.user.id === author.id) return send("You can't send Mons to yourself!"); |                 else if (member.user.id === author.id) return send("You can't send Mons to yourself!"); | ||||||
|                 else if (member.user.bot && process.argv[2] !== "dev") return send("You can't send Mons to a bot!"); |                 else if (member.user.bot && process.argv[2] !== "dev") return send("You can't send Mons to a bot!"); | ||||||
| 
 | 
 | ||||||
|                 const target = member.user; |                 const target = member.user; | ||||||
| 
 | 
 | ||||||
|                 return prompt( |                 const result = await confirm( | ||||||
|                     await send( |                     await send(`Are you sure you want to send ${pluralise(amount, "Mon", "s")} to this person?`, { | ||||||
|                         `Are you sure you want to send ${pluralise( |                         embed: { | ||||||
|                             amount, |                             color: ECO_EMBED_COLOR, | ||||||
|                             "Mon", |                             author: { | ||||||
|                             "s" |                                 name: target.tag, | ||||||
|                         )} to this person?\n*(This message will automatically be deleted after 10 seconds.)*`,
 |                                 icon_url: target.displayAvatarURL({ | ||||||
|                         { |                                     format: "png", | ||||||
|                             embed: { |                                     dynamic: true | ||||||
|                                 color: ECO_EMBED_COLOR, |                                 }) | ||||||
|                                 author: { |  | ||||||
|                                     name: `${target.username}#${target.discriminator}`, |  | ||||||
|                                     icon_url: target.displayAvatarURL({ |  | ||||||
|                                         format: "png", |  | ||||||
|                                         dynamic: true |  | ||||||
|                                     }) |  | ||||||
|                                 } |  | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|                     ), |                     }), | ||||||
|                     author.id, |                     author.id | ||||||
|                     () => { |  | ||||||
|                         const receiver = Storage.getUser(target.id); |  | ||||||
|                         sender.money -= amount; |  | ||||||
|                         receiver.money += amount; |  | ||||||
|                         Storage.save(); |  | ||||||
|                         send(getSendEmbed(author, target, amount)); |  | ||||||
|                     } |  | ||||||
|                 ); |                 ); | ||||||
|  | 
 | ||||||
|  |                 if (result) { | ||||||
|  |                     const receiver = Storage.getUser(target.id); | ||||||
|  |                     sender.money -= amount; | ||||||
|  |                     receiver.money += amount; | ||||||
|  |                     Storage.save(); | ||||||
|  |                     send(getSendEmbed(author, target, amount)); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|         } |         } | ||||||
|     }) |     }) | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -34,12 +34,17 @@ export const ShopCommand = new NamedCommand({ | ||||||
|             const shopPages = split(ShopItems, 5); |             const shopPages = split(ShopItems, 5); | ||||||
|             const pageAmount = shopPages.length; |             const pageAmount = shopPages.length; | ||||||
| 
 | 
 | ||||||
|             paginate(send, author.id, pageAmount, (page, hasMultiplePages) => { |             paginate( | ||||||
|                 return getShopEmbed( |                 send, | ||||||
|                     shopPages[page], |                 (page, hasMultiplePages) => { | ||||||
|                     hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop" |                     return getShopEmbed( | ||||||
|                 ); |                         shopPages[page], | ||||||
|             }); |                         hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop" | ||||||
|  |                     ); | ||||||
|  |                 }, | ||||||
|  |                 pageAmount, | ||||||
|  |                 author.id | ||||||
|  |             ); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -42,11 +42,11 @@ export function getSendEmbed(sender: User, receiver: User, amount: number): obje | ||||||
|             description: `${sender.toString()} has sent ${pluralise(amount, "Mon", "s")} to ${receiver.toString()}!`, |             description: `${sender.toString()} has sent ${pluralise(amount, "Mon", "s")} to ${receiver.toString()}!`, | ||||||
|             fields: [ |             fields: [ | ||||||
|                 { |                 { | ||||||
|                     name: `Sender: ${sender.username}#${sender.discriminator}`, |                     name: `Sender: ${sender.tag}`, | ||||||
|                     value: pluralise(Storage.getUser(sender.id).money, "Mon", "s") |                     value: pluralise(Storage.getUser(sender.id).money, "Mon", "s") | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     name: `Receiver: ${receiver.username}#${receiver.discriminator}`, |                     name: `Receiver: ${receiver.tag}`, | ||||||
|                     value: pluralise(Storage.getUser(receiver.id).money, "Mon", "s") |                     value: pluralise(Storage.getUser(receiver.id).money, "Mon", "s") | ||||||
|                 } |                 } | ||||||
|             ], |             ], | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import {User, GuildMember} from "discord.js"; | import {User} from "discord.js"; | ||||||
| import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE, RestCommand} from "../../core"; | import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE, RestCommand} from "../../core"; | ||||||
| 
 | 
 | ||||||
| // Quotes must be used here or the numbers will change
 | // Quotes must be used here or the numbers will change
 | ||||||
|  | @ -70,7 +70,7 @@ export default new NamedCommand({ | ||||||
|         async run({send, message, channel, guild, author, client, args, combined}) { |         async run({send, message, channel, guild, author, client, args, combined}) { | ||||||
|             const member = await getMemberByName(guild!, combined); |             const member = await getMemberByName(guild!, combined); | ||||||
| 
 | 
 | ||||||
|             if (member instanceof GuildMember) { |             if (typeof member !== "string") { | ||||||
|                 if (member.id in registry) { |                 if (member.id in registry) { | ||||||
|                     send(`\`${member.nickname ?? member.user.username}\` - ${registry[member.id]}`); |                     send(`\`${member.nickname ?? member.user.username}\` - ${registry[member.id]}`); | ||||||
|                 } else { |                 } else { | ||||||
|  |  | ||||||
|  | @ -20,16 +20,21 @@ export default new NamedCommand({ | ||||||
|         const commands = await getCommandList(); |         const commands = await getCommandList(); | ||||||
|         const categoryArray = commands.keyArray(); |         const categoryArray = commands.keyArray(); | ||||||
| 
 | 
 | ||||||
|         paginate(send, author.id, categoryArray.length, (page, hasMultiplePages) => { |         paginate( | ||||||
|             const category = categoryArray[page]; |             send, | ||||||
|             const commandList = commands.get(category)!; |             (page, hasMultiplePages) => { | ||||||
|             let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\`\n`; |                 const category = categoryArray[page]; | ||||||
|             for (const command of commandList) output += `\n❯ \`${command.name}\`: ${command.description}`; |                 const commandList = commands.get(category)!; | ||||||
|             return new MessageEmbed() |                 let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\`\n`; | ||||||
|                 .setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category) |                 for (const command of commandList) output += `\n❯ \`${command.name}\`: ${command.description}`; | ||||||
|                 .setDescription(output) |                 return new MessageEmbed() | ||||||
|                 .setColor(EMBED_COLOR); |                     .setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category) | ||||||
|         }); |                     .setDescription(output) | ||||||
|  |                     .setColor(EMBED_COLOR); | ||||||
|  |             }, | ||||||
|  |             categoryArray.length, | ||||||
|  |             author.id | ||||||
|  |         ); | ||||||
|     }, |     }, | ||||||
|     any: new Command({ |     any: new Command({ | ||||||
|         async run({send, message, channel, guild, author, member, client, args}) { |         async run({send, message, channel, guild, author, member, client, args}) { | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ export default new NamedCommand({ | ||||||
|                 async run({send, message, channel, guild, author, client, args, combined}) { |                 async run({send, message, channel, guild, author, client, args, combined}) { | ||||||
|                     const member = await getMemberByName(guild!, combined); |                     const member = await getMemberByName(guild!, combined); | ||||||
| 
 | 
 | ||||||
|                     if (member instanceof GuildMember) { |                     if (typeof member !== "string") { | ||||||
|                         send( |                         send( | ||||||
|                             member.user.displayAvatarURL({ |                             member.user.displayAvatarURL({ | ||||||
|                                 dynamic: true, |                                 dynamic: true, | ||||||
|  | @ -110,7 +110,7 @@ export default new NamedCommand({ | ||||||
|                 async run({send, message, channel, guild, author, member, client, args, combined}) { |                 async run({send, message, channel, guild, author, member, client, args, combined}) { | ||||||
|                     const targetGuild = getGuildByName(combined); |                     const targetGuild = getGuildByName(combined); | ||||||
| 
 | 
 | ||||||
|                     if (targetGuild instanceof Guild) { |                     if (typeof targetGuild !== "string") { | ||||||
|                         send(await getGuildInfo(targetGuild, guild)); |                         send(await getGuildInfo(targetGuild, guild)); | ||||||
|                     } else { |                     } else { | ||||||
|                         send(targetGuild); |                         send(targetGuild); | ||||||
|  |  | ||||||
|  | @ -90,17 +90,22 @@ async function displayEmoteList(emotes: GuildEmoji[], send: SendFunction, author | ||||||
| 
 | 
 | ||||||
|     // Gather the first page (if it even exists, which it might not if there no valid emotes appear)
 |     // Gather the first page (if it even exists, which it might not if there no valid emotes appear)
 | ||||||
|     if (pages > 0) { |     if (pages > 0) { | ||||||
|         paginate(send, author.id, pages, (page, hasMultiplePages) => { |         paginate( | ||||||
|             embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**"); |             send, | ||||||
|  |             (page, hasMultiplePages) => { | ||||||
|  |                 embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**"); | ||||||
| 
 | 
 | ||||||
|             let desc = ""; |                 let desc = ""; | ||||||
|             for (const emote of sections[page]) { |                 for (const emote of sections[page]) { | ||||||
|                 desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`; |                     desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`; | ||||||
|             } |                 } | ||||||
|             embed.setDescription(desc); |                 embed.setDescription(desc); | ||||||
| 
 | 
 | ||||||
|             return embed; |                 return embed; | ||||||
|         }); |             }, | ||||||
|  |             pages, | ||||||
|  |             author.id | ||||||
|  |         ); | ||||||
|     } else { |     } else { | ||||||
|         send("No valid emotes found by that query."); |         send("No valid emotes found by that query."); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,15 +1,6 @@ | ||||||
| import { | import {Command, NamedCommand, askForReply, confirm, askMultipleChoice, getMemberByName, RestCommand} from "../../core"; | ||||||
|     Command, |  | ||||||
|     NamedCommand, |  | ||||||
|     ask, |  | ||||||
|     askYesOrNo, |  | ||||||
|     askMultipleChoice, |  | ||||||
|     prompt, |  | ||||||
|     getMemberByName, |  | ||||||
|     RestCommand |  | ||||||
| } from "../../core"; |  | ||||||
| import {Storage} from "../../structures"; | import {Storage} from "../../structures"; | ||||||
| import {User, GuildMember} from "discord.js"; | import {User} from "discord.js"; | ||||||
| import moment from "moment"; | import moment from "moment"; | ||||||
| 
 | 
 | ||||||
| const DATE_FORMAT = "D MMMM YYYY"; | const DATE_FORMAT = "D MMMM YYYY"; | ||||||
|  | @ -178,183 +169,184 @@ function getTimeEmbed(user: User) { | ||||||
| export default new NamedCommand({ | export default new NamedCommand({ | ||||||
|     description: "Show others what time it is for you.", |     description: "Show others what time it is for you.", | ||||||
|     aliases: ["tz"], |     aliases: ["tz"], | ||||||
|     async run({send, channel, author}) { |     async run({send, author}) { | ||||||
|         send(getTimeEmbed(author)); |         send(getTimeEmbed(author)); | ||||||
|     }, |     }, | ||||||
|     subcommands: { |     subcommands: { | ||||||
|         // Welcome to callback hell. We hope you enjoy your stay here!
 |         // Welcome to callback hell. We hope you enjoy your stay here!
 | ||||||
|         setup: new NamedCommand({ |         setup: new NamedCommand({ | ||||||
|             description: "Registers your timezone information for the bot.", |             description: "Registers your timezone information for the bot.", | ||||||
|             async run({send, author, channel}) { |             async run({send, author}) { | ||||||
|                 const profile = Storage.getUser(author.id); |                 const profile = Storage.getUser(author.id); | ||||||
|                 profile.timezone = null; |                 profile.timezone = null; | ||||||
|                 profile.daylightSavingsRegion = null; |                 profile.daylightSavingsRegion = null; | ||||||
|                 let hour: number; |  | ||||||
| 
 | 
 | ||||||
|                 ask( |                 // Parse and validate reply
 | ||||||
|  |                 const reply = await askForReply( | ||||||
|                     await send( |                     await send( | ||||||
|                         "What hour (0 to 23) is it for you right now?\n*(Note: Make sure to use Discord's inline reply feature or this won't work!)*" |                         "What hour (0 to 23) is it for you right now?\n*(Note: Make sure to use Discord's inline reply feature or this won't work!)*" | ||||||
|                     ), |                     ), | ||||||
|                     author.id, |                     author.id, | ||||||
|                     (reply) => { |                     30000 | ||||||
|                         hour = parseInt(reply); |  | ||||||
| 
 |  | ||||||
|                         if (isNaN(hour)) { |  | ||||||
|                             return false; |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         return hour >= 0 && hour <= 23; |  | ||||||
|                     }, |  | ||||||
|                     async () => { |  | ||||||
|                         // You need to also take into account whether or not it's the same day in UTC or not.
 |  | ||||||
|                         // The problem this setup avoids is messing up timezones by 24 hours.
 |  | ||||||
|                         // For example, the old logic was just (hour - hourUTC). When I setup my timezone (UTC-6) at 18:00, it was UTC 00:00.
 |  | ||||||
|                         // That means that that formula was doing (18 - 0) getting me UTC+18 instead of UTC-6 because that naive formula didn't take into account a difference in days.
 |  | ||||||
| 
 |  | ||||||
|                         // (day * 24 + hour) - (day * 24 + hour)
 |  | ||||||
|                         // Since the timezones will be restricted to -12 to +14, you'll be given three options.
 |  | ||||||
|                         // The end of the month should be calculated automatically, you should have enough information at that point.
 |  | ||||||
| 
 |  | ||||||
|                         // But after mapping out the edge cases, I figured out that you can safely gather accurate information just based on whether the UTC day matches the user's day.
 |  | ||||||
|                         // 21:xx (-12, -d) -- 09:xx (+0, 0d) -- 23:xx (+14, 0d)
 |  | ||||||
|                         // 22:xx (-12, -d) -- 10:xx (+0, 0d) -- 00:xx (+14, +d)
 |  | ||||||
|                         // 23:xx (-12, -d) -- 11:xx (+0, 0d) -- 01:xx (+14, +d)
 |  | ||||||
|                         // 00:xx (-12, 0d) -- 12:xx (+0, 0d) -- 02:xx (+14, +d)
 |  | ||||||
| 
 |  | ||||||
|                         // For example, 23:xx (-12) - 01:xx (+14) shows that even the edge cases of UTC-12 and UTC+14 cannot overlap, so the dataset can be reduced to a yes or no option.
 |  | ||||||
|                         // - 23:xx same day = +0, 23:xx diff day = -1
 |  | ||||||
|                         // - 00:xx same day = +0, 00:xx diff day = +1
 |  | ||||||
|                         // - 01:xx same day = +0, 01:xx diff day = +1
 |  | ||||||
| 
 |  | ||||||
|                         // First, create a tuple list matching each possible hour-dayOffset-timezoneOffset combination. In the above example, it'd go something like this:
 |  | ||||||
|                         // [[23, -1, -12], [0, 0, -11], ..., [23, 0, 12], [0, 1, 13], [1, 1, 14]]
 |  | ||||||
|                         // Then just find the matching one by filtering through dayOffset (equals zero or not), then the hour from user input.
 |  | ||||||
|                         // Remember that you don't know where the difference in day might be at this point, so you can't do (hour - hourUTC) safely.
 |  | ||||||
|                         // In terms of the equation, you're missing a variable in (--> day <-- * 24 + hour) - (day * 24 + hour). That's what the loop is for.
 |  | ||||||
| 
 |  | ||||||
|                         // Now you might be seeing a problem with setting this up at the end/beginning of a month, but that actually isn't a problem.
 |  | ||||||
|                         // Let's say that it's 00:xx of the first UTC day of a month. hourSumUTC = 24
 |  | ||||||
|                         // UTC-12 --> hourSumLowerBound (hourSumUTC - 12) = 12
 |  | ||||||
|                         // UTC+14 --> hourSumUpperBound (hourSumUTC + 14) = 38
 |  | ||||||
|                         // Remember, the nice thing about making these bounds relative to the UTC hour sum is that there can't be any conflicts even at the edges of months.
 |  | ||||||
|                         // And remember that we aren't using this question: (day * 24 + hour) - (day * 24 + hour). We're drawing from a list which does not store its data in absolute terms.
 |  | ||||||
|                         // That means there's no 31st, 1st, 2nd, it's -1, 0, +1. I just need to make sure to calculate an offset to subtract from the hour sums.
 |  | ||||||
| 
 |  | ||||||
|                         const date = new Date(); // e.g. 2021-05-01 @ 05:00
 |  | ||||||
|                         const day = date.getUTCDate(); // e.g. 1
 |  | ||||||
|                         const hourSumUTC = day * 24 + date.getUTCHours(); // e.g. 29
 |  | ||||||
|                         const timezoneTupleList: [number, number, number][] = []; |  | ||||||
|                         const uniques: number[] = []; // only for temporary use
 |  | ||||||
|                         const duplicates = []; |  | ||||||
| 
 |  | ||||||
|                         // Setup the tuple list in a separate block.
 |  | ||||||
|                         for (let timezoneOffset = -12; timezoneOffset <= 14; timezoneOffset++) { |  | ||||||
|                             const hourSum = hourSumUTC + timezoneOffset; // e.g. 23, UTC-6 (17 to 43)
 |  | ||||||
|                             const hour = hourSum % 24; // e.g. 23
 |  | ||||||
|                             // This works because you get the # of days w/o hours minus UTC days without hours.
 |  | ||||||
|                             // Since it's all relative to UTC, it'll end up being -1, 0, or 1.
 |  | ||||||
|                             const dayOffset = Math.floor(hourSum / 24) - day; // e.g. -1
 |  | ||||||
|                             timezoneTupleList.push([hour, dayOffset, timezoneOffset]); |  | ||||||
| 
 |  | ||||||
|                             if (uniques.includes(hour)) { |  | ||||||
|                                 duplicates.push(hour); |  | ||||||
|                             } else { |  | ||||||
|                                 uniques.push(hour); |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         // I calculate the list beforehand and check for duplicates to reduce unnecessary asking.
 |  | ||||||
|                         if (duplicates.includes(hour)) { |  | ||||||
|                             const isSameDay = await askYesOrNo( |  | ||||||
|                                 await send( |  | ||||||
|                                     `Is the current day of the month the ${moment().utc().format("Do")} for you?` |  | ||||||
|                                 ), |  | ||||||
|                                 author.id |  | ||||||
|                             ); |  | ||||||
| 
 |  | ||||||
|                             // Filter through isSameDay (aka !hasDayOffset) then hour from user-generated input.
 |  | ||||||
|                             // isSameDay is checked first to reduce the amount of conditionals per loop.
 |  | ||||||
|                             if (isSameDay) { |  | ||||||
|                                 for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { |  | ||||||
|                                     if (dayOffset === 0 && hour === hourPoint) { |  | ||||||
|                                         profile.timezone = timezoneOffset; |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                             } else { |  | ||||||
|                                 for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { |  | ||||||
|                                     if (dayOffset !== 0 && hour === hourPoint) { |  | ||||||
|                                         profile.timezone = timezoneOffset; |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } else { |  | ||||||
|                             // If it's a unique hour, just search through the tuple list and find the matching entry.
 |  | ||||||
|                             for (const [hourPoint, _dayOffset, timezoneOffset] of timezoneTupleList) { |  | ||||||
|                                 if (hour === hourPoint) { |  | ||||||
|                                     profile.timezone = timezoneOffset; |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         // I should note that error handling should be added sometime because await throws an exception on Promise.reject.
 |  | ||||||
|                         const hasDST = await askYesOrNo( |  | ||||||
|                             await send("Does your timezone change based on daylight savings?"), |  | ||||||
|                             author.id |  | ||||||
|                         ); |  | ||||||
| 
 |  | ||||||
|                         const finalize = () => { |  | ||||||
|                             Storage.save(); |  | ||||||
|                             send( |  | ||||||
|                                 "You've finished setting up your timezone! Just check to see if this looks right, and if it doesn't, run this setup again.", |  | ||||||
|                                 getTimeEmbed(author) |  | ||||||
|                             ); |  | ||||||
|                         }; |  | ||||||
| 
 |  | ||||||
|                         if (hasDST) { |  | ||||||
|                             const finalizeDST = (region: DST) => { |  | ||||||
|                                 profile.daylightSavingsRegion = region; |  | ||||||
| 
 |  | ||||||
|                                 // If daylight savings is active, subtract the timezone offset by one to store the standard time.
 |  | ||||||
|                                 if (hasDaylightSavings(region)) { |  | ||||||
|                                     profile.timezone!--; |  | ||||||
|                                 } |  | ||||||
| 
 |  | ||||||
|                                 finalize(); |  | ||||||
|                             }; |  | ||||||
| 
 |  | ||||||
|                             askMultipleChoice(await send(DST_NOTE_SETUP), author.id, [ |  | ||||||
|                                 () => finalizeDST("na"), |  | ||||||
|                                 () => finalizeDST("eu"), |  | ||||||
|                                 () => finalizeDST("sh") |  | ||||||
|                             ]); |  | ||||||
|                         } else { |  | ||||||
|                             finalize(); |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     () => "you need to enter in a valid integer between 0 to 23" |  | ||||||
|                 ); |                 ); | ||||||
|  |                 if (reply === null) return send("Message timed out."); | ||||||
|  |                 const hour = parseInt(reply.content); | ||||||
|  |                 const isValidHour = !isNaN(hour) && hour >= 0 && hour <= 23; | ||||||
|  |                 if (!isValidHour) return reply.reply("you need to enter in a valid integer between 0 to 23"); | ||||||
|  | 
 | ||||||
|  |                 // You need to also take into account whether or not it's the same day in UTC or not.
 | ||||||
|  |                 // The problem this setup avoids is messing up timezones by 24 hours.
 | ||||||
|  |                 // For example, the old logic was just (hour - hourUTC). When I setup my timezone (UTC-6) at 18:00, it was UTC 00:00.
 | ||||||
|  |                 // That means that that formula was doing (18 - 0) getting me UTC+18 instead of UTC-6 because that naive formula didn't take into account a difference in days.
 | ||||||
|  | 
 | ||||||
|  |                 // (day * 24 + hour) - (day * 24 + hour)
 | ||||||
|  |                 // Since the timezones will be restricted to -12 to +14, you'll be given three options.
 | ||||||
|  |                 // The end of the month should be calculated automatically, you should have enough information at that point.
 | ||||||
|  | 
 | ||||||
|  |                 // But after mapping out the edge cases, I figured out that you can safely gather accurate information just based on whether the UTC day matches the user's day.
 | ||||||
|  |                 // 21:xx (-12, -d) -- 09:xx (+0, 0d) -- 23:xx (+14, 0d)
 | ||||||
|  |                 // 22:xx (-12, -d) -- 10:xx (+0, 0d) -- 00:xx (+14, +d)
 | ||||||
|  |                 // 23:xx (-12, -d) -- 11:xx (+0, 0d) -- 01:xx (+14, +d)
 | ||||||
|  |                 // 00:xx (-12, 0d) -- 12:xx (+0, 0d) -- 02:xx (+14, +d)
 | ||||||
|  | 
 | ||||||
|  |                 // For example, 23:xx (-12) - 01:xx (+14) shows that even the edge cases of UTC-12 and UTC+14 cannot overlap, so the dataset can be reduced to a yes or no option.
 | ||||||
|  |                 // - 23:xx same day = +0, 23:xx diff day = -1
 | ||||||
|  |                 // - 00:xx same day = +0, 00:xx diff day = +1
 | ||||||
|  |                 // - 01:xx same day = +0, 01:xx diff day = +1
 | ||||||
|  | 
 | ||||||
|  |                 // First, create a tuple list matching each possible hour-dayOffset-timezoneOffset combination. In the above example, it'd go something like this:
 | ||||||
|  |                 // [[23, -1, -12], [0, 0, -11], ..., [23, 0, 12], [0, 1, 13], [1, 1, 14]]
 | ||||||
|  |                 // Then just find the matching one by filtering through dayOffset (equals zero or not), then the hour from user input.
 | ||||||
|  |                 // Remember that you don't know where the difference in day might be at this point, so you can't do (hour - hourUTC) safely.
 | ||||||
|  |                 // In terms of the equation, you're missing a variable in (--> day <-- * 24 + hour) - (day * 24 + hour). That's what the loop is for.
 | ||||||
|  | 
 | ||||||
|  |                 // Now you might be seeing a problem with setting this up at the end/beginning of a month, but that actually isn't a problem.
 | ||||||
|  |                 // Let's say that it's 00:xx of the first UTC day of a month. hourSumUTC = 24
 | ||||||
|  |                 // UTC-12 --> hourSumLowerBound (hourSumUTC - 12) = 12
 | ||||||
|  |                 // UTC+14 --> hourSumUpperBound (hourSumUTC + 14) = 38
 | ||||||
|  |                 // Remember, the nice thing about making these bounds relative to the UTC hour sum is that there can't be any conflicts even at the edges of months.
 | ||||||
|  |                 // And remember that we aren't using this question: (day * 24 + hour) - (day * 24 + hour). We're drawing from a list which does not store its data in absolute terms.
 | ||||||
|  |                 // That means there's no 31st, 1st, 2nd, it's -1, 0, +1. I just need to make sure to calculate an offset to subtract from the hour sums.
 | ||||||
|  | 
 | ||||||
|  |                 const date = new Date(); // e.g. 2021-05-01 @ 05:00
 | ||||||
|  |                 const day = date.getUTCDate(); // e.g. 1
 | ||||||
|  |                 const hourSumUTC = day * 24 + date.getUTCHours(); // e.g. 29
 | ||||||
|  |                 const timezoneTupleList: [number, number, number][] = []; | ||||||
|  |                 const uniques: number[] = []; // only for temporary use
 | ||||||
|  |                 const duplicates = []; | ||||||
|  | 
 | ||||||
|  |                 // Setup the tuple list in a separate block.
 | ||||||
|  |                 for (let timezoneOffset = -12; timezoneOffset <= 14; timezoneOffset++) { | ||||||
|  |                     const hourSum = hourSumUTC + timezoneOffset; // e.g. 23, UTC-6 (17 to 43)
 | ||||||
|  |                     const hour = hourSum % 24; // e.g. 23
 | ||||||
|  |                     // This works because you get the # of days w/o hours minus UTC days without hours.
 | ||||||
|  |                     // Since it's all relative to UTC, it'll end up being -1, 0, or 1.
 | ||||||
|  |                     const dayOffset = Math.floor(hourSum / 24) - day; // e.g. -1
 | ||||||
|  |                     timezoneTupleList.push([hour, dayOffset, timezoneOffset]); | ||||||
|  | 
 | ||||||
|  |                     if (uniques.includes(hour)) { | ||||||
|  |                         duplicates.push(hour); | ||||||
|  |                     } else { | ||||||
|  |                         uniques.push(hour); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // I calculate the list beforehand and check for duplicates to reduce unnecessary asking.
 | ||||||
|  |                 if (duplicates.includes(hour)) { | ||||||
|  |                     const isSameDay = await confirm( | ||||||
|  |                         await send(`Is the current day of the month the ${moment().utc().format("Do")} for you?`), | ||||||
|  |                         author.id | ||||||
|  |                     ); | ||||||
|  | 
 | ||||||
|  |                     // Filter through isSameDay (aka !hasDayOffset) then hour from user-generated input.
 | ||||||
|  |                     // isSameDay is checked first to reduce the amount of conditionals per loop.
 | ||||||
|  |                     if (isSameDay) { | ||||||
|  |                         for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { | ||||||
|  |                             if (dayOffset === 0 && hour === hourPoint) { | ||||||
|  |                                 profile.timezone = timezoneOffset; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { | ||||||
|  |                             if (dayOffset !== 0 && hour === hourPoint) { | ||||||
|  |                                 profile.timezone = timezoneOffset; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     // If it's a unique hour, just search through the tuple list and find the matching entry.
 | ||||||
|  |                     for (const [hourPoint, _dayOffset, timezoneOffset] of timezoneTupleList) { | ||||||
|  |                         if (hour === hourPoint) { | ||||||
|  |                             profile.timezone = timezoneOffset; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // I should note that error handling should be added sometime because await throws an exception on Promise.reject.
 | ||||||
|  |                 const hasDST = await confirm( | ||||||
|  |                     await send("Does your timezone change based on daylight savings?"), | ||||||
|  |                     author.id | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|  |                 const finalize = () => { | ||||||
|  |                     Storage.save(); | ||||||
|  |                     send( | ||||||
|  |                         "You've finished setting up your timezone! Just check to see if this looks right, and if it doesn't, run this setup again.", | ||||||
|  |                         getTimeEmbed(author) | ||||||
|  |                     ); | ||||||
|  |                 }; | ||||||
|  | 
 | ||||||
|  |                 if (hasDST) { | ||||||
|  |                     const finalizeDST = (region: DST) => { | ||||||
|  |                         profile.daylightSavingsRegion = region; | ||||||
|  | 
 | ||||||
|  |                         // If daylight savings is active, subtract the timezone offset by one to store the standard time.
 | ||||||
|  |                         if (hasDaylightSavings(region)) { | ||||||
|  |                             profile.timezone!--; | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         finalize(); | ||||||
|  |                     }; | ||||||
|  | 
 | ||||||
|  |                     const index = await askMultipleChoice(await send(DST_NOTE_SETUP), author.id, 3); | ||||||
|  | 
 | ||||||
|  |                     switch (index) { | ||||||
|  |                         case 0: | ||||||
|  |                             finalizeDST("na"); | ||||||
|  |                             break; | ||||||
|  |                         case 1: | ||||||
|  |                             finalizeDST("eu"); | ||||||
|  |                             break; | ||||||
|  |                         case 2: | ||||||
|  |                             finalizeDST("sh"); | ||||||
|  |                             break; | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     finalize(); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return; | ||||||
|             } |             } | ||||||
|         }), |         }), | ||||||
|         delete: new NamedCommand({ |         delete: new NamedCommand({ | ||||||
|             description: "Delete your timezone information.", |             description: "Delete your timezone information.", | ||||||
|             async run({send, channel, author}) { |             async run({send, author}) { | ||||||
|                 prompt( |                 const result = await confirm( | ||||||
|                     await send( |                     await send("Are you sure you want to delete your timezone information?"), | ||||||
|                         "Are you sure you want to delete your timezone information?\n*(This message will automatically be deleted after 10 seconds.)*" |                     author.id | ||||||
|                     ), |  | ||||||
|                     author.id, |  | ||||||
|                     () => { |  | ||||||
|                         const profile = Storage.getUser(author.id); |  | ||||||
|                         profile.timezone = null; |  | ||||||
|                         profile.daylightSavingsRegion = null; |  | ||||||
|                         Storage.save(); |  | ||||||
|                     } |  | ||||||
|                 ); |                 ); | ||||||
|  | 
 | ||||||
|  |                 if (result) { | ||||||
|  |                     const profile = Storage.getUser(author.id); | ||||||
|  |                     profile.timezone = null; | ||||||
|  |                     profile.daylightSavingsRegion = null; | ||||||
|  |                     Storage.save(); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         }), |         }), | ||||||
|         utc: new NamedCommand({ |         utc: new NamedCommand({ | ||||||
|             description: "Displays UTC time.", |             description: "Displays UTC time.", | ||||||
|             async run({send, channel}) { |             async run({send}) { | ||||||
|                 const time = moment().utc(); |                 const time = moment().utc(); | ||||||
| 
 | 
 | ||||||
|                 send({ |                 send({ | ||||||
|  | @ -386,15 +378,15 @@ export default new NamedCommand({ | ||||||
|     id: "user", |     id: "user", | ||||||
|     user: new Command({ |     user: new Command({ | ||||||
|         description: "See what time it is for someone else.", |         description: "See what time it is for someone else.", | ||||||
|         async run({send, channel, args}) { |         async run({send, args}) { | ||||||
|             send(getTimeEmbed(args[0])); |             send(getTimeEmbed(args[0])); | ||||||
|         } |         } | ||||||
|     }), |     }), | ||||||
|     any: new RestCommand({ |     any: new RestCommand({ | ||||||
|         description: "See what time it is for someone else (by their username).", |         description: "See what time it is for someone else (by their username).", | ||||||
|         async run({send, channel, args, guild, combined}) { |         async run({send, guild, combined}) { | ||||||
|             const member = await getMemberByName(guild!, combined); |             const member = await getMemberByName(guild!, combined); | ||||||
|             if (member instanceof GuildMember) send(getTimeEmbed(member.user)); |             if (typeof member !== "string") send(getTimeEmbed(member.user)); | ||||||
|             else send(member); |             else send(member); | ||||||
|         } |         } | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ import { | ||||||
|     GuildChannel, |     GuildChannel, | ||||||
|     Channel |     Channel | ||||||
| } from "discord.js"; | } from "discord.js"; | ||||||
| import {getChannelByID, getGuildByID, getMessageByID, getUserByID, SingleMessageOptions, SendFunction} from "./libd"; | import {getChannelByID, getGuildByID, getMessageByID, getUserByID, SendFunction} from "./libd"; | ||||||
| import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions"; | import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions"; | ||||||
| import {getPrefix} from "./interface"; | import {getPrefix} from "./interface"; | ||||||
| import {parseVars, requireAllCasesHandledFor} from "../lib"; | import {parseVars, requireAllCasesHandledFor} from "../lib"; | ||||||
|  | @ -244,18 +244,14 @@ export class Command extends BaseCommand { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Go through the arguments provided and find the right subcommand, then execute with the given arguments.
 |     // Go through the arguments provided and find the right subcommand, then execute with the given arguments.
 | ||||||
|     // Will return null if it successfully executes, SingleMessageOptions if there's an error (to let the user know what it is).
 |     // Will return null if it successfully executes, string if there's an error (to let the user know what it is).
 | ||||||
|     //
 |     //
 | ||||||
|     // Calls the resulting subcommand's execute method in order to make more modular code, basically pushing the chain of execution to the subcommand.
 |     // Calls the resulting subcommand's execute method in order to make more modular code, basically pushing the chain of execution to the subcommand.
 | ||||||
|     // For example, a numeric subcommand would accept args of [4] then execute on it.
 |     // For example, a numeric subcommand would accept args of [4] then execute on it.
 | ||||||
|     //
 |     //
 | ||||||
|     // Because each Command instance is isolated from others, it becomes practically impossible to predict the total amount of subcommands when isolating the code to handle each individual layer of recursion.
 |     // Because each Command instance is isolated from others, it becomes practically impossible to predict the total amount of subcommands when isolating the code to handle each individual layer of recursion.
 | ||||||
|     // Therefore, if a Command is declared as a rest type, any typed args that come at the end must be handled manually.
 |     // Therefore, if a Command is declared as a rest type, any typed args that come at the end must be handled manually.
 | ||||||
|     public async execute( |     public async execute(args: string[], menu: CommandMenu, metadata: ExecuteCommandMetadata): Promise<string | null> { | ||||||
|         args: string[], |  | ||||||
|         menu: CommandMenu, |  | ||||||
|         metadata: ExecuteCommandMetadata |  | ||||||
|     ): Promise<SingleMessageOptions | null> { |  | ||||||
|         // Update inherited properties if the current command specifies a property.
 |         // Update inherited properties if the current command specifies a property.
 | ||||||
|         // In case there are no initial arguments, these should go first so that it can register.
 |         // In case there are no initial arguments, these should go first so that it can register.
 | ||||||
|         if (this.permission !== -1) metadata.permission = this.permission; |         if (this.permission !== -1) metadata.permission = this.permission; | ||||||
|  | @ -292,9 +288,7 @@ export class Command extends BaseCommand { | ||||||
|                     const errorMessage = error.stack ?? error; |                     const errorMessage = error.stack ?? error; | ||||||
|                     console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); |                     console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); | ||||||
| 
 | 
 | ||||||
|                     return { |                     return `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``; | ||||||
|                         content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\`` |  | ||||||
|                     }; |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -313,15 +307,13 @@ export class Command extends BaseCommand { | ||||||
|             const id = patterns.channel.exec(param)![1]; |             const id = patterns.channel.exec(param)![1]; | ||||||
|             const channel = await getChannelByID(id); |             const channel = await getChannelByID(id); | ||||||
| 
 | 
 | ||||||
|             if (channel instanceof Channel) { |             if (typeof channel !== "string") { | ||||||
|                 if (channel instanceof TextChannel || channel instanceof DMChannel) { |                 if (channel instanceof TextChannel || channel instanceof DMChannel) { | ||||||
|                     metadata.symbolicArgs.push("<channel>"); |                     metadata.symbolicArgs.push("<channel>"); | ||||||
|                     menu.args.push(channel); |                     menu.args.push(channel); | ||||||
|                     return this.channel.execute(args, menu, metadata); |                     return this.channel.execute(args, menu, metadata); | ||||||
|                 } else { |                 } else { | ||||||
|                     return { |                     return `\`${id}\` is not a valid text channel!`; | ||||||
|                         content: `\`${id}\` is not a valid text channel!` |  | ||||||
|                     }; |  | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|                 return channel; |                 return channel; | ||||||
|  | @ -330,9 +322,7 @@ export class Command extends BaseCommand { | ||||||
|             const id = patterns.role.exec(param)![1]; |             const id = patterns.role.exec(param)![1]; | ||||||
| 
 | 
 | ||||||
|             if (!menu.guild) { |             if (!menu.guild) { | ||||||
|                 return { |                 return "You can't use role parameters in DM channels!"; | ||||||
|                     content: "You can't use role parameters in DM channels!" |  | ||||||
|                 }; |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const role = menu.guild.roles.cache.get(id); |             const role = menu.guild.roles.cache.get(id); | ||||||
|  | @ -342,9 +332,7 @@ export class Command extends BaseCommand { | ||||||
|                 menu.args.push(role); |                 menu.args.push(role); | ||||||
|                 return this.role.execute(args, menu, metadata); |                 return this.role.execute(args, menu, metadata); | ||||||
|             } else { |             } else { | ||||||
|                 return { |                 return `\`${id}\` is not a valid role in this server!`; | ||||||
|                     content: `\`${id}\` is not a valid role in this server!` |  | ||||||
|                 }; |  | ||||||
|             } |             } | ||||||
|         } else if (this.emote && patterns.emote.test(param)) { |         } else if (this.emote && patterns.emote.test(param)) { | ||||||
|             const id = patterns.emote.exec(param)![1]; |             const id = patterns.emote.exec(param)![1]; | ||||||
|  | @ -355,9 +343,7 @@ export class Command extends BaseCommand { | ||||||
|                 menu.args.push(emote); |                 menu.args.push(emote); | ||||||
|                 return this.emote.execute(args, menu, metadata); |                 return this.emote.execute(args, menu, metadata); | ||||||
|             } else { |             } else { | ||||||
|                 return { |                 return `\`${id}\` isn't a valid emote!`; | ||||||
|                     content: `\`${id}\` isn't a valid emote!` |  | ||||||
|                 }; |  | ||||||
|             } |             } | ||||||
|         } else if (this.message && (isMessageLink || isMessagePair)) { |         } else if (this.message && (isMessageLink || isMessagePair)) { | ||||||
|             let channelID = ""; |             let channelID = ""; | ||||||
|  | @ -375,7 +361,7 @@ export class Command extends BaseCommand { | ||||||
| 
 | 
 | ||||||
|             const message = await getMessageByID(channelID, messageID); |             const message = await getMessageByID(channelID, messageID); | ||||||
| 
 | 
 | ||||||
|             if (message instanceof Message) { |             if (typeof message !== "string") { | ||||||
|                 metadata.symbolicArgs.push("<message>"); |                 metadata.symbolicArgs.push("<message>"); | ||||||
|                 menu.args.push(message); |                 menu.args.push(message); | ||||||
|                 return this.message.execute(args, menu, metadata); |                 return this.message.execute(args, menu, metadata); | ||||||
|  | @ -386,7 +372,7 @@ export class Command extends BaseCommand { | ||||||
|             const id = patterns.user.exec(param)![1]; |             const id = patterns.user.exec(param)![1]; | ||||||
|             const user = await getUserByID(id); |             const user = await getUserByID(id); | ||||||
| 
 | 
 | ||||||
|             if (user instanceof User) { |             if (typeof user !== "string") { | ||||||
|                 metadata.symbolicArgs.push("<user>"); |                 metadata.symbolicArgs.push("<user>"); | ||||||
|                 menu.args.push(user); |                 menu.args.push(user); | ||||||
|                 return this.user.execute(args, menu, metadata); |                 return this.user.execute(args, menu, metadata); | ||||||
|  | @ -403,24 +389,20 @@ export class Command extends BaseCommand { | ||||||
|                 case "channel": |                 case "channel": | ||||||
|                     const channel = await getChannelByID(id); |                     const channel = await getChannelByID(id); | ||||||
| 
 | 
 | ||||||
|                     if (channel instanceof Channel) { |                     if (typeof channel !== "string") { | ||||||
|                         if (channel instanceof TextChannel || channel instanceof DMChannel) { |                         if (channel instanceof TextChannel || channel instanceof DMChannel) { | ||||||
|                             metadata.symbolicArgs.push("<channel>"); |                             metadata.symbolicArgs.push("<channel>"); | ||||||
|                             menu.args.push(channel); |                             menu.args.push(channel); | ||||||
|                             return this.id.execute(args, menu, metadata); |                             return this.id.execute(args, menu, metadata); | ||||||
|                         } else { |                         } else { | ||||||
|                             return { |                             return `\`${id}\` is not a valid text channel!`; | ||||||
|                                 content: `\`${id}\` is not a valid text channel!` |  | ||||||
|                             }; |  | ||||||
|                         } |                         } | ||||||
|                     } else { |                     } else { | ||||||
|                         return channel; |                         return channel; | ||||||
|                     } |                     } | ||||||
|                 case "role": |                 case "role": | ||||||
|                     if (!menu.guild) { |                     if (!menu.guild) { | ||||||
|                         return { |                         return "You can't use role parameters in DM channels!"; | ||||||
|                             content: "You can't use role parameters in DM channels!" |  | ||||||
|                         }; |  | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     const role = menu.guild.roles.cache.get(id); |                     const role = menu.guild.roles.cache.get(id); | ||||||
|  | @ -429,9 +411,7 @@ export class Command extends BaseCommand { | ||||||
|                         menu.args.push(role); |                         menu.args.push(role); | ||||||
|                         return this.id.execute(args, menu, metadata); |                         return this.id.execute(args, menu, metadata); | ||||||
|                     } else { |                     } else { | ||||||
|                         return { |                         return `\`${id}\` isn't a valid role in this server!`; | ||||||
|                             content: `\`${id}\` isn't a valid role in this server!` |  | ||||||
|                         }; |  | ||||||
|                     } |                     } | ||||||
|                 case "emote": |                 case "emote": | ||||||
|                     const emote = menu.client.emojis.cache.get(id); |                     const emote = menu.client.emojis.cache.get(id); | ||||||
|  | @ -440,14 +420,12 @@ export class Command extends BaseCommand { | ||||||
|                         menu.args.push(emote); |                         menu.args.push(emote); | ||||||
|                         return this.id.execute(args, menu, metadata); |                         return this.id.execute(args, menu, metadata); | ||||||
|                     } else { |                     } else { | ||||||
|                         return { |                         return `\`${id}\` isn't a valid emote!`; | ||||||
|                             content: `\`${id}\` isn't a valid emote!` |  | ||||||
|                         }; |  | ||||||
|                     } |                     } | ||||||
|                 case "message": |                 case "message": | ||||||
|                     const message = await getMessageByID(menu.channel, id); |                     const message = await getMessageByID(menu.channel, id); | ||||||
| 
 | 
 | ||||||
|                     if (message instanceof Message) { |                     if (typeof message !== "string") { | ||||||
|                         menu.args.push(message); |                         menu.args.push(message); | ||||||
|                         return this.id.execute(args, menu, metadata); |                         return this.id.execute(args, menu, metadata); | ||||||
|                     } else { |                     } else { | ||||||
|  | @ -456,7 +434,7 @@ export class Command extends BaseCommand { | ||||||
|                 case "user": |                 case "user": | ||||||
|                     const user = await getUserByID(id); |                     const user = await getUserByID(id); | ||||||
| 
 | 
 | ||||||
|                     if (user instanceof User) { |                     if (typeof user !== "string") { | ||||||
|                         menu.args.push(user); |                         menu.args.push(user); | ||||||
|                         return this.id.execute(args, menu, metadata); |                         return this.id.execute(args, menu, metadata); | ||||||
|                     } else { |                     } else { | ||||||
|  | @ -465,7 +443,7 @@ export class Command extends BaseCommand { | ||||||
|                 case "guild": |                 case "guild": | ||||||
|                     const guild = getGuildByID(id); |                     const guild = getGuildByID(id); | ||||||
| 
 | 
 | ||||||
|                     if (guild instanceof Guild) { |                     if (typeof guild !== "string") { | ||||||
|                         menu.args.push(guild); |                         menu.args.push(guild); | ||||||
|                         return this.id.execute(args, menu, metadata); |                         return this.id.execute(args, menu, metadata); | ||||||
|                     } else { |                     } else { | ||||||
|  | @ -489,11 +467,9 @@ export class Command extends BaseCommand { | ||||||
|             return this.any.execute(args.join(" "), menu, metadata); |             return this.any.execute(args.join(" "), menu, metadata); | ||||||
|         } else { |         } else { | ||||||
|             metadata.symbolicArgs.push(`"${param}"`); |             metadata.symbolicArgs.push(`"${param}"`); | ||||||
|             return { |             return `No valid command sequence matching \`${metadata.header} ${metadata.symbolicArgs.join( | ||||||
|                 content: `No valid command sequence matching \`${metadata.header} ${metadata.symbolicArgs.join( |                 " " | ||||||
|                     " " |             )}\` found.`; | ||||||
|                 )}\` found.` |  | ||||||
|             }; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Note: Do NOT add a return statement here. In case one of the other sections is missing
 |         // Note: Do NOT add a return statement here. In case one of the other sections is missing
 | ||||||
|  | @ -682,7 +658,7 @@ export class RestCommand extends BaseCommand { | ||||||
|         combined: string, |         combined: string, | ||||||
|         menu: CommandMenu, |         menu: CommandMenu, | ||||||
|         metadata: ExecuteCommandMetadata |         metadata: ExecuteCommandMetadata | ||||||
|     ): Promise<SingleMessageOptions | null> { |     ): Promise<string | null> { | ||||||
|         // Update inherited properties if the current command specifies a property.
 |         // Update inherited properties if the current command specifies a property.
 | ||||||
|         // In case there are no initial arguments, these should go first so that it can register.
 |         // In case there are no initial arguments, these should go first so that it can register.
 | ||||||
|         if (this.permission !== -1) metadata.permission = this.permission; |         if (this.permission !== -1) metadata.permission = this.permission; | ||||||
|  | @ -716,9 +692,7 @@ export class RestCommand extends BaseCommand { | ||||||
|                 const errorMessage = error.stack ?? error; |                 const errorMessage = error.stack ?? error; | ||||||
|                 console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); |                 console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); | ||||||
| 
 | 
 | ||||||
|                 return { |                 return `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``; | ||||||
|                     content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\`` |  | ||||||
|                 }; |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -743,36 +717,34 @@ export class RestCommand extends BaseCommand { | ||||||
| 
 | 
 | ||||||
| // See if there is anything that'll prevent the user from executing the command.
 | // See if there is anything that'll prevent the user from executing the command.
 | ||||||
| // Returns null if successful, otherwise returns a message with the error.
 | // Returns null if successful, otherwise returns a message with the error.
 | ||||||
| function canExecute(menu: CommandMenu, metadata: ExecuteCommandMetadata): SingleMessageOptions | null { | function canExecute(menu: CommandMenu, metadata: ExecuteCommandMetadata): string | null { | ||||||
|     // 1. Does this command specify a required channel type? If so, does the channel type match?
 |     // 1. Does this command specify a required channel type? If so, does the channel type match?
 | ||||||
|     if ( |     if ( | ||||||
|         metadata.channelType === CHANNEL_TYPE.GUILD && |         metadata.channelType === CHANNEL_TYPE.GUILD && | ||||||
|         (!(menu.channel instanceof GuildChannel) || menu.guild === null || menu.member === null) |         (!(menu.channel instanceof GuildChannel) || menu.guild === null || menu.member === null) | ||||||
|     ) { |     ) { | ||||||
|         return {content: "This command must be executed in a server."}; |         return "This command must be executed in a server."; | ||||||
|     } else if ( |     } else if ( | ||||||
|         metadata.channelType === CHANNEL_TYPE.DM && |         metadata.channelType === CHANNEL_TYPE.DM && | ||||||
|         (menu.channel.type !== "dm" || menu.guild !== null || menu.member !== null) |         (menu.channel.type !== "dm" || menu.guild !== null || menu.member !== null) | ||||||
|     ) { |     ) { | ||||||
|         return {content: "This command must be executed as a direct message."}; |         return "This command must be executed as a direct message."; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // 2. Is this an NSFW command where the channel prevents such use? (DM channels bypass this requirement.)
 |     // 2. Is this an NSFW command where the channel prevents such use? (DM channels bypass this requirement.)
 | ||||||
|     if (metadata.nsfw && menu.channel.type !== "dm" && !menu.channel.nsfw) { |     if (metadata.nsfw && menu.channel.type !== "dm" && !menu.channel.nsfw) { | ||||||
|         return {content: "This command must be executed in either an NSFW channel or as a direct message."}; |         return "This command must be executed in either an NSFW channel or as a direct message."; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // 3. Does the user have permission to execute the command?
 |     // 3. Does the user have permission to execute the command?
 | ||||||
|     if (!hasPermission(menu.author, menu.member, metadata.permission)) { |     if (!hasPermission(menu.author, menu.member, metadata.permission)) { | ||||||
|         const userPermLevel = getPermissionLevel(menu.author, menu.member); |         const userPermLevel = getPermissionLevel(menu.author, menu.member); | ||||||
| 
 | 
 | ||||||
|         return { |         return `You don't have access to this command! Your permission level is \`${getPermissionName( | ||||||
|             content: `You don't have access to this command! Your permission level is \`${getPermissionName( |             userPermLevel | ||||||
|                 userPermLevel |         )}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName( | ||||||
|             )}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName( |             metadata.permission | ||||||
|                 metadata.permission |         )}\` (${metadata.permission}).`; | ||||||
|             )}\` (${metadata.permission}).` |  | ||||||
|         }; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return null; |     return null; | ||||||
|  |  | ||||||
							
								
								
									
										288
									
								
								src/core/libd.ts
									
										
									
									
									
								
							
							
						
						
									
										288
									
								
								src/core/libd.ts
									
										
									
									
									
								
							|  | @ -15,7 +15,9 @@ import { | ||||||
|     MessageAdditions, |     MessageAdditions, | ||||||
|     SplitOptions, |     SplitOptions, | ||||||
|     APIMessage, |     APIMessage, | ||||||
|     StringResolvable |     StringResolvable, | ||||||
|  |     EmojiIdentifierResolvable, | ||||||
|  |     MessageReaction | ||||||
| } from "discord.js"; | } from "discord.js"; | ||||||
| import {unreactEventListeners, replyEventListeners} from "./eventListeners"; | import {unreactEventListeners, replyEventListeners} from "./eventListeners"; | ||||||
| import {client} from "./interface"; | import {client} from "./interface"; | ||||||
|  | @ -31,19 +33,6 @@ export type SendFunction = (( | ||||||
|     ((content: StringResolvable, options: MessageOptions & {split: true | SplitOptions}) => Promise<Message[]>) & |     ((content: StringResolvable, options: MessageOptions & {split: true | SplitOptions}) => Promise<Message[]>) & | ||||||
|     ((content: StringResolvable, options: MessageOptions) => Promise<Message | Message[]>); |     ((content: StringResolvable, options: MessageOptions) => Promise<Message | Message[]>); | ||||||
| 
 | 
 | ||||||
| /** |  | ||||||
|  * Tests if a bot has a certain permission in a specified guild. |  | ||||||
|  */ |  | ||||||
| export function botHasPermission(guild: Guild | null, permission: number): boolean { |  | ||||||
|     return !!guild?.me?.hasPermission(permission); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // The SoonTM Section //
 |  | ||||||
| // Maybe promisify this section to reduce the potential for creating callback hell? Especially if multiple questions in a row are being asked.
 |  | ||||||
| // It's probably a good idea to modularize the base reaction handler so there's less copy pasted code.
 |  | ||||||
| // Maybe also make a reaction handler that listens for when reactions are added and removed.
 |  | ||||||
| // The reaction handler would also run an async function to react in order (parallel to the reaction handler).
 |  | ||||||
| 
 |  | ||||||
| const FIVE_BACKWARDS_EMOJI = "⏪"; | const FIVE_BACKWARDS_EMOJI = "⏪"; | ||||||
| const BACKWARDS_EMOJI = "⬅️"; | const BACKWARDS_EMOJI = "⬅️"; | ||||||
| const FORWARDS_EMOJI = "➡️"; | const FORWARDS_EMOJI = "➡️"; | ||||||
|  | @ -56,34 +45,35 @@ const FIVE_FORWARDS_EMOJI = "⏩"; | ||||||
|  */ |  */ | ||||||
| export async function paginate( | export async function paginate( | ||||||
|     send: SendFunction, |     send: SendFunction, | ||||||
|     senderID: string, |     onTurnPage: (page: number, hasMultiplePages: boolean) => SingleMessageOptions, | ||||||
|     total: number, |     totalPages: number, | ||||||
|     callback: (page: number, hasMultiplePages: boolean) => SingleMessageOptions, |     listenTo: string | null = null, | ||||||
|     duration = 60000 |     duration = 60000 | ||||||
| ) { | ): Promise<void> { | ||||||
|     const hasMultiplePages = total > 1; |     const hasMultiplePages = totalPages > 1; | ||||||
|     const message = await send(callback(0, hasMultiplePages)); |     const message = await send(onTurnPage(0, hasMultiplePages)); | ||||||
| 
 | 
 | ||||||
|     if (hasMultiplePages) { |     if (hasMultiplePages) { | ||||||
|         let page = 0; |         let page = 0; | ||||||
|         const turn = (amount: number) => { |         const turn = (amount: number) => { | ||||||
|             page += amount; |             page += amount; | ||||||
| 
 | 
 | ||||||
|             if (page >= total) { |             if (page >= totalPages) { | ||||||
|                 page %= total; |                 page %= totalPages; | ||||||
|             } else if (page < 0) { |             } else if (page < 0) { | ||||||
|                 // Assuming 3 total pages, it's a bit tricker, but if we just take the modulo of the absolute value (|page| % total), we get (1 2 0 ...), and we just need the pattern (2 1 0 ...). It needs to reverse order except for when it's 0. I want to find a better solution, but for the time being... total - (|page| % total) unless (|page| % total) = 0, then return 0.
 |                 // Assuming 3 total pages, it's a bit tricker, but if we just take the modulo of the absolute value (|page| % total), we get (1 2 0 ...), and we just need the pattern (2 1 0 ...). It needs to reverse order except for when it's 0. I want to find a better solution, but for the time being... total - (|page| % total) unless (|page| % total) = 0, then return 0.
 | ||||||
|                 const flattened = Math.abs(page) % total; |                 const flattened = Math.abs(page) % totalPages; | ||||||
|                 if (flattened !== 0) page = total - flattened; |                 if (flattened !== 0) page = totalPages - flattened; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             message.edit(callback(page, true)); |             message.edit(onTurnPage(page, true)); | ||||||
|         }; |         }; | ||||||
|         const handle = (emote: string, reacterID: string) => { |         const handle = (emote: string, reacterID: string) => { | ||||||
|             if (senderID === reacterID) { |             if (reacterID === listenTo || listenTo === null) { | ||||||
|  |                 collector.resetTimer(); // The timer refresh MUST be present in both react and unreact.
 | ||||||
|                 switch (emote) { |                 switch (emote) { | ||||||
|                     case FIVE_BACKWARDS_EMOJI: |                     case FIVE_BACKWARDS_EMOJI: | ||||||
|                         if (total > 5) turn(-5); |                         if (totalPages > 5) turn(-5); | ||||||
|                         break; |                         break; | ||||||
|                     case BACKWARDS_EMOJI: |                     case BACKWARDS_EMOJI: | ||||||
|                         turn(-1); |                         turn(-1); | ||||||
|  | @ -92,28 +82,28 @@ export async function paginate( | ||||||
|                         turn(1); |                         turn(1); | ||||||
|                         break; |                         break; | ||||||
|                     case FIVE_FORWARDS_EMOJI: |                     case FIVE_FORWARDS_EMOJI: | ||||||
|                         if (total > 5) turn(5); |                         if (totalPages > 5) turn(5); | ||||||
|                         break; |                         break; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         // Listen for reactions and call the handler.
 |         // Listen for reactions and call the handler.
 | ||||||
|         let backwardsReactionFive = total > 5 ? await message.react(FIVE_BACKWARDS_EMOJI) : null; |         let backwardsReactionFive = totalPages > 5 ? await message.react(FIVE_BACKWARDS_EMOJI) : null; | ||||||
|         let backwardsReaction = await message.react(BACKWARDS_EMOJI); |         let backwardsReaction = await message.react(BACKWARDS_EMOJI); | ||||||
|         let forwardsReaction = await message.react(FORWARDS_EMOJI); |         let forwardsReaction = await message.react(FORWARDS_EMOJI); | ||||||
|         let forwardsReactionFive = total > 5 ? await message.react(FIVE_FORWARDS_EMOJI) : null; |         let forwardsReactionFive = totalPages > 5 ? await message.react(FIVE_FORWARDS_EMOJI) : null; | ||||||
|         unreactEventListeners.set(message.id, handle); |         unreactEventListeners.set(message.id, handle); | ||||||
| 
 | 
 | ||||||
|         const collector = message.createReactionCollector( |         const collector = message.createReactionCollector( | ||||||
|             (reaction, user) => { |             (reaction, user) => { | ||||||
|                 if (user.id === senderID) { |                 // This check is actually redundant because of handle().
 | ||||||
|  |                 if (user.id === listenTo || listenTo === null) { | ||||||
|                     // The reason this is inside the call is because it's possible to switch a user's permissions halfway and suddenly throw an error.
 |                     // The reason this is inside the call is because it's possible to switch a user's permissions halfway and suddenly throw an error.
 | ||||||
|                     // This will dynamically adjust for that, switching modes depending on whether it currently has the "Manage Messages" permission.
 |                     // This will dynamically adjust for that, switching modes depending on whether it currently has the "Manage Messages" permission.
 | ||||||
|                     const canDeleteEmotes = botHasPermission(message.guild, Permissions.FLAGS.MANAGE_MESSAGES); |                     const canDeleteEmotes = botHasPermission(message.guild, Permissions.FLAGS.MANAGE_MESSAGES); | ||||||
|                     handle(reaction.emoji.name, user.id); |                     handle(reaction.emoji.name, user.id); | ||||||
|                     if (canDeleteEmotes) reaction.users.remove(user); |                     if (canDeleteEmotes) reaction.users.remove(user); | ||||||
|                     collector.resetTimer(); |  | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 return false; |                 return false; | ||||||
|  | @ -134,100 +124,73 @@ export async function paginate( | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Waits for the sender to either confirm an action or let it pass (and delete the message).
 | //export function generateMulti
 | ||||||
| // This should probably be renamed to "confirm" now that I think of it, "prompt" is better used elsewhere.
 | // paginate after generateonetimeprompt
 | ||||||
| // Append "\n*(This message will automatically be deleted after 10 seconds.)*" in the future?
 |  | ||||||
| /** |  | ||||||
|  * Prompts the user about a decision before following through. |  | ||||||
|  */ |  | ||||||
| export async function prompt(message: Message, senderID: string, onConfirm: () => void, duration = 10000) { |  | ||||||
|     let isDeleted = false; |  | ||||||
| 
 | 
 | ||||||
|     message.react("✅"); | // Returns null if timed out, otherwise, returns the value.
 | ||||||
|     await message.awaitReactions( | export function generateOneTimePrompt<T>( | ||||||
|         (reaction, user) => { |  | ||||||
|             if (user.id === senderID) { |  | ||||||
|                 if (reaction.emoji.name === "✅") { |  | ||||||
|                     onConfirm(); |  | ||||||
|                     isDeleted = true; |  | ||||||
|                     message.delete(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // CollectorFilter requires a boolean to be returned.
 |  | ||||||
|             // My guess is that the return value of awaitReactions can be altered by making a boolean filter.
 |  | ||||||
|             // However, because that's not my concern with this command, I don't have to worry about it.
 |  | ||||||
|             // May as well just set it to false because I'm not concerned with collecting any reactions.
 |  | ||||||
|             return false; |  | ||||||
|         }, |  | ||||||
|         {time: duration} |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     if (!isDeleted) message.delete(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Asks the user for some input using the inline reply feature. The message here is a message you send beforehand.
 |  | ||||||
| // If the reply is rejected, reply with an error message (when stable support comes from discord.js).
 |  | ||||||
| // Append "\n*(Note: Make sure to use Discord's inline reply feature or this won't work!)*" in the future? And also the "you can now reply to this message" edit.
 |  | ||||||
| export function ask( |  | ||||||
|     message: Message, |     message: Message, | ||||||
|     senderID: string, |     stack: {[emote: string]: T}, | ||||||
|     condition: (reply: string) => boolean, |     listenTo: string | null = null, | ||||||
|     onSuccess: () => void, |     duration = 60000 | ||||||
|     onReject: () => string, | ): Promise<T | null> { | ||||||
|     timeout = 60000 |     return new Promise(async (resolve) => { | ||||||
| ) { |         // First, start reacting to the message in order.
 | ||||||
|     const referenceID = `${message.channel.id}-${message.id}`; |         reactInOrder(message, Object.keys(stack)); | ||||||
| 
 | 
 | ||||||
|     replyEventListeners.set(referenceID, (reply) => { |         // Then setup the reaction listener in parallel.
 | ||||||
|         if (reply.author.id === senderID) { |  | ||||||
|             if (condition(reply.content)) { |  | ||||||
|                 onSuccess(); |  | ||||||
|                 replyEventListeners.delete(referenceID); |  | ||||||
|             } else { |  | ||||||
|                 reply.reply(onReject()); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     setTimeout(() => { |  | ||||||
|         replyEventListeners.set(referenceID, (reply) => { |  | ||||||
|             reply.reply("that action timed out, try using the command again"); |  | ||||||
|             replyEventListeners.delete(referenceID); |  | ||||||
|         }); |  | ||||||
|     }, timeout); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function askYesOrNo(message: Message, senderID: string, timeout = 30000): Promise<boolean> { |  | ||||||
|     return new Promise(async (resolve, reject) => { |  | ||||||
|         let isDeleted = false; |  | ||||||
| 
 |  | ||||||
|         await message.react("✅"); |  | ||||||
|         message.react("❌"); |  | ||||||
|         await message.awaitReactions( |         await message.awaitReactions( | ||||||
|             (reaction, user) => { |             (reaction: MessageReaction, user: User) => { | ||||||
|                 if (user.id === senderID) { |                 if (user.id === listenTo || listenTo === null) { | ||||||
|                     const isCheckReacted = reaction.emoji.name === "✅"; |                     const emote = reaction.emoji.name; | ||||||
| 
 | 
 | ||||||
|                     if (isCheckReacted || reaction.emoji.name === "❌") { |                     if (emote in stack) { | ||||||
|                         resolve(isCheckReacted); |                         resolve(stack[emote]); | ||||||
|                         isDeleted = true; |  | ||||||
|                         message.delete(); |                         message.delete(); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|  |                 // CollectorFilter requires a boolean to be returned.
 | ||||||
|  |                 // My guess is that the return value of awaitReactions can be altered by making a boolean filter.
 | ||||||
|  |                 // However, because that's not my concern with this command, I don't have to worry about it.
 | ||||||
|  |                 // May as well just set it to false because I'm not concerned with collecting any reactions.
 | ||||||
|                 return false; |                 return false; | ||||||
|             }, |             }, | ||||||
|             {time: timeout} |             {time: duration} | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         if (!isDeleted) { |         if (!message.deleted) { | ||||||
|             message.delete(); |             message.delete(); | ||||||
|             reject("Prompt timed out."); |             resolve(null); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Start a parallel chain of ordered reactions, allowing a collector to end early.
 | ||||||
|  | // Check if the collector ended early by seeing if the message is already deleted.
 | ||||||
|  | // Though apparently, message.deleted doesn't seem to update fast enough, so just put a try catch block on message.react().
 | ||||||
|  | async function reactInOrder(message: Message, emotes: EmojiIdentifierResolvable[]): Promise<void> { | ||||||
|  |     for (const emote of emotes) { | ||||||
|  |         try { | ||||||
|  |             await message.react(emote); | ||||||
|  |         } catch { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function confirm(message: Message, senderID: string, timeout = 30000): Promise<boolean | null> { | ||||||
|  |     return generateOneTimePrompt( | ||||||
|  |         message, | ||||||
|  |         { | ||||||
|  |             "✅": true, | ||||||
|  |             "❌": false | ||||||
|  |         }, | ||||||
|  |         senderID, | ||||||
|  |         timeout | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // This MUST be split into an array. These emojis are made up of several characters each, adding up to 29 in length.
 | // This MUST be split into an array. These emojis are made up of several characters each, adding up to 29 in length.
 | ||||||
| const multiNumbers = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"]; | const multiNumbers = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"]; | ||||||
| 
 | 
 | ||||||
|  | @ -236,40 +199,47 @@ const multiNumbers = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6 | ||||||
| export async function askMultipleChoice( | export async function askMultipleChoice( | ||||||
|     message: Message, |     message: Message, | ||||||
|     senderID: string, |     senderID: string, | ||||||
|     callbackStack: (() => void)[], |     choices: number, | ||||||
|     timeout = 90000 |     timeout = 90000 | ||||||
| ) { | ): Promise<number | null> { | ||||||
|     if (callbackStack.length > multiNumbers.length) { |     if (choices > multiNumbers.length) | ||||||
|         message.channel.send( |         throw new Error( | ||||||
|             `\`ERROR: The amount of callbacks in "askMultipleChoice" must not exceed the total amount of allowed options (${multiNumbers.length})!\`` |             `askMultipleChoice only accepts up to ${multiNumbers.length} options, ${choices} was provided.` | ||||||
|         ); |         ); | ||||||
|         return; |     const numbers: {[emote: string]: number} = {}; | ||||||
|     } |     for (let i = 0; i < choices; i++) numbers[multiNumbers[i]] = i; | ||||||
|  |     return generateOneTimePrompt(message, numbers, senderID, timeout); | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     let isDeleted = false; | // Asks the user for some input using the inline reply feature. The message here is a message you send beforehand.
 | ||||||
|  | // If the reply is rejected, reply with an error message (when stable support comes from discord.js).
 | ||||||
|  | export function askForReply(message: Message, listenTo: string, timeout?: number): Promise<Message | null> { | ||||||
|  |     return new Promise((resolve) => { | ||||||
|  |         const referenceID = `${message.channel.id}-${message.id}`; | ||||||
| 
 | 
 | ||||||
|     for (let i = 0; i < callbackStack.length; i++) { |         replyEventListeners.set(referenceID, (reply) => { | ||||||
|         await message.react(multiNumbers[i]); |             if (reply.author.id === listenTo) { | ||||||
|     } |                 message.delete(); | ||||||
| 
 |                 replyEventListeners.delete(referenceID); | ||||||
|     await message.awaitReactions( |                 resolve(reply); | ||||||
|         (reaction, user) => { |  | ||||||
|             if (user.id === senderID) { |  | ||||||
|                 const index = multiNumbers.indexOf(reaction.emoji.name); |  | ||||||
| 
 |  | ||||||
|                 if (index !== -1) { |  | ||||||
|                     callbackStack[index](); |  | ||||||
|                     isDeleted = true; |  | ||||||
|                     message.delete(); |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|  |         }); | ||||||
| 
 | 
 | ||||||
|             return false; |         if (timeout) { | ||||||
|         }, |             client.setTimeout(() => { | ||||||
|         {time: timeout} |                 if (!message.deleted) message.delete(); | ||||||
|     ); |                 replyEventListeners.delete(referenceID); | ||||||
|  |                 resolve(null); | ||||||
|  |             }, timeout); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     if (!isDeleted) message.delete(); | /** | ||||||
|  |  * Tests if a bot has a certain permission in a specified guild. | ||||||
|  |  */ | ||||||
|  | export function botHasPermission(guild: Guild | null, permission: number): boolean { | ||||||
|  |     return !!guild?.me?.hasPermission(permission); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // For "get x by y" methods:
 | // For "get x by y" methods:
 | ||||||
|  | @ -277,79 +247,75 @@ export async function askMultipleChoice( | ||||||
| // It's more reliable to get users/members by fetching their IDs. fetch() will searching through the cache anyway.
 | // It's more reliable to get users/members by fetching their IDs. fetch() will searching through the cache anyway.
 | ||||||
| // For guilds, do an extra check to make sure there isn't an outage (guild.available).
 | // For guilds, do an extra check to make sure there isn't an outage (guild.available).
 | ||||||
| 
 | 
 | ||||||
| export function getGuildByID(id: string): Guild | SingleMessageOptions { | export function getGuildByID(id: string): Guild | string { | ||||||
|     const guild = client.guilds.cache.get(id); |     const guild = client.guilds.cache.get(id); | ||||||
| 
 | 
 | ||||||
|     if (guild) { |     if (guild) { | ||||||
|         if (guild.available) return guild; |         if (guild.available) return guild; | ||||||
|         else return {content: `The guild \`${guild.name}\` (ID: \`${id}\`) is unavailable due to an outage.`}; |         else return `The guild \`${guild.name}\` (ID: \`${id}\`) is unavailable due to an outage.`; | ||||||
|     } else { |     } else { | ||||||
|         return { |         return `No guild found by the ID of \`${id}\`!`; | ||||||
|             content: `No guild found by the ID of \`${id}\`!` |  | ||||||
|         }; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getGuildByName(name: string): Guild | SingleMessageOptions { | export function getGuildByName(name: string): Guild | string { | ||||||
|     const query = name.toLowerCase(); |     const query = name.toLowerCase(); | ||||||
|     const guild = client.guilds.cache.find((guild) => guild.name.toLowerCase().includes(query)); |     const guild = client.guilds.cache.find((guild) => guild.name.toLowerCase().includes(query)); | ||||||
| 
 | 
 | ||||||
|     if (guild) { |     if (guild) { | ||||||
|         if (guild.available) return guild; |         if (guild.available) return guild; | ||||||
|         else return {content: `The guild \`${guild.name}\` (ID: \`${guild.id}\`) is unavailable due to an outage.`}; |         else return `The guild \`${guild.name}\` (ID: \`${guild.id}\`) is unavailable due to an outage.`; | ||||||
|     } else { |     } else { | ||||||
|         return { |         return `No guild found by the name of \`${name}\`!`; | ||||||
|             content: `No guild found by the name of \`${name}\`!` |  | ||||||
|         }; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getChannelByID(id: string): Promise<Channel | SingleMessageOptions> { | export async function getChannelByID(id: string): Promise<Channel | string> { | ||||||
|     try { |     try { | ||||||
|         return await client.channels.fetch(id); |         return await client.channels.fetch(id); | ||||||
|     } catch { |     } catch { | ||||||
|         return {content: `No channel found by the ID of \`${id}\`!`}; |         return `No channel found by the ID of \`${id}\`!`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Only go through the cached channels (non-DM channels). Plus, searching DM channels by name wouldn't really make sense, nor do they have names to search anyway.
 | // Only go through the cached channels (non-DM channels). Plus, searching DM channels by name wouldn't really make sense, nor do they have names to search anyway.
 | ||||||
| export function getChannelByName(name: string): GuildChannel | SingleMessageOptions { | export function getChannelByName(name: string): GuildChannel | string { | ||||||
|     const query = name.toLowerCase(); |     const query = name.toLowerCase(); | ||||||
|     const channel = client.channels.cache.find( |     const channel = client.channels.cache.find( | ||||||
|         (channel) => channel instanceof GuildChannel && channel.name.toLowerCase().includes(query) |         (channel) => channel instanceof GuildChannel && channel.name.toLowerCase().includes(query) | ||||||
|     ) as GuildChannel | undefined; |     ) as GuildChannel | undefined; | ||||||
|     if (channel) return channel; |     if (channel) return channel; | ||||||
|     else return {content: `No channel found by the name of \`${name}\`!`}; |     else return `No channel found by the name of \`${name}\`!`; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getMessageByID( | export async function getMessageByID( | ||||||
|     channel: TextChannel | DMChannel | NewsChannel | string, |     channel: TextChannel | DMChannel | NewsChannel | string, | ||||||
|     id: string |     id: string | ||||||
| ): Promise<Message | SingleMessageOptions> { | ): Promise<Message | string> { | ||||||
|     if (typeof channel === "string") { |     if (typeof channel === "string") { | ||||||
|         const targetChannel = await getChannelByID(channel); |         const targetChannel = await getChannelByID(channel); | ||||||
|         if (targetChannel instanceof TextChannel || targetChannel instanceof DMChannel) channel = targetChannel; |         if (targetChannel instanceof TextChannel || targetChannel instanceof DMChannel) channel = targetChannel; | ||||||
|         else if (targetChannel instanceof Channel) return {content: `\`${id}\` isn't a valid text-based channel!`}; |         else if (targetChannel instanceof Channel) return `\`${id}\` isn't a valid text-based channel!`; | ||||||
|         else return targetChannel; |         else return targetChannel; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|         return await channel.messages.fetch(id); |         return await channel.messages.fetch(id); | ||||||
|     } catch { |     } catch { | ||||||
|         return {content: `\`${id}\` isn't a valid message of the channel ${channel}!`}; |         return `\`${id}\` isn't a valid message of the channel ${channel}!`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getUserByID(id: string): Promise<User | SingleMessageOptions> { | export async function getUserByID(id: string): Promise<User | string> { | ||||||
|     try { |     try { | ||||||
|         return await client.users.fetch(id); |         return await client.users.fetch(id); | ||||||
|     } catch { |     } catch { | ||||||
|         return {content: `No user found by the ID of \`${id}\`!`}; |         return `No user found by the ID of \`${id}\`!`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Also check tags (if provided) to narrow down users.
 | // Also check tags (if provided) to narrow down users.
 | ||||||
| export function getUserByName(name: string): User | SingleMessageOptions { | export function getUserByName(name: string): User | string { | ||||||
|     let query = name.toLowerCase(); |     let query = name.toLowerCase(); | ||||||
|     const tagMatch = /^(.+?)#(\d{4})$/.exec(name); |     const tagMatch = /^(.+?)#(\d{4})$/.exec(name); | ||||||
|     let tag: string | null = null; |     let tag: string | null = null; | ||||||
|  | @ -366,19 +332,19 @@ export function getUserByName(name: string): User | SingleMessageOptions { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (user) return user; |     if (user) return user; | ||||||
|     else return {content: `No user found by the name of \`${name}\`!`}; |     else return `No user found by the name of \`${name}\`!`; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getMemberByID(guild: Guild, id: string): Promise<GuildMember | SingleMessageOptions> { | export async function getMemberByID(guild: Guild, id: string): Promise<GuildMember | string> { | ||||||
|     try { |     try { | ||||||
|         return await guild.members.fetch(id); |         return await guild.members.fetch(id); | ||||||
|     } catch { |     } catch { | ||||||
|         return {content: `No member found by the ID of \`${id}\`!`}; |         return `No member found by the ID of \`${id}\`!`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // First checks if a member can be found by that nickname, then check if a member can be found by that username.
 | // First checks if a member can be found by that nickname, then check if a member can be found by that username.
 | ||||||
| export async function getMemberByName(guild: Guild, name: string): Promise<GuildMember | SingleMessageOptions> { | export async function getMemberByName(guild: Guild, name: string): Promise<GuildMember | string> { | ||||||
|     const member = ( |     const member = ( | ||||||
|         await guild.members.fetch({ |         await guild.members.fetch({ | ||||||
|             query: name, |             query: name, | ||||||
|  | @ -395,9 +361,9 @@ export async function getMemberByName(guild: Guild, name: string): Promise<Guild | ||||||
|         if (user instanceof User) { |         if (user instanceof User) { | ||||||
|             const member = guild.members.resolve(user); |             const member = guild.members.resolve(user); | ||||||
|             if (member) return member; |             if (member) return member; | ||||||
|             else return {content: `The user \`${user.tag}\` isn't in this guild!`}; |             else return `The user \`${user.tag}\` isn't in this guild!`; | ||||||
|         } else { |         } else { | ||||||
|             return {content: `No member found by the name of \`${name}\`!`}; |             return `No member found by the name of \`${name}\`!`; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import {client} from "../index"; | import {client} from "../index"; | ||||||
| import {Message, MessageEmbed} from "discord.js"; | import {MessageEmbed} from "discord.js"; | ||||||
| import {getPrefix} from "../structures"; | import {getPrefix} from "../structures"; | ||||||
| import {getMessageByID} from "../core"; | import {getMessageByID} from "../core"; | ||||||
| 
 | 
 | ||||||
|  | @ -13,7 +13,7 @@ client.on("message", async (message) => { | ||||||
|     const linkMessage = await getMessageByID(channelID, messageID); |     const linkMessage = await getMessageByID(channelID, messageID); | ||||||
| 
 | 
 | ||||||
|     // If it's an invalid link (or the bot doesn't have access to it).
 |     // If it's an invalid link (or the bot doesn't have access to it).
 | ||||||
|     if (!(linkMessage instanceof Message)) { |     if (typeof linkMessage === "string") { | ||||||
|         return message.channel.send("I don't have access to that channel!"); |         return message.channel.send("I don't have access to that channel!"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue