I have a Discord.js bot that retrieves prayer times using an external API and sets reminders for these times. The bot works fine on my localhost, where the timezone is set to Cairo, Egypt on the localhost. However, when I deploy the bot to hosting environments like Repl.it or Render, I notice a discrepancy in time displayed in the bot's output. where the time zone of set on the command is 3 hours more than the current one.
Here's my code:
const discord = require("discord.js");
const {
SlashCommandBuilder,
StringSelectMenuBuilder,
ActionRowBuilder,
ButtonBuilder,
} = require("discord.js");
const fetch = require("node-fetch");
const tc = require("../../functions/TimeConvert");
const cfl = require("../../functions/CapitalizedChar");
const schema = require("../../schema/TimeOut-Schema");
const moment = require("moment");
const momentTz = require("moment-timezone");
let dinMS;
module.exports = {
clientpermissions: [
discord.PermissionsBitField.Flags.EmbedLinks,
discord.PermissionsBitField.Flags.ReadMessageHistory,
],
data: new SlashCommandBuilder()
.setName("prays")
.setDescription("Replies with prays times!")
.addStringOption((option) =>
option
.setName("country")
.setDescription("Enter country name.")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("city")
.setDescription("Enter city name.")
.setRequired(true)
),
async execute(client, interaction) {
try {
async function LoadButtons(MAX_BTNS, arr) {
const MAX_BUTTONS_PER_ROW = 5;
const MAX_BUTTONS_PER_MESSAGE = MAX_BTNS;
const buttonRows = [];
let currentRow = new ActionRowBuilder();
for (let i = 0; i < arr.length; i++) {
const [label, value] = arr[i];
const button = new ButtonBuilder()
.setLabel(label)
.setCustomId(`${label} ${value}`)
.setStyle(1);
if (
currentRow.components.length >= MAX_BUTTONS_PER_ROW ||
buttonRows.length * MAX_BUTTONS_PER_ROW +
currentRow.components.length >=
MAX_BUTTONS_PER_MESSAGE
) {
buttonRows.push(currentRow);
currentRow = new ActionRowBuilder();
}
currentRow.addComponents(button);
}
if (currentRow.components.length > 0) {
buttonRows.push(currentRow);
}
return buttonRows;
}
async function setReminder(interaction, timezone, prayerTime) {
try {
let data = await schema.findOne({ userId: interaction.user.id });
if (!data) {
data = await schema.create({ userId: interaction.user.id });
}
if (data.Reminder.current) {
await interaction.channel
.send(
`${interaction.user}, looks like you already have an \`active reminder\`, do you want to add this one instead? \`(y/n)\``
)
.catch(() => null);
const filter = (_message) =>
interaction.user.id === _message.author.id &&
["y", "n", "yes", "no"].includes(_message.content.toLowerCase());
const proceed = await interaction.channel
.awaitMessages({ filter, max: 1, time: 40000, errors: ["time"] })
.then((collected) =>
["y", "yes"].includes(collected.first().content.toLowerCase())
? true
: false
)
.catch(() => false);
if (!proceed) {
return interaction.channel
.send({
content: `\\❌ | **${interaction.user.tag}**, Cancelled the \`reminder\`!`,
ephemeral: true,
})
.catch(() => null);
}
}
const [hours, minutes] = prayerTime.split(":");
let currentDatetime;
await CurrentTime(timezone).then(async (time) => {
currentDatetime = time.date.toLocaleString("en-US", {
timeZone: time.timezone,
hour12: false,
});
});
const [date, time] = currentDatetime.split(", ");
const [month, day, year] = date.split("/");
const [hour, minute, second] = time.split(":");
const prayerDatetime = new Date(
+year,
month - 1,
day,
hours,
minutes
).getTime();
const currentTime = new Date(
+year,
month - 1,
day,
hour,
minute,
second
).getTime();
const TimeDiff = (prayerDatetime - currentTime);
if (currentTime >= prayerDatetime || TimeDiff <= 0) {
return interaction
.reply({
content: `\\❌ ${interaction.user}, This pray time has already passed!`,
ephemeral: true,
})
.catch(() => null);
}
const Reason = interaction.customId.split(" ")[0];
data.Reminder.current = true;
data.Reminder.time = prayerDatetime;
data.Reminder.reason = `${Reason} will start soon`;
data.Reminder.timezone = timezone;
await data.save();
/*
const duration = moment.duration(TimeDiff, "milliseconds");
const formattedDuration = duration.format(
"H [hours,] m [minutes, and] s [seconds,]"
);
*/
const dnEmbed = new discord.EmbedBuilder()
.setAuthor({
name: "| Reminder Set!",
iconURL: interaction.user.displayAvatarURL(),
})
.setDescription(
`Successfully set \`${interaction.user.tag}'s\` reminder!`
)
.addFields(
{
name: "❯ Remind You In:",
value: `<t:${prayerDatetime / 1000}:R>`,
},
{
name: "❯ Remind Reason:",
value: `${Reason} will start soon`,
}
)
.setColor("Green")
.setTimestamp()
.setFooter({
text: "Successfully set the reminder!",
iconURL: client.user.displayAvatarURL(),
});
interaction.channel
.send({ embeds: [dnEmbed], ephemeral: true })
.catch(() => null);
} catch (err) {
console.error(err);
}
}
async function CurrentTime(timezone) {
const now = momentTz().tz(timezone);
const isDST = now.isDST();
let options = {
timeZone: timezone,
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: false,
};
let formatter = new Intl.DateTimeFormat([], options);
const d = new Date(formatter.format(now.toDate()).split(",").join(" "));
if (isDST) {
d.setHours(d.getHours() + 1);
}
return {
date: d,
timezone: timezone,
MiliSeconds: Math.floor(d.getTime() / 1000),
};
}
const country = interaction.options.getString("country");
const city = interaction.options.getString("city");
const url = `https://api.aladhan.com/v1/timingsByCity?city=${city}&country=${country}`;
const options = {
method: "GET",
};
fetch(url, options)
.then((res) => res.json())
.then(async (json) => {
if (json.code != 200) {
return interaction.editReply({
content:
"<:error:888264104081522698> Please enter valid country and city in the options!",
});
}
let rows = [];
const json_data = json.data.timings;
let result = Object.entries(json_data)
.filter(([key]) => key !== "Imsak" && key !== "Sunset")
.map(([key, value]) => [key, value]);
rows = await LoadButtons(25, result);
const timezone = json.data.meta.timezone;
try {
if (timezone) {
dinMS = await CurrentTime(timezone).then(
(time) => time.MiliSeconds
);
} else {
return await interaction.editReply({
content:
"<:error:888264104081522698> I can't identify this timezone, please write the right `City, Country`!",
});
}
} catch (e) {
console.error(e);
return await interaction.editReply({
content:
"<:error:888264104081522698> I can't identify this timezone, please write the right `City, Country`!",
});
}
let pTimeInMS;
let str;
let nxtStr = null;
let num = -1;
let marked = false;
for (const pTime of result) {
num++;
str = `${json.data.date.readable.split(" ").join("/")} ${pTime[1]}`;
const [dateComponents, timeComponents] = str.split(" ");
const [day, month, year] = dateComponents.split("/");
const [hours, minutes] = timeComponents.split(":");
const formatter = new Intl.DateTimeFormat([], { month: "numeric" });
const monthNumber = formatter.format(
new Date(`${year}-${month}-${day}`)
);
pTimeInMS = Math.floor(
new Date(
+year,
monthNumber - 1,
+day,
+hours,
+minutes,
0
).getTime() / 1000
);
if (dinMS < pTimeInMS) {
if (!marked) {
const TimeDiff = Math.floor(pTimeInMS - dinMS) * 1000;
nxtStr = `${pTime[0]} - *${moment
.duration(TimeDiff, "milliseconds")
.humanize()}*!`;
result[num][0] = pTime[0] + "(`Next`)";
marked = true;
}
}
}
const ActionRow = new ActionRowBuilder().addComponents(
new StringSelectMenuBuilder()
.setCustomId("kwthbek4m221pyddhwp4")
.setPlaceholder("Nothing selected!")
.addOptions([
{
label: "Remind Before",
description: `Choose this option to set a time to remind to before the pray (like 5 minutes before)`,
value: "remind_before1",
},
{
label: "Set timezone",
description: `Choose this option to set the default timezone for the cmd.`,
value: "timezone1",
},
{
label: "Auto Reminder",
description: `Choose this option to get notified before every pray.`,
value: "auto_reminder1",
},
])
);
rows.push(ActionRow);
const embed = new discord.EmbedBuilder()
.setAuthor({
name: interaction.user.username,
iconURL: interaction.user.displayAvatarURL({ dynamic: true }),
})
.setDescription(
[
`<:Tag:836168214525509653> Praying times for country \`${cfl.capitalizeFirstLetter(
country
)}\` in city \`${cfl.capitalizeFirstLetter(city)}\`!`,
`You can set a \`reminder\` for the next pray from the **buttons** below, use the **action Row** to set the \`default timezone\` and if you want the \`auto reminder\`.`,
].join("\n")
)
.addFields(
{
name: "<:star:888264104026992670> Date",
value: `<t:${dinMS}>`,
inline: false,
},
{
name: "<:Timer:853494926850654249> Next Pray in:",
value: nxtStr || "Tomorrow!",
inline: false,
},
{ name: " ", value: ` `, inline: false }
)
.addFields(
result.flatMap((i) => [
{
name: i[0],
value: `\`\`\`${tc.tConvert(i[1])}\`\`\``,
inline: true,
},
])
)
.setFooter({
text: [
`Based on: ${
json.data.meta.method.name
? json.data.meta.method.name
: "Unknown"
}\n`,
"Times may vary!",
].join(" "),
iconURL: client.user.displayAvatarURL({ dynamic: true }),
})
.setTimestamp();
await interaction
.editReply({ embeds: [embed], components: rows })
.then((msg) => {
const filter = (int) => int.user.id == interaction.user.id;
const collector = msg.createMessageComponentCollector({
filter: filter,
time: 180000,
fetch: true,
});
collector.on("collect", async (interaction) => {
if (interaction.isButton()) {
interaction.deferUpdate().then(async () => {
const PrayingTime = result.find(
(time) => time[1] === interaction.customId.split(" ")[1]
)[1];
await setReminder(interaction, timezone, PrayingTime);
});
} else {
return;
}
});
collector.on("end", async () => {
return await msg.edit({ embeds: [embed], components: [] });
});
});
})
.catch((err) => {
console.error(err);
interaction.channel.send({
content: `<:error:888264104081522698> ${interaction.user} Something went wrong, please try again later!`,
});
});
} catch (error) {
console.error("Error in execute function:", error);
interaction.reply({
content: "An error occurred. Please try again later.",
ephemeral: true,
});
}
},
};
Additional Information:
I'm using Moment.js version 2.30.1 and Moment Timezone version 0.5.40. Here's my package.json file for reference:
{
"name": "wolfy-bot",
"version": "2.3.9",
"main": "index.js",
"scripts": {
"start": "node index.js",
"build": "node build.js",
"test": "node test.js"
},
"author": "Yousef osama(WOLF)",
"license": "ISC",
"description": "",
"dependencies": {
"@discordjs/opus": "^0.9.0",
"@discordjs/voice": "^0.15.0",
"axios": "^0.27.2",
"canvacord": "^6.0.1",
"canvas": "^2.11.2",
"discord-player": "^5.4.0",
"discord.js": "^14.8.0",
"dotenv": "^16.0.1",
"express": "^4.17.1",
"ffmpeg-static": "^4.2.7",
"fs-extra": "^10.0.0",
"got": "^11.8.5",
"he": "^1.2.0",
"html2markdown": "^1.1.0",
"humanize-duration": "^3.28.0",
"jsdom": "^16.4.0",
"math-expression-evaluator": "^1.4.0",
"minecraft-player": "^1.0.1",
"moment": "^2.30.1",
"moment-timezone": "^0.5.40",
"mongoose": "^5.13.20",
"ms": "^2.1.3",
"node": "^16.19.0",
"node-fetch": "^2.6.9",
"openai": "^3.1.0",
"opusscript": "^0.0.8",
"parse-ms": "^2.1.0",
"play-dl": "^1.9.6",
"snekfetch": "^4.0.4",
"sourcebin": "^5.0.0",
"sourcebin_js": "^0.0.3-ignore",
"txtgen": "3.0.1",
"uuid": "^8.3.2",
"weather-js": "^2.0.0",
"youtube-sr": "^4.3.4",
"ytdl-core": "^4.8.3"
}
}
Any insights or suggestions would be greatly appreciated. Thank you!
I've verified that I'm using Moment.js and Moment Timezone libraries to handle timezones correctly in my code. Date, nextPray duration is not correct on server and correct on localhost, but next pray string that is next to the next pray works fine on both localhost and server host.
The code includes a few
Dateconstructor calls vianew Date()followed bygetTime().This will mean the UNIX time that results will be interpreted as however many milliseconds from January 1, 1970 (anchored on UTC) to that date, but that target date will be based on the environment locale. And naturally, since "12 noon" in one country is ahead/behind "12 noon" in another country, that means the resulting value can then vary.
In node, the default timezone that will be used is based on that which is set on the parent operating system.
Let's say you have this:
In an environment with a server locale of
Europe/Londonthis returnsIn an
America/Montrealserver locale this returnsWhen converting the difference between these numbers from ms to hours, it equates to 5 hours.
If the dates you are ingesting are intended to be interepreted as UTC, you should instead use the
Date.UTC()method. That means that the input date is assumed to be UTC. the environment locale is not used and UTC is instead used every time.If they are intended to be interpreted as being in another timezone you could use
moment.tz()to correctly ingest them with respect to that timezone, which you have to manually pass in.