diff --git a/src/commands/utilities/time.ts b/src/commands/utilities/time.ts index 8cdecc1..de4fe12 100644 --- a/src/commands/utilities/time.ts +++ b/src/commands/utilities/time.ts @@ -4,10 +4,8 @@ import {User} from "discord.js"; import moment from "moment"; const DATE_FORMAT = "D MMMM YYYY"; -const DOW_FORMAT = "dddd"; const TIME_FORMAT = "HH:mm:ss"; type DST = "na" | "eu" | "sh"; -const TIME_EMBED_COLOR = 0x191970; const DAYLIGHT_SAVINGS_REGIONS: {[region in DST]: string} = { na: "North America", @@ -18,12 +16,12 @@ const DAYLIGHT_SAVINGS_REGIONS: {[region in DST]: string} = { const DST_NOTE_INFO = `*Note: To make things simple, the way the bot will handle specific points in time when switching Daylight Savings is just to switch at UTC 00:00, ignoring local timezones. After all, there's no need to get this down to the exact hour.* North America -- Starts: 2nd Sunday of March -- Ends: 1st Sunday of November +- Starts: 2nd Sunday March +- Ends: 1st Sunday November Europe -- Starts: Last Sunday of March -- Ends: Last Sunday of October +- Starts: Last Sunday March +- Ends: Last Sunday October Southern Hemisphere - Starts: 1st Sunday of October @@ -32,12 +30,12 @@ Southern Hemisphere const DST_NOTE_SETUP = `Which daylight savings region most closely matches your own? North America (1️⃣) -- Starts: 2nd Sunday of March -- Ends: 1st Sunday of November +- Starts: 2nd Sunday March +- Ends: 1st Sunday November Europe (2️⃣) -- Starts: Last Sunday of March -- Ends: Last Sunday of October +- Starts: Last Sunday March +- Ends: Last Sunday October Southern Hemisphere (3️⃣) - Starts: 1st Sunday of October @@ -101,7 +99,6 @@ function hasDaylightSavings(region: DST) { function getTimeEmbed(user: User) { const {timezone, daylightSavingsRegion} = Storage.getUser(user.id); let localDate = "N/A"; - let dayOfWeek = "N/A"; let localTime = "N/A"; let timezoneOffset = "N/A"; @@ -110,14 +107,13 @@ function getTimeEmbed(user: User) { const daylightTimezone = timezone + daylightSavingsOffset; const now = moment().utcOffset(daylightTimezone * 60); localDate = now.format(DATE_FORMAT); - dayOfWeek = now.format(DOW_FORMAT); localTime = now.format(TIME_FORMAT); - timezoneOffset = daylightTimezone >= 0 ? `+${daylightTimezone}` : daylightTimezone.toString(); + timezoneOffset = daylightTimezone > 0 ? `+${daylightTimezone}` : daylightTimezone.toString(); } const embed = { embed: { - color: TIME_EMBED_COLOR, + color: 0x000080, author: { name: user.username, icon_url: user.displayAvatarURL({ @@ -130,16 +126,12 @@ function getTimeEmbed(user: User) { name: "Local Date", value: localDate }, - { - name: "Day of the Week", - value: dayOfWeek - }, { name: "Local Time", value: localTime }, { - name: daylightSavingsRegion !== null ? "Current Timezone Offset" : "Timezone Offset", + name: timezone !== null ? "Current Timezone Offset" : "Timezone Offset", value: timezoneOffset }, { @@ -168,19 +160,14 @@ function getTimeEmbed(user: User) { export default new Command({ description: "Show others what time it is for you.", - aliases: ["tz"], async run({channel, author}) { channel.send(getTimeEmbed(author)); }, subcommands: { - // Welcome to callback hell. We hope you enjoy your stay here! setup: new Command({ description: "Registers your timezone information for the bot.", - async run({author, channel, ask, askYesOrNo, askMultipleChoice}) { + async run({author, channel, ask, askYesOrNo, askMultipleChoice, prompt}) { const profile = Storage.getUser(author.id); - profile.timezone = null; - profile.daylightSavingsRegion = null; - let hour: number; ask( await channel.send( @@ -188,141 +175,60 @@ export default new Command({ ), author.id, (reply) => { - hour = parseInt(reply); + const hour = parseInt(reply); if (isNaN(hour)) { return false; } - return hour >= 0 && hour <= 23; + const isValidHour = hour >= 0 && hour <= 23; + + if (isValidHour) { + const date = new Date(); + profile.timezone = hour - date.getUTCHours(); + } + + return isValidHour; }, 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 channel.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( + askYesOrNo( await channel.send("Does your timezone change based on daylight savings?"), - author.id - ); + author.id, + async (hasDST) => { + const finalize = () => { + Storage.save(); + channel.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) + ); + }; - const finalize = () => { - Storage.save(); - channel.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 (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 as number)--; + } - // If daylight savings is active, subtract the timezone offset by one to store the standard time. - if (hasDaylightSavings(region)) { - profile.timezone!--; + finalize(); + }; + + askMultipleChoice(await channel.send(DST_NOTE_SETUP), author.id, [ + () => finalizeDST("na"), + () => finalizeDST("eu"), + () => finalizeDST("sh") + ]); + } else { + finalize(); } - - finalize(); - }; - - askMultipleChoice(await channel.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" + () => { + return "you need to enter in a valid integer between 0 to 23"; + } ); } }), @@ -350,16 +256,12 @@ export default new Command({ channel.send({ embed: { - color: TIME_EMBED_COLOR, + color: 0x000080, fields: [ { name: "Local Date", value: time.format(DATE_FORMAT) }, - { - name: "Day of the Week", - value: time.format(DOW_FORMAT) - }, { name: "Local Time", value: time.format(TIME_FORMAT) diff --git a/src/core/lib.ts b/src/core/lib.ts index 42852d9..f8efea6 100644 --- a/src/core/lib.ts +++ b/src/core/lib.ts @@ -46,7 +46,7 @@ export interface CommonLibrary { onReject: () => string, timeout?: number ) => void; - askYesOrNo: (message: Message, senderID: string, timeout?: number) => Promise; + askYesOrNo: (message: Message, senderID: string, onSuccess: (condition: boolean) => void, timeout?: number) => void; askMultipleChoice: ( message: Message, senderID: string, @@ -196,8 +196,6 @@ export function updateGlobalEmoteRegistry(): void { FileManager.write("emote-registry", data, true); } -// Maybe promisify this section to reduce the potential for creating callback hell? Especially if multiple questions in a row are being asked. - // Pagination function that allows for customization via a callback. // Define your own pages outside the function because this only manages the actual turning of pages. $.paginate = async ( @@ -317,49 +315,36 @@ $.ask = async ( }, timeout); }; -$.askYesOrNo = (message: Message, senderID: string, timeout = 30000): Promise => { - return new Promise(async (resolve, reject) => { - let isDeleted = false; +$.askYesOrNo = async (message: Message, senderID: string, onSuccess: (condition: boolean) => void, timeout = 30000) => { + let isDeleted = false; - await message.react("✅"); - message.react("❌"); - await message.awaitReactions( - (reaction, user) => { - if (user.id === senderID) { - const isCheckReacted = reaction.emoji.name === "✅"; + await message.react("✅"); + message.react("❌"); + await message.awaitReactions( + (reaction, user) => { + if (user.id === senderID) { + const isCheckReacted = reaction.emoji.name === "✅"; - if (isCheckReacted || reaction.emoji.name === "❌") { - resolve(isCheckReacted); - isDeleted = true; - message.delete(); - } + if (isCheckReacted || reaction.emoji.name === "❌") { + onSuccess(isCheckReacted); + isDeleted = true; + message.delete(); } + } - return false; - }, - {time: timeout} - ); + return false; + }, + {time: timeout} + ); - if (!isDeleted) { - message.delete(); - reject("Prompt timed out."); - } - }); + if (!isDeleted) message.delete(); }; // 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️⃣", "🔟"]; // This will bring up an option to let the user choose between one option out of many. -// This definitely needs a single callback alternative, because using the numerical version isn't actually that uncommon of a pattern. $.askMultipleChoice = async (message: Message, senderID: string, callbackStack: (() => void)[], timeout = 90000) => { - if (callbackStack.length > multiNumbers.length) { - message.channel.send( - `\`ERROR: The amount of callbacks in "askMultipleChoice" must not exceed the total amount of allowed options (${multiNumbers.length})!\`` - ); - return; - } - let isDeleted = false; for (let i = 0; i < callbackStack.length; i++) { diff --git a/src/core/structures.ts b/src/core/structures.ts index 37b2194..b37e169 100644 --- a/src/core/structures.ts +++ b/src/core/structures.ts @@ -110,18 +110,7 @@ if (process.argv[2] === "dev") { } export function getPrefix(guild: DiscordGuild | null): string { - let prefix = Config.prefix; - - if (guild) { - const possibleGuildPrefix = Storage.getGuild(guild.id).prefix; - - // Here, lossy comparison works in our favor because you wouldn't want an empty string to trigger the prefix. - if (possibleGuildPrefix) { - prefix = possibleGuildPrefix; - } - } - - return prefix; + return Storage.getGuild(guild?.id || "N/A").prefix ?? Config.prefix; } export interface EmoteRegistryDumpEntry { diff --git a/src/events/message.ts b/src/events/message.ts index 107c8cf..00e6458 100644 --- a/src/events/message.ts +++ b/src/events/message.ts @@ -22,41 +22,16 @@ export default new Event<"message">({ replyEventListeners.get(`${reference.channelID}-${reference.messageID}`)?.(message); } - let prefix = getPrefix(message.guild); - const originalPrefix = prefix; - let exitEarly = !message.content.startsWith(prefix); - const clientUser = message.client.user; - let usesBotSpecificPrefix = false; + const prefix = getPrefix(message.guild); - // If the client user exists, check if it starts with the bot-specific prefix. - if (clientUser) { - // If the prefix starts with the bot-specific prefix, go off that instead (these two options must mutually exclude each other). - // The pattern here has an optional space at the end to capture that and make it not mess with the header and args. - const matches = message.content.match(new RegExp(`^<@!?${clientUser.id}> ?`)); - - if (matches) { - prefix = matches[0]; - exitEarly = false; - usesBotSpecificPrefix = true; - } + if (!message.content.startsWith(prefix) && !message.reference) { + if (message.client.user && message.mentions.has(message.client.user)) + message.channel.send(`${message.author.toString()}, my prefix on this guild is \`${prefix}\`.`); + return; } - // If it doesn't start with the current normal prefix or the bot-specific unique prefix, exit the thread of execution early. - // Inline replies should still be captured here because if it doesn't exit early, two characters for a two-length prefix would still trigger commands. - if (exitEarly) return; - const [header, ...args] = message.content.substring(prefix.length).split(/ +/); - // If the message is just the prefix itself, move onto this block. - if (header === "" && args.length === 0) { - // I moved the bot-specific prefix to a separate conditional block to separate the logic. - // And because it listens for the mention as a prefix instead of a free-form mention, inline replies (probably) shouldn't ever trigger this unintentionally. - if (usesBotSpecificPrefix) { - message.channel.send(`${message.author.toString()}, my prefix on this guild is \`${originalPrefix}\`.`); - return; - } - } - if (!commands.has(header)) return; if ( @@ -91,7 +66,7 @@ export default new Event<"message">({ for (let param of args) { if (command.endpoint) { if (command.subcommands.size > 0 || command.user || command.number || command.any) - $.warn(`An endpoint cannot have subcommands! Check ${originalPrefix}${header} again.`); + $.warn(`An endpoint cannot have subcommands! Check ${prefix}${header} again.`); isEndpoint = true; break; }