1195 lines
20 KiB
JavaScript
1195 lines
20 KiB
JavaScript
const {
|
|
EmbedBuilder,
|
|
AttachmentBuilder,
|
|
GuildScheduledEventEntityType,
|
|
GuildScheduledEventPrivacyLevel,
|
|
PermissionFlagsBits,
|
|
MessageFlags
|
|
} = require("discord.js");
|
|
|
|
const { ObjectId } = require("mongodb");
|
|
const { randomUUID } = require("node:crypto");
|
|
const path = require("node:path");
|
|
|
|
const { mClient } = require("..");
|
|
|
|
|
|
/*
|
|
* CONFIG
|
|
*/
|
|
|
|
const DATABASE = "neo-db";
|
|
|
|
const DUTY_COLLECTION =
|
|
"items_duties";
|
|
|
|
const EVENT_COLLECTION =
|
|
"items_events";
|
|
|
|
|
|
const RAID_PLANNER_CHANNEL_ID =
|
|
process.env.RAID_PLANNER_CHANNEL_ID;
|
|
|
|
|
|
const RAID_TIMEZONE =
|
|
process.env.RAID_TIMEZONE ||
|
|
"Europe/Berlin";
|
|
|
|
|
|
/*
|
|
* LOCAL ASSETS
|
|
*/
|
|
|
|
const dutyIconDirectory =
|
|
path.resolve(
|
|
__dirname,
|
|
"../assets/duties"
|
|
);
|
|
|
|
|
|
const DUTY_ICONS = {
|
|
Guildhest:
|
|
"Guildhest.png",
|
|
|
|
Dungeon:
|
|
"Dungeon.png",
|
|
|
|
Trial:
|
|
"Trial.png",
|
|
|
|
Raid:
|
|
"Raid.png",
|
|
|
|
AllianceRaid:
|
|
"Raid.png",
|
|
|
|
VariantDungeon:
|
|
"Variant_and_criterion_dungeons_icon1.png",
|
|
|
|
Criterion:
|
|
"Variant_and_criterion_dungeons_icon1.png"
|
|
};
|
|
|
|
|
|
const DUTY_DIFFICULTY_ICONS = {
|
|
Ultimate:
|
|
"Ultimate_Raid.png",
|
|
|
|
Chaotic:
|
|
"Chaotic_Alliance_Raid.png"
|
|
};
|
|
|
|
|
|
/*
|
|
* ROLE EMOJIS
|
|
*/
|
|
|
|
const RAID_EMOJIS = {
|
|
Tank: {
|
|
id:
|
|
"1537931883369726042",
|
|
|
|
display:
|
|
"<:TankRole:1537931883369726042>"
|
|
},
|
|
|
|
Healer: {
|
|
id:
|
|
"1537931880756805704",
|
|
|
|
display:
|
|
"<:HealerRole:1537931880756805704>"
|
|
},
|
|
|
|
DPS: {
|
|
id:
|
|
"1537931878668050442",
|
|
|
|
display:
|
|
"<:DPSRole:1537931878668050442>"
|
|
},
|
|
|
|
Flex: {
|
|
id:
|
|
"1537948665606574210",
|
|
|
|
display:
|
|
"<:AllRounderRole:1537948665606574210>"
|
|
}
|
|
};
|
|
|
|
|
|
module.exports = {
|
|
customId: "raidPlan",
|
|
|
|
async execute(interaction) {
|
|
return handleRaidPlan(
|
|
interaction
|
|
);
|
|
}
|
|
};
|
|
|
|
|
|
/*
|
|
* MAIN MODAL HANDLER
|
|
*/
|
|
|
|
async function handleRaidPlan(interaction) {
|
|
await interaction.deferReply({
|
|
flags:
|
|
MessageFlags.Ephemeral
|
|
});
|
|
|
|
|
|
if (!interaction.guild) {
|
|
return interaction.editReply({
|
|
content:
|
|
"Raid planning can only be used inside a server."
|
|
});
|
|
}
|
|
|
|
|
|
if (!RAID_PLANNER_CHANNEL_ID) {
|
|
return interaction.editReply({
|
|
content:
|
|
"RAID_PLANNER_CHANNEL_ID is not configured."
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* raidPlan:<dutyId>
|
|
*/
|
|
|
|
const [
|
|
prefix,
|
|
dutyId
|
|
] =
|
|
interaction.customId
|
|
.split(":");
|
|
|
|
|
|
if (
|
|
prefix !== "raidPlan" ||
|
|
!ObjectId.isValid(dutyId)
|
|
) {
|
|
return interaction.editReply({
|
|
content:
|
|
"Invalid raid planner data."
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* LOAD DUTY
|
|
*/
|
|
|
|
const db =
|
|
mClient.db(DATABASE);
|
|
|
|
|
|
const duty =
|
|
await db
|
|
.collection(
|
|
DUTY_COLLECTION
|
|
)
|
|
.findOne({
|
|
_id:
|
|
new ObjectId(
|
|
dutyId
|
|
)
|
|
});
|
|
|
|
|
|
if (!duty) {
|
|
return interaction.editReply({
|
|
content:
|
|
"The selected duty could not be found."
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* MODAL VALUES
|
|
*/
|
|
|
|
const name =
|
|
interaction.fields
|
|
.getTextInputValue(
|
|
"raid-name"
|
|
)
|
|
.trim();
|
|
|
|
|
|
const description =
|
|
interaction.fields
|
|
.getTextInputValue(
|
|
"raid-description"
|
|
)
|
|
.trim();
|
|
|
|
|
|
const date =
|
|
interaction.fields
|
|
.getTextInputValue(
|
|
"raid-date"
|
|
)
|
|
.trim();
|
|
|
|
|
|
const timeRange =
|
|
interaction.fields
|
|
.getTextInputValue(
|
|
"raid-time"
|
|
)
|
|
.trim();
|
|
|
|
|
|
/*
|
|
* DATE
|
|
*/
|
|
|
|
if (!isValidDateString(date)) {
|
|
return interaction.editReply({
|
|
content:
|
|
"Invalid date. Use `DD-MM-YYYY`, for example `22-08-2026`."
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* TIME
|
|
*/
|
|
|
|
const parsedTimeRange =
|
|
parseTimeRange(
|
|
timeRange
|
|
);
|
|
|
|
|
|
if (!parsedTimeRange) {
|
|
return interaction.editReply({
|
|
content:
|
|
"Invalid time. Use `HH:mm - HH:mm` using the 24-hour clock, for example `20:00 - 23:00`."
|
|
});
|
|
}
|
|
|
|
|
|
const {
|
|
startTime,
|
|
endTime
|
|
} =
|
|
parsedTimeRange;
|
|
|
|
|
|
/*
|
|
* DATES
|
|
*/
|
|
|
|
const startDate =
|
|
zonedDateTimeToDate(
|
|
date,
|
|
startTime,
|
|
RAID_TIMEZONE
|
|
);
|
|
|
|
|
|
let endDate =
|
|
zonedDateTimeToDate(
|
|
date,
|
|
endTime,
|
|
RAID_TIMEZONE
|
|
);
|
|
|
|
|
|
if (
|
|
!startDate ||
|
|
!endDate
|
|
) {
|
|
return interaction.editReply({
|
|
content:
|
|
"I couldn't understand that date and time."
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* CROSSING MIDNIGHT
|
|
*
|
|
* 23:00 - 02:00
|
|
*/
|
|
|
|
if (
|
|
endDate <= startDate
|
|
) {
|
|
endDate =
|
|
new Date(
|
|
endDate.getTime() +
|
|
24 * 60 * 60 * 1000
|
|
);
|
|
}
|
|
|
|
|
|
if (
|
|
startDate.getTime() <=
|
|
Date.now()
|
|
) {
|
|
return interaction.editReply({
|
|
content:
|
|
"The raid must start in the future."
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* PLANNER CHANNEL
|
|
*/
|
|
|
|
const channel =
|
|
await interaction.guild
|
|
.channels
|
|
.fetch(
|
|
RAID_PLANNER_CHANNEL_ID
|
|
);
|
|
|
|
|
|
if (
|
|
!channel ||
|
|
!channel.isTextBased() ||
|
|
typeof channel.send !==
|
|
"function"
|
|
) {
|
|
return interaction.editReply({
|
|
content:
|
|
"The configured raid planner channel could not be found."
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* PERMISSIONS
|
|
*/
|
|
|
|
const me =
|
|
interaction.guild.members.me;
|
|
|
|
|
|
if (!me) {
|
|
return interaction.editReply({
|
|
content:
|
|
"I couldn't determine my server permissions."
|
|
});
|
|
}
|
|
|
|
|
|
if (
|
|
!me.permissions.has(
|
|
PermissionFlagsBits
|
|
.CreateEvents
|
|
)
|
|
) {
|
|
return interaction.editReply({
|
|
content:
|
|
"I need the **Create Events** permission."
|
|
});
|
|
}
|
|
|
|
|
|
const channelPermissions =
|
|
channel.permissionsFor(me);
|
|
|
|
|
|
if (
|
|
!channelPermissions ||
|
|
!channelPermissions.has([
|
|
PermissionFlagsBits
|
|
.ViewChannel,
|
|
|
|
PermissionFlagsBits
|
|
.SendMessages,
|
|
|
|
PermissionFlagsBits
|
|
.EmbedLinks,
|
|
|
|
PermissionFlagsBits
|
|
.AttachFiles,
|
|
|
|
PermissionFlagsBits
|
|
.AddReactions
|
|
])
|
|
) {
|
|
return interaction.editReply({
|
|
content:
|
|
"I am missing one or more permissions in the raid planner channel."
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* UNIQUE RAID IDENTIFIER
|
|
*/
|
|
|
|
const raidId =
|
|
randomUUID();
|
|
|
|
|
|
const createdAt =
|
|
getUnixTime();
|
|
|
|
|
|
const startAt =
|
|
Math.floor(
|
|
startDate.getTime() /
|
|
1000
|
|
);
|
|
|
|
|
|
const endAt =
|
|
Math.floor(
|
|
endDate.getTime() /
|
|
1000
|
|
);
|
|
|
|
|
|
/*
|
|
* CREATE SCHEDULED EVENT
|
|
*/
|
|
|
|
let scheduledEvent;
|
|
|
|
|
|
try {
|
|
scheduledEvent =
|
|
await createScheduledEvent(
|
|
interaction,
|
|
{
|
|
name,
|
|
description,
|
|
duty,
|
|
startDate,
|
|
endDate
|
|
}
|
|
);
|
|
|
|
} catch (error) {
|
|
console.error(
|
|
"Scheduled raid event creation failed:",
|
|
error
|
|
);
|
|
|
|
|
|
return interaction.editReply({
|
|
content:
|
|
"I couldn't create the Discord scheduled event."
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* CREATE PLANNER POST
|
|
*/
|
|
|
|
let message;
|
|
|
|
|
|
try {
|
|
message =
|
|
await createRaidMessage(
|
|
interaction,
|
|
channel,
|
|
{
|
|
raidId,
|
|
name,
|
|
description,
|
|
duty,
|
|
startDate,
|
|
endDate
|
|
},
|
|
scheduledEvent
|
|
);
|
|
|
|
} catch (error) {
|
|
console.error(
|
|
"Raid planner message failed:",
|
|
error
|
|
);
|
|
|
|
|
|
try {
|
|
await scheduledEvent.delete(
|
|
"Raid planner post failed"
|
|
);
|
|
} catch (cleanupError) {
|
|
console.error(
|
|
"Failed to remove scheduled event:",
|
|
cleanupError
|
|
);
|
|
}
|
|
|
|
|
|
return interaction.editReply({
|
|
content:
|
|
"I couldn't publish the raid planner."
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* STORE EVENT IN MONGO
|
|
*/
|
|
|
|
try {
|
|
await db
|
|
.collection(
|
|
EVENT_COLLECTION
|
|
)
|
|
.insertOne({
|
|
raidId,
|
|
|
|
eventType:
|
|
"Raid",
|
|
|
|
guildId:
|
|
interaction.guildId,
|
|
|
|
channelId:
|
|
channel.id,
|
|
|
|
messageId:
|
|
message.id,
|
|
|
|
scheduledEventId:
|
|
scheduledEvent.id,
|
|
|
|
dutyId:
|
|
duty._id,
|
|
|
|
creatorId:
|
|
interaction.user.id,
|
|
|
|
createdAt,
|
|
|
|
startAt,
|
|
|
|
endAt,
|
|
|
|
signups: {
|
|
Tank: [],
|
|
Healer: [],
|
|
DPS: [],
|
|
Flex: []
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error(
|
|
"Failed to persist raid event:",
|
|
error
|
|
);
|
|
|
|
|
|
try {
|
|
await message.delete();
|
|
} catch {}
|
|
|
|
|
|
try {
|
|
await scheduledEvent.delete(
|
|
"Raid database insert failed"
|
|
);
|
|
} catch {}
|
|
|
|
|
|
return interaction.editReply({
|
|
content:
|
|
"I couldn't save the raid planner."
|
|
});
|
|
}
|
|
|
|
|
|
return interaction.editReply({
|
|
content:
|
|
`Raid created successfully!\n${message.url}`
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* CREATE DISCORD SCHEDULED EVENT
|
|
*/
|
|
|
|
async function createScheduledEvent(
|
|
interaction,
|
|
data
|
|
) {
|
|
const eventDescription = [
|
|
data.description,
|
|
"",
|
|
`Duty: ${data.duty.Name}`,
|
|
`Difficulty: ${data.duty.Difficulty}`,
|
|
`Organized by: ${interaction.user.displayName}`
|
|
]
|
|
.join("\n")
|
|
.slice(0, 1000);
|
|
|
|
|
|
return interaction.guild
|
|
.scheduledEvents
|
|
.create({
|
|
|
|
name:
|
|
data.name.slice(
|
|
0,
|
|
100
|
|
),
|
|
|
|
description:
|
|
eventDescription,
|
|
|
|
scheduledStartTime:
|
|
data.startDate,
|
|
|
|
scheduledEndTime:
|
|
data.endDate,
|
|
|
|
privacyLevel:
|
|
GuildScheduledEventPrivacyLevel
|
|
.GuildOnly,
|
|
|
|
entityType:
|
|
GuildScheduledEventEntityType
|
|
.External,
|
|
|
|
entityMetadata: {
|
|
location:
|
|
`Final Fantasy XIV — ${data.duty.Name}`
|
|
.slice(
|
|
0,
|
|
100
|
|
)
|
|
},
|
|
|
|
reason:
|
|
`Raid planned by ${interaction.user.tag}`
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* CREATE PLANNER EMBED
|
|
*/
|
|
|
|
async function createRaidMessage(
|
|
interaction,
|
|
channel,
|
|
data,
|
|
scheduledEvent
|
|
) {
|
|
const startUnix =
|
|
Math.floor(
|
|
data.startDate.getTime() /
|
|
1000
|
|
);
|
|
|
|
|
|
const endUnix =
|
|
Math.floor(
|
|
data.endDate.getTime() /
|
|
1000
|
|
);
|
|
|
|
|
|
const eventUrl =
|
|
`https://discord.com/events/${interaction.guildId}/${scheduledEvent.id}`;
|
|
|
|
|
|
const icon =
|
|
getDutyIconFile(
|
|
data.duty
|
|
);
|
|
|
|
|
|
const embed =
|
|
new EmbedBuilder()
|
|
|
|
.setTitle(
|
|
"Shaiwase Raid Planner"
|
|
)
|
|
|
|
.setDescription(
|
|
[
|
|
`## ${data.name}`,
|
|
data.description
|
|
].join(
|
|
"\n\n"
|
|
)
|
|
)
|
|
|
|
.addFields(
|
|
|
|
{
|
|
name:
|
|
"Duty",
|
|
|
|
value:
|
|
`**${data.duty.Name}**\n` +
|
|
`${formatDutyType(data.duty.Type)} • ${data.duty.Difficulty}`,
|
|
|
|
inline:
|
|
false
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Date",
|
|
|
|
value:
|
|
`<t:${startUnix}:D>`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Time",
|
|
|
|
value:
|
|
`<t:${startUnix}:t> - <t:${endUnix}:t>`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Starts",
|
|
|
|
value:
|
|
`<t:${startUnix}:R>`,
|
|
|
|
inline:
|
|
true
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Discord Event",
|
|
|
|
value:
|
|
`[View Event](${eventUrl})`,
|
|
|
|
inline:
|
|
false
|
|
},
|
|
|
|
{
|
|
name:
|
|
"Party",
|
|
|
|
value:
|
|
buildPartyField(
|
|
data.duty,
|
|
{
|
|
Tank: 0,
|
|
Healer: 0,
|
|
DPS: 0,
|
|
Flex: 0
|
|
},
|
|
0
|
|
),
|
|
|
|
inline:
|
|
false
|
|
}
|
|
)
|
|
|
|
.setFooter({
|
|
text:
|
|
`raidPlanner:${data.raidId}`
|
|
});
|
|
|
|
|
|
const files = [];
|
|
|
|
|
|
if (icon) {
|
|
const attachmentName =
|
|
"duty-icon.png";
|
|
|
|
|
|
files.push(
|
|
new AttachmentBuilder(
|
|
icon.path,
|
|
{
|
|
name:
|
|
attachmentName
|
|
}
|
|
)
|
|
);
|
|
|
|
|
|
embed.setThumbnail(
|
|
`attachment://${attachmentName}`
|
|
);
|
|
}
|
|
|
|
|
|
const message =
|
|
await channel.send({
|
|
embeds: [
|
|
embed
|
|
]
|
|
});
|
|
|
|
|
|
/*
|
|
* Initial signup reactions.
|
|
*/
|
|
|
|
await message.react(
|
|
RAID_EMOJIS.Tank.id
|
|
);
|
|
|
|
|
|
await message.react(
|
|
RAID_EMOJIS.Healer.id
|
|
);
|
|
|
|
|
|
await message.react(
|
|
RAID_EMOJIS.DPS.id
|
|
);
|
|
|
|
|
|
await message.react(
|
|
RAID_EMOJIS.Flex.id
|
|
);
|
|
|
|
|
|
return message;
|
|
}
|
|
|
|
|
|
/*
|
|
* PARTY DISPLAY
|
|
*/
|
|
|
|
function buildPartyField(
|
|
duty,
|
|
counts,
|
|
uniqueParticipants
|
|
) {
|
|
return (
|
|
`${RAID_EMOJIS.Tank.display} Tank **(${counts.Tank}/${duty.Party.Tanks})**\n` +
|
|
`${RAID_EMOJIS.Healer.display} Healer **(${counts.Healer}/${duty.Party.Healers})**\n` +
|
|
`${RAID_EMOJIS.DPS.display} DPS **(${counts.DPS}/${duty.Party.DPS})**\n` +
|
|
`${RAID_EMOJIS.Flex.display} Flex **(${counts.Flex})**\n\n` +
|
|
`**Participants: ${uniqueParticipants}/${duty.Party.Size}**`
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* DUTY ICON
|
|
*/
|
|
|
|
function getDutyIconFile(duty) {
|
|
const fileName =
|
|
DUTY_DIFFICULTY_ICONS[
|
|
duty.Difficulty
|
|
]
|
|
??
|
|
DUTY_ICONS[
|
|
duty.Type
|
|
];
|
|
|
|
|
|
if (!fileName)
|
|
return null;
|
|
|
|
|
|
return {
|
|
fileName,
|
|
|
|
path:
|
|
path.join(
|
|
dutyIconDirectory,
|
|
fileName
|
|
)
|
|
};
|
|
}
|
|
|
|
|
|
/*
|
|
* DUTY TYPE
|
|
*/
|
|
|
|
function formatDutyType(type) {
|
|
switch (type) {
|
|
case "AllianceRaid":
|
|
return "Alliance Raid";
|
|
|
|
case "VariantDungeon":
|
|
return "Variant Dungeon";
|
|
|
|
default:
|
|
return type;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* DATE VALIDATION
|
|
*/
|
|
|
|
function isValidDateString(value) {
|
|
const match =
|
|
/^(\d{2})-(\d{2})-(\d{4})$/
|
|
.exec(value);
|
|
|
|
|
|
if (!match)
|
|
return false;
|
|
|
|
|
|
const day =
|
|
Number(match[1]);
|
|
|
|
const month =
|
|
Number(match[2]);
|
|
|
|
const year =
|
|
Number(match[3]);
|
|
|
|
|
|
const date =
|
|
new Date(
|
|
Date.UTC(
|
|
year,
|
|
month - 1,
|
|
day
|
|
)
|
|
);
|
|
|
|
|
|
return (
|
|
date.getUTCFullYear() ===
|
|
year
|
|
&&
|
|
date.getUTCMonth() ===
|
|
month - 1
|
|
&&
|
|
date.getUTCDate() ===
|
|
day
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* TIME RANGE
|
|
*/
|
|
|
|
function parseTimeRange(value) {
|
|
const match =
|
|
/^(\d{2}):(\d{2})\s*-\s*(\d{2}):(\d{2})$/
|
|
.exec(value);
|
|
|
|
|
|
if (!match)
|
|
return null;
|
|
|
|
|
|
const startHour =
|
|
Number(match[1]);
|
|
|
|
const startMinute =
|
|
Number(match[2]);
|
|
|
|
const endHour =
|
|
Number(match[3]);
|
|
|
|
const endMinute =
|
|
Number(match[4]);
|
|
|
|
|
|
if (
|
|
startHour > 23 ||
|
|
startMinute > 59 ||
|
|
endHour > 23 ||
|
|
endMinute > 59
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
|
|
return {
|
|
startTime:
|
|
`${String(startHour).padStart(2, "0")}:${String(startMinute).padStart(2, "0")}`,
|
|
|
|
endTime:
|
|
`${String(endHour).padStart(2, "0")}:${String(endMinute).padStart(2, "0")}`
|
|
};
|
|
}
|
|
|
|
|
|
/*
|
|
* TIMEZONE CONVERSION
|
|
*/
|
|
|
|
function zonedDateTimeToDate(
|
|
dateString,
|
|
timeString,
|
|
timeZone
|
|
) {
|
|
const [
|
|
day,
|
|
month,
|
|
year
|
|
] =
|
|
dateString
|
|
.split("-")
|
|
.map(Number);
|
|
|
|
|
|
const [
|
|
hour,
|
|
minute
|
|
] =
|
|
timeString
|
|
.split(":")
|
|
.map(Number);
|
|
|
|
|
|
let timestamp =
|
|
Date.UTC(
|
|
year,
|
|
month - 1,
|
|
day,
|
|
hour,
|
|
minute
|
|
);
|
|
|
|
|
|
for (
|
|
let i = 0;
|
|
i < 2;
|
|
i++
|
|
) {
|
|
const offset =
|
|
getTimeZoneOffset(
|
|
new Date(timestamp),
|
|
timeZone
|
|
);
|
|
|
|
|
|
timestamp =
|
|
Date.UTC(
|
|
year,
|
|
month - 1,
|
|
day,
|
|
hour,
|
|
minute
|
|
)
|
|
-
|
|
offset;
|
|
}
|
|
|
|
|
|
const result =
|
|
new Date(timestamp);
|
|
|
|
|
|
return Number.isNaN(
|
|
result.getTime()
|
|
)
|
|
? null
|
|
: result;
|
|
}
|
|
|
|
|
|
function getTimeZoneOffset(
|
|
date,
|
|
timeZone
|
|
) {
|
|
const formatter =
|
|
new Intl.DateTimeFormat(
|
|
"en-US",
|
|
{
|
|
timeZone,
|
|
|
|
year:
|
|
"numeric",
|
|
|
|
month:
|
|
"2-digit",
|
|
|
|
day:
|
|
"2-digit",
|
|
|
|
hour:
|
|
"2-digit",
|
|
|
|
minute:
|
|
"2-digit",
|
|
|
|
second:
|
|
"2-digit",
|
|
|
|
hourCycle:
|
|
"h23"
|
|
}
|
|
);
|
|
|
|
|
|
const parts =
|
|
formatter
|
|
.formatToParts(date)
|
|
.reduce(
|
|
(
|
|
result,
|
|
part
|
|
) => {
|
|
|
|
if (
|
|
part.type !==
|
|
"literal"
|
|
) {
|
|
result[
|
|
part.type
|
|
] =
|
|
Number(
|
|
part.value
|
|
);
|
|
}
|
|
|
|
|
|
return result;
|
|
},
|
|
{}
|
|
);
|
|
|
|
|
|
const asUTC =
|
|
Date.UTC(
|
|
parts.year,
|
|
parts.month - 1,
|
|
parts.day,
|
|
parts.hour,
|
|
parts.minute,
|
|
parts.second
|
|
);
|
|
|
|
|
|
return (
|
|
asUTC -
|
|
date.getTime()
|
|
);
|
|
}
|
|
|
|
|
|
function getUnixTime() {
|
|
return Math.floor(
|
|
Date.now() / 1000
|
|
);
|
|
} |