mirror of
https://github.com/keanuplayz/TravBot-v3.git
synced 2024-08-15 02:33:12 +00:00
Fixed time setup accounting for differences in day
This commit is contained in:
parent
8da5ad0ca6
commit
d7c18d1b06
2 changed files with 184 additions and 71 deletions
|
@ -4,8 +4,10 @@ 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",
|
||||
|
@ -16,12 +18,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 March
|
||||
- Ends: 1st Sunday November
|
||||
- Starts: 2nd Sunday of March
|
||||
- Ends: 1st Sunday of November
|
||||
|
||||
Europe
|
||||
- Starts: Last Sunday March
|
||||
- Ends: Last Sunday October
|
||||
- Starts: Last Sunday of March
|
||||
- Ends: Last Sunday of October
|
||||
|
||||
Southern Hemisphere
|
||||
- Starts: 1st Sunday of October
|
||||
|
@ -30,12 +32,12 @@ Southern Hemisphere
|
|||
const DST_NOTE_SETUP = `Which daylight savings region most closely matches your own?
|
||||
|
||||
North America (1️⃣)
|
||||
- Starts: 2nd Sunday March
|
||||
- Ends: 1st Sunday November
|
||||
- Starts: 2nd Sunday of March
|
||||
- Ends: 1st Sunday of November
|
||||
|
||||
Europe (2️⃣)
|
||||
- Starts: Last Sunday March
|
||||
- Ends: Last Sunday October
|
||||
- Starts: Last Sunday of March
|
||||
- Ends: Last Sunday of October
|
||||
|
||||
Southern Hemisphere (3️⃣)
|
||||
- Starts: 1st Sunday of October
|
||||
|
@ -99,6 +101,7 @@ 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";
|
||||
|
||||
|
@ -107,13 +110,14 @@ 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: 0x000080,
|
||||
color: TIME_EMBED_COLOR,
|
||||
author: {
|
||||
name: user.username,
|
||||
icon_url: user.displayAvatarURL({
|
||||
|
@ -126,12 +130,16 @@ function getTimeEmbed(user: User) {
|
|||
name: "Local Date",
|
||||
value: localDate
|
||||
},
|
||||
{
|
||||
name: "Day of the Week",
|
||||
value: dayOfWeek
|
||||
},
|
||||
{
|
||||
name: "Local Time",
|
||||
value: localTime
|
||||
},
|
||||
{
|
||||
name: timezone !== null ? "Current Timezone Offset" : "Timezone Offset",
|
||||
name: daylightSavingsRegion !== null ? "Current Timezone Offset" : "Timezone Offset",
|
||||
value: timezoneOffset
|
||||
},
|
||||
{
|
||||
|
@ -160,14 +168,19 @@ 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, prompt}) {
|
||||
async run({author, channel, ask, askYesOrNo, askMultipleChoice}) {
|
||||
const profile = Storage.getUser(author.id);
|
||||
profile.timezone = null;
|
||||
profile.daylightSavingsRegion = null;
|
||||
let hour: number;
|
||||
|
||||
ask(
|
||||
await channel.send(
|
||||
|
@ -175,60 +188,141 @@ export default new Command({
|
|||
),
|
||||
author.id,
|
||||
(reply) => {
|
||||
const hour = parseInt(reply);
|
||||
hour = parseInt(reply);
|
||||
|
||||
if (isNaN(hour)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isValidHour = hour >= 0 && hour <= 23;
|
||||
|
||||
if (isValidHour) {
|
||||
const date = new Date();
|
||||
profile.timezone = hour - date.getUTCHours();
|
||||
}
|
||||
|
||||
return isValidHour;
|
||||
return hour >= 0 && hour <= 23;
|
||||
},
|
||||
async () => {
|
||||
askYesOrNo(
|
||||
await channel.send("Does your timezone change based on daylight savings?"),
|
||||
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)
|
||||
);
|
||||
};
|
||||
// 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.
|
||||
|
||||
if (hasDST) {
|
||||
const finalizeDST = (region: DST) => {
|
||||
profile.daylightSavingsRegion = region;
|
||||
// (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.
|
||||
|
||||
// If daylight savings is active, subtract the timezone offset by one to store the standard time.
|
||||
if (hasDaylightSavings(region)) {
|
||||
(profile.timezone as number)--;
|
||||
}
|
||||
// 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)
|
||||
|
||||
finalize();
|
||||
};
|
||||
// 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
|
||||
|
||||
askMultipleChoice(await channel.send(DST_NOTE_SETUP), author.id, [
|
||||
() => finalizeDST("na"),
|
||||
() => finalizeDST("eu"),
|
||||
() => finalizeDST("sh")
|
||||
]);
|
||||
} else {
|
||||
finalize();
|
||||
// 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(
|
||||
await channel.send("Does your timezone change based on daylight savings?"),
|
||||
author.id
|
||||
);
|
||||
|
||||
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 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();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
return "you need to enter in a valid integer between 0 to 23";
|
||||
}
|
||||
() => "you need to enter in a valid integer between 0 to 23"
|
||||
);
|
||||
}
|
||||
}),
|
||||
|
@ -256,12 +350,16 @@ export default new Command({
|
|||
|
||||
channel.send({
|
||||
embed: {
|
||||
color: 0x000080,
|
||||
color: TIME_EMBED_COLOR,
|
||||
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)
|
||||
|
|
|
@ -46,7 +46,7 @@ export interface CommonLibrary {
|
|||
onReject: () => string,
|
||||
timeout?: number
|
||||
) => void;
|
||||
askYesOrNo: (message: Message, senderID: string, onSuccess: (condition: boolean) => void, timeout?: number) => void;
|
||||
askYesOrNo: (message: Message, senderID: string, timeout?: number) => Promise<boolean>;
|
||||
askMultipleChoice: (
|
||||
message: Message,
|
||||
senderID: string,
|
||||
|
@ -196,6 +196,8 @@ 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 (
|
||||
|
@ -315,36 +317,49 @@ $.ask = async (
|
|||
}, timeout);
|
||||
};
|
||||
|
||||
$.askYesOrNo = async (message: Message, senderID: string, onSuccess: (condition: boolean) => void, timeout = 30000) => {
|
||||
let isDeleted = false;
|
||||
$.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(
|
||||
(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 === "❌") {
|
||||
onSuccess(isCheckReacted);
|
||||
isDeleted = true;
|
||||
message.delete();
|
||||
if (isCheckReacted || reaction.emoji.name === "❌") {
|
||||
resolve(isCheckReacted);
|
||||
isDeleted = true;
|
||||
message.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
{time: timeout}
|
||||
);
|
||||
return false;
|
||||
},
|
||||
{time: timeout}
|
||||
);
|
||||
|
||||
if (!isDeleted) message.delete();
|
||||
if (!isDeleted) {
|
||||
message.delete();
|
||||
reject("Prompt timed out.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 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++) {
|
||||
|
|
Loading…
Reference in a new issue