Compare commits
2 Commits
dafa81e2a6
...
832863c1e4
| Author | SHA1 | Date | |
|---|---|---|---|
| 832863c1e4 | |||
| 7c49e05aec |
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
@@ -1,40 +0,0 @@
|
|||||||
require("dotenv").config();
|
|
||||||
|
|
||||||
const { REST, Routes } = require("discord.js");
|
|
||||||
|
|
||||||
const rest = new REST({ version: "10" })
|
|
||||||
.setToken(process.env.D_TOKEN);
|
|
||||||
|
|
||||||
async function clearCommands() {
|
|
||||||
try {
|
|
||||||
if (!process.env.D_TOKEN)
|
|
||||||
throw new Error("Missing D_TOKEN environment variable.");
|
|
||||||
|
|
||||||
if (!process.env.D_ID)
|
|
||||||
throw new Error("Missing D_ID environment variable.");
|
|
||||||
|
|
||||||
if (!process.env.D_GID)
|
|
||||||
throw new Error("Missing G_GID environment variable.");
|
|
||||||
|
|
||||||
console.log("Clearing guild application commands...");
|
|
||||||
|
|
||||||
await rest.put(
|
|
||||||
Routes.applicationGuildCommands(
|
|
||||||
process.env.D_ID,
|
|
||||||
process.env.D_GID
|
|
||||||
),
|
|
||||||
{
|
|
||||||
body: []
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Successfully cleared all guild application commands.");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to clear application commands:");
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
process.exitCode = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearCommands();
|
|
||||||
@@ -3,7 +3,8 @@ const {
|
|||||||
ModalBuilder,
|
ModalBuilder,
|
||||||
TextInputBuilder,
|
TextInputBuilder,
|
||||||
TextInputStyle,
|
TextInputStyle,
|
||||||
ActionRowBuilder
|
ActionRowBuilder,
|
||||||
|
MessageFlags
|
||||||
} = require("discord.js");
|
} = require("discord.js");
|
||||||
|
|
||||||
const { ObjectId } = require("mongodb");
|
const { ObjectId } = require("mongodb");
|
||||||
@@ -20,6 +21,7 @@ const RAID_TYPES = [
|
|||||||
"AllianceRaid"
|
"AllianceRaid"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
const RAID_DIFFICULTIES = [
|
const RAID_DIFFICULTIES = [
|
||||||
"Extreme",
|
"Extreme",
|
||||||
"Unreal",
|
"Unreal",
|
||||||
@@ -42,7 +44,9 @@ module.exports = {
|
|||||||
.addStringOption(option =>
|
.addStringOption(option =>
|
||||||
option
|
option
|
||||||
.setName("duty")
|
.setName("duty")
|
||||||
.setDescription("Select the duty you want to run.")
|
.setDescription(
|
||||||
|
"Select the duty you want to run."
|
||||||
|
)
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
.setAutocomplete(true)
|
.setAutocomplete(true)
|
||||||
)
|
)
|
||||||
@@ -50,7 +54,9 @@ module.exports = {
|
|||||||
|
|
||||||
|
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
switch (interaction.options.getSubcommand()) {
|
switch (
|
||||||
|
interaction.options.getSubcommand()
|
||||||
|
) {
|
||||||
case "plan":
|
case "plan":
|
||||||
return planRaid(interaction);
|
return planRaid(interaction);
|
||||||
}
|
}
|
||||||
@@ -63,6 +69,10 @@ module.exports = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AUTOCOMPLETE
|
||||||
|
*/
|
||||||
|
|
||||||
async function autocompleteDuty(interaction) {
|
async function autocompleteDuty(interaction) {
|
||||||
const focused =
|
const focused =
|
||||||
interaction.options.getFocused();
|
interaction.options.getFocused();
|
||||||
@@ -109,7 +119,21 @@ async function autocompleteDuty(interaction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* /raid plan
|
||||||
|
*/
|
||||||
|
|
||||||
async function planRaid(interaction) {
|
async function planRaid(interaction) {
|
||||||
|
if (!interaction.guild) {
|
||||||
|
return interaction.reply({
|
||||||
|
content:
|
||||||
|
"Raid planning can only be used inside a server.",
|
||||||
|
flags:
|
||||||
|
MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const dutyId =
|
const dutyId =
|
||||||
interaction.options.getString(
|
interaction.options.getString(
|
||||||
"duty",
|
"duty",
|
||||||
@@ -121,7 +145,8 @@ async function planRaid(interaction) {
|
|||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content:
|
content:
|
||||||
"Please select a duty from the autocomplete results.",
|
"Please select a duty from the autocomplete results.",
|
||||||
ephemeral: true
|
flags:
|
||||||
|
MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,42 +156,45 @@ async function planRaid(interaction) {
|
|||||||
.collection(DUTY_COLLECTION);
|
.collection(DUTY_COLLECTION);
|
||||||
|
|
||||||
|
|
||||||
const duty = await collection.findOne({
|
const duty =
|
||||||
_id: new ObjectId(dutyId),
|
await collection.findOne({
|
||||||
|
_id:
|
||||||
|
new ObjectId(dutyId),
|
||||||
|
|
||||||
Type: {
|
Type: {
|
||||||
$in: RAID_TYPES
|
$in: RAID_TYPES
|
||||||
},
|
},
|
||||||
|
|
||||||
Difficulty: {
|
Difficulty: {
|
||||||
$in: RAID_DIFFICULTIES
|
$in: RAID_DIFFICULTIES
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!duty) {
|
if (!duty) {
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content:
|
content:
|
||||||
"That duty could not be found or is not eligible for raid planning.",
|
"That duty could not be found or is not eligible for raid planning.",
|
||||||
ephemeral: true
|
flags:
|
||||||
|
MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The duty ObjectId is embedded in the modal customId.
|
|
||||||
*
|
|
||||||
* The modal handler can then retrieve the selected duty
|
|
||||||
* without trusting editable modal input.
|
|
||||||
*/
|
|
||||||
const modal =
|
const modal =
|
||||||
buildRaidModal(duty);
|
buildRaidModal(duty);
|
||||||
|
|
||||||
|
|
||||||
return interaction.showModal(modal);
|
return interaction.showModal(
|
||||||
|
modal
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* MODAL
|
||||||
|
*/
|
||||||
|
|
||||||
function buildRaidModal(duty) {
|
function buildRaidModal(duty) {
|
||||||
const modal =
|
const modal =
|
||||||
new ModalBuilder()
|
new ModalBuilder()
|
||||||
@@ -209,29 +237,16 @@ function buildRaidModal(duty) {
|
|||||||
.setRequired(true);
|
.setRequired(true);
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Display only.
|
|
||||||
*
|
|
||||||
* We don't use this field to identify the duty.
|
|
||||||
* The ObjectId in the modal customId is authoritative.
|
|
||||||
*/
|
|
||||||
const dutyInput =
|
|
||||||
new TextInputBuilder()
|
|
||||||
.setCustomId("raid-duty")
|
|
||||||
.setLabel("Duty")
|
|
||||||
.setValue(duty.Name)
|
|
||||||
.setStyle(
|
|
||||||
TextInputStyle.Short
|
|
||||||
)
|
|
||||||
.setRequired(true);
|
|
||||||
|
|
||||||
|
|
||||||
const dateInput =
|
const dateInput =
|
||||||
new TextInputBuilder()
|
new TextInputBuilder()
|
||||||
.setCustomId("raid-date")
|
.setCustomId("raid-date")
|
||||||
.setLabel("Date")
|
.setLabel("Date")
|
||||||
.setPlaceholder("DD-MM-YYYY")
|
.setPlaceholder(
|
||||||
.setStyle(TextInputStyle.Short)
|
"DD-MM-YYYY"
|
||||||
|
)
|
||||||
|
.setStyle(
|
||||||
|
TextInputStyle.Short
|
||||||
|
)
|
||||||
.setMinLength(10)
|
.setMinLength(10)
|
||||||
.setMaxLength(10)
|
.setMaxLength(10)
|
||||||
.setRequired(true);
|
.setRequired(true);
|
||||||
@@ -240,29 +255,39 @@ function buildRaidModal(duty) {
|
|||||||
const timeInput =
|
const timeInput =
|
||||||
new TextInputBuilder()
|
new TextInputBuilder()
|
||||||
.setCustomId("raid-time")
|
.setCustomId("raid-time")
|
||||||
.setLabel("Start - End (24h clock)")
|
.setLabel(
|
||||||
.setPlaceholder("20:00 - 23:00")
|
"Start - End (24h clock)"
|
||||||
.setStyle(TextInputStyle.Short)
|
)
|
||||||
.setMinLength(13)
|
.setPlaceholder(
|
||||||
.setMaxLength(13)
|
"20:00 - 23:00"
|
||||||
|
)
|
||||||
|
.setStyle(
|
||||||
|
TextInputStyle.Short
|
||||||
|
)
|
||||||
.setRequired(true);
|
.setRequired(true);
|
||||||
|
|
||||||
|
|
||||||
modal.addComponents(
|
modal.addComponents(
|
||||||
new ActionRowBuilder()
|
|
||||||
.addComponents(nameInput),
|
|
||||||
|
|
||||||
new ActionRowBuilder()
|
new ActionRowBuilder()
|
||||||
.addComponents(descriptionInput),
|
.addComponents(
|
||||||
|
nameInput
|
||||||
|
),
|
||||||
|
|
||||||
new ActionRowBuilder()
|
new ActionRowBuilder()
|
||||||
.addComponents(dutyInput),
|
.addComponents(
|
||||||
|
descriptionInput
|
||||||
|
),
|
||||||
|
|
||||||
new ActionRowBuilder()
|
new ActionRowBuilder()
|
||||||
.addComponents(dateInput),
|
.addComponents(
|
||||||
|
dateInput
|
||||||
|
),
|
||||||
|
|
||||||
new ActionRowBuilder()
|
new ActionRowBuilder()
|
||||||
.addComponents(timeInput)
|
.addComponents(
|
||||||
|
timeInput
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -270,6 +295,10 @@ function buildRaidModal(duty) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* HELPERS
|
||||||
|
*/
|
||||||
|
|
||||||
function buildAutocompleteName(duty) {
|
function buildAutocompleteName(duty) {
|
||||||
const label =
|
const label =
|
||||||
`${duty.Name} — ${formatDutyType(duty.Type)} / ${duty.Difficulty}`;
|
`${duty.Name} — ${formatDutyType(duty.Type)} / ${duty.Difficulty}`;
|
||||||
|
|||||||
@@ -221,13 +221,17 @@ async function selectMenuHandler(interaction) {
|
|||||||
|
|
||||||
|
|
||||||
async function modalHandler(interaction) {
|
async function modalHandler(interaction) {
|
||||||
const baseCustomId =
|
const customId =
|
||||||
interaction.customId.split(":")[0];
|
interaction.customId
|
||||||
|
.split(":")[0];
|
||||||
|
|
||||||
|
|
||||||
const modal =
|
const modal =
|
||||||
interaction.client.modals.get(
|
interaction.client
|
||||||
baseCustomId
|
.modals
|
||||||
);
|
.get(
|
||||||
|
customId
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
if (!modal) {
|
if (!modal) {
|
||||||
@@ -245,6 +249,7 @@ async function modalHandler(interaction) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
interaction.replied ||
|
interaction.replied ||
|
||||||
interaction.deferred
|
interaction.deferred
|
||||||
@@ -252,6 +257,7 @@ async function modalHandler(interaction) {
|
|||||||
await interaction.followUp({
|
await interaction.followUp({
|
||||||
content:
|
content:
|
||||||
"There was an error while executing this modal!",
|
"There was an error while executing this modal!",
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
MessageFlags.Ephemeral
|
MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
@@ -260,6 +266,7 @@ async function modalHandler(interaction) {
|
|||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content:
|
content:
|
||||||
"There was an error while executing this modal!",
|
"There was an error while executing this modal!",
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
MessageFlags.Ephemeral
|
MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
const {
|
||||||
|
Events
|
||||||
|
} = require("discord.js");
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleRaidReaction
|
||||||
|
} = require(
|
||||||
|
"../utils/raidPlanner"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name:
|
||||||
|
Events.MessageReactionAdd,
|
||||||
|
|
||||||
|
once:
|
||||||
|
false,
|
||||||
|
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
reaction,
|
||||||
|
user
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await handleRaidReaction(
|
||||||
|
reaction,
|
||||||
|
user,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Raid planner reaction add error:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
const {
|
||||||
|
Events
|
||||||
|
} = require("discord.js");
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleRaidReaction
|
||||||
|
} = require(
|
||||||
|
"../utils/raidPlanner"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name:
|
||||||
|
Events.MessageReactionRemove,
|
||||||
|
|
||||||
|
once:
|
||||||
|
false,
|
||||||
|
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
reaction,
|
||||||
|
user
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await handleRaidReaction(
|
||||||
|
reaction,
|
||||||
|
user,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Raid planner reaction remove error:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -15,8 +15,15 @@ const client = new Client({
|
|||||||
GatewayIntentBits.GuildVoiceStates,
|
GatewayIntentBits.GuildVoiceStates,
|
||||||
GatewayIntentBits.DirectMessages,
|
GatewayIntentBits.DirectMessages,
|
||||||
GatewayIntentBits.MessageContent,
|
GatewayIntentBits.MessageContent,
|
||||||
|
|
||||||
],
|
],
|
||||||
partials: [Partials.Channel, Partials.Message, Partials.User, Partials.GuildMember, Partials.Reaction]
|
partials: [
|
||||||
|
Partials.Channel,
|
||||||
|
Partials.Message,
|
||||||
|
Partials.User,
|
||||||
|
Partials.GuildMember,
|
||||||
|
Partials.Reaction
|
||||||
|
]
|
||||||
});
|
});
|
||||||
exports.client = client
|
exports.client = client
|
||||||
|
|
||||||
|
|||||||
+288
-214
@@ -8,42 +8,60 @@ const {
|
|||||||
} = require("discord.js");
|
} = require("discord.js");
|
||||||
|
|
||||||
const { ObjectId } = require("mongodb");
|
const { ObjectId } = require("mongodb");
|
||||||
|
const { randomUUID } = require("node:crypto");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
|
|
||||||
const { mClient } = require("..");
|
const { mClient } = require("..");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* CONFIG
|
* CONFIG
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DATABASE = "neo-db";
|
const DATABASE = "neo-db";
|
||||||
const DUTY_COLLECTION = "items_duties";
|
|
||||||
|
const DUTY_COLLECTION =
|
||||||
|
"items_duties";
|
||||||
|
|
||||||
|
const EVENT_COLLECTION =
|
||||||
|
"items_events";
|
||||||
|
|
||||||
|
|
||||||
const RAID_PLANNER_CHANNEL_ID =
|
const RAID_PLANNER_CHANNEL_ID =
|
||||||
process.env.RAID_PLANNER_CHANNEL_ID;
|
process.env.RAID_PLANNER_CHANNEL_ID;
|
||||||
|
|
||||||
|
|
||||||
const RAID_TIMEZONE =
|
const RAID_TIMEZONE =
|
||||||
process.env.RAID_TIMEZONE || "Europe/Berlin";
|
process.env.RAID_TIMEZONE ||
|
||||||
|
"Europe/Berlin";
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* LOCAL DUTY ASSETS
|
* LOCAL ASSETS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const dutyIconDirectory = path.resolve(
|
const dutyIconDirectory =
|
||||||
__dirname,
|
path.resolve(
|
||||||
"../assets/duties"
|
__dirname,
|
||||||
);
|
"../assets/duties"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
const DUTY_ICONS = {
|
const DUTY_ICONS = {
|
||||||
Guildhest: "Guildhest.png",
|
Guildhest:
|
||||||
Dungeon: "Dungeon.png",
|
"Guildhest.png",
|
||||||
Trial: "Trial.png",
|
|
||||||
Raid: "Raid.png",
|
Dungeon:
|
||||||
AllianceRaid: "Raid.png",
|
"Dungeon.png",
|
||||||
|
|
||||||
|
Trial:
|
||||||
|
"Trial.png",
|
||||||
|
|
||||||
|
Raid:
|
||||||
|
"Raid.png",
|
||||||
|
|
||||||
|
AllianceRaid:
|
||||||
|
"Raid.png",
|
||||||
|
|
||||||
VariantDungeon:
|
VariantDungeon:
|
||||||
"Variant_and_criterion_dungeons_icon1.png",
|
"Variant_and_criterion_dungeons_icon1.png",
|
||||||
@@ -54,61 +72,72 @@ const DUTY_ICONS = {
|
|||||||
|
|
||||||
|
|
||||||
const DUTY_DIFFICULTY_ICONS = {
|
const DUTY_DIFFICULTY_ICONS = {
|
||||||
Ultimate: "Ultimate_Raid.png",
|
Ultimate:
|
||||||
Chaotic: "Chaotic_Alliance_Raid.png"
|
"Ultimate_Raid.png",
|
||||||
|
|
||||||
|
Chaotic:
|
||||||
|
"Chaotic_Alliance_Raid.png"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* RAID SIGNUP EMOJIS
|
* ROLE EMOJIS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const RAID_EMOJIS = {
|
const RAID_EMOJIS = {
|
||||||
Tank: {
|
Tank: {
|
||||||
id: "1537931883369726042",
|
id:
|
||||||
|
"1537931883369726042",
|
||||||
|
|
||||||
display:
|
display:
|
||||||
"<:TankRole:1537931883369726042>"
|
"<:TankRole:1537931883369726042>"
|
||||||
},
|
},
|
||||||
|
|
||||||
Healer: {
|
Healer: {
|
||||||
id: "1537931880756805704",
|
id:
|
||||||
|
"1537931880756805704",
|
||||||
|
|
||||||
display:
|
display:
|
||||||
"<:HealerRole:1537931880756805704>"
|
"<:HealerRole:1537931880756805704>"
|
||||||
},
|
},
|
||||||
|
|
||||||
DPS: {
|
DPS: {
|
||||||
id: "1537931878668050442",
|
id:
|
||||||
|
"1537931878668050442",
|
||||||
|
|
||||||
display:
|
display:
|
||||||
"<:DPSRole:1537931878668050442>"
|
"<:DPSRole:1537931878668050442>"
|
||||||
|
},
|
||||||
|
|
||||||
|
Flex: {
|
||||||
|
id:
|
||||||
|
"1537948665606574210",
|
||||||
|
|
||||||
|
display:
|
||||||
|
"<:AllRounderRole:1537948665606574210>"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* MODAL EXPORT
|
|
||||||
*
|
|
||||||
* Your modal handler should use the base customId:
|
|
||||||
* raidPlan:<dutyObjectId>
|
|
||||||
*
|
|
||||||
* and resolve "raidPlan".
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
customId: "raidPlan",
|
customId: "raidPlan",
|
||||||
|
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
return handleRaidPlan(interaction);
|
return handleRaidPlan(
|
||||||
|
interaction
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MAIN HANDLER
|
* MAIN MODAL HANDLER
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function handleRaidPlan(interaction) {
|
async function handleRaidPlan(interaction) {
|
||||||
await interaction.deferReply({
|
await interaction.deferReply({
|
||||||
flags: MessageFlags.Ephemeral
|
flags:
|
||||||
|
MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -129,14 +158,15 @@ async function handleRaidPlan(interaction) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* customId:
|
* raidPlan:<dutyId>
|
||||||
*
|
|
||||||
* raidPlan:<ObjectId>
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const [
|
const [
|
||||||
prefix,
|
prefix,
|
||||||
dutyId
|
dutyId
|
||||||
] = interaction.customId.split(":");
|
] =
|
||||||
|
interaction.customId
|
||||||
|
.split(":");
|
||||||
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -151,18 +181,24 @@ async function handleRaidPlan(interaction) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Fetch selected duty.
|
* LOAD DUTY
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const collection = mClient
|
const db =
|
||||||
.db(DATABASE)
|
mClient.db(DATABASE);
|
||||||
.collection(DUTY_COLLECTION);
|
|
||||||
|
|
||||||
|
|
||||||
const duty =
|
const duty =
|
||||||
await collection.findOne({
|
await db
|
||||||
_id: new ObjectId(dutyId)
|
.collection(
|
||||||
});
|
DUTY_COLLECTION
|
||||||
|
)
|
||||||
|
.findOne({
|
||||||
|
_id:
|
||||||
|
new ObjectId(
|
||||||
|
dutyId
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!duty) {
|
if (!duty) {
|
||||||
@@ -174,7 +210,7 @@ async function handleRaidPlan(interaction) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Read modal fields.
|
* MODAL VALUES
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const name =
|
const name =
|
||||||
@@ -193,14 +229,6 @@ async function handleRaidPlan(interaction) {
|
|||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
|
||||||
const enteredDuty =
|
|
||||||
interaction.fields
|
|
||||||
.getTextInputValue(
|
|
||||||
"raid-duty"
|
|
||||||
)
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
|
|
||||||
const date =
|
const date =
|
||||||
interaction.fields
|
interaction.fields
|
||||||
.getTextInputValue(
|
.getTextInputValue(
|
||||||
@@ -218,30 +246,10 @@ async function handleRaidPlan(interaction) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Duty field is display-only in practice.
|
* DATE
|
||||||
* Verify user didn't modify it.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (
|
if (!isValidDateString(date)) {
|
||||||
enteredDuty !==
|
|
||||||
duty.Name
|
|
||||||
) {
|
|
||||||
return interaction.editReply({
|
|
||||||
content:
|
|
||||||
"The Duty field was changed. Please create the raid again."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Validate date.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isValidDateString(
|
|
||||||
date
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
content:
|
content:
|
||||||
"Invalid date. Use `DD-MM-YYYY`, for example `22-08-2026`."
|
"Invalid date. Use `DD-MM-YYYY`, for example `22-08-2026`."
|
||||||
@@ -250,7 +258,7 @@ async function handleRaidPlan(interaction) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Validate time range.
|
* TIME
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const parsedTimeRange =
|
const parsedTimeRange =
|
||||||
@@ -262,7 +270,7 @@ async function handleRaidPlan(interaction) {
|
|||||||
if (!parsedTimeRange) {
|
if (!parsedTimeRange) {
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
content:
|
content:
|
||||||
"Invalid time. Use the 24-hour format `HH:mm - HH:mm`, for example `20:00 - 23:00`."
|
"Invalid time. Use `HH:mm - HH:mm` using the 24-hour clock, for example `20:00 - 23:00`."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,11 +278,12 @@ async function handleRaidPlan(interaction) {
|
|||||||
const {
|
const {
|
||||||
startTime,
|
startTime,
|
||||||
endTime
|
endTime
|
||||||
} = parsedTimeRange;
|
} =
|
||||||
|
parsedTimeRange;
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convert configured timezone to JS Date.
|
* DATES
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const startDate =
|
const startDate =
|
||||||
@@ -305,16 +314,13 @@ async function handleRaidPlan(interaction) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If the end time is before/equal to the start,
|
* CROSSING MIDNIGHT
|
||||||
* assume the raid crosses midnight.
|
|
||||||
*
|
*
|
||||||
* Example:
|
|
||||||
* 23:00 - 02:00
|
* 23:00 - 02:00
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (
|
if (
|
||||||
endDate <=
|
endDate <= startDate
|
||||||
startDate
|
|
||||||
) {
|
) {
|
||||||
endDate =
|
endDate =
|
||||||
new Date(
|
new Date(
|
||||||
@@ -324,23 +330,19 @@ async function handleRaidPlan(interaction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Start must be in the future.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
startDate.getTime() <=
|
startDate.getTime() <=
|
||||||
Date.now()
|
Date.now()
|
||||||
) {
|
) {
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
content:
|
content:
|
||||||
"The raid must be scheduled for a future date and time."
|
"The raid must start in the future."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Fetch planner channel.
|
* PLANNER CHANNEL
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const channel =
|
const channel =
|
||||||
@@ -359,13 +361,13 @@ async function handleRaidPlan(interaction) {
|
|||||||
) {
|
) {
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
content:
|
content:
|
||||||
"The configured raid planner channel could not be found or is not a text channel."
|
"The configured raid planner channel could not be found."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Bot member.
|
* PERMISSIONS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const me =
|
const me =
|
||||||
@@ -380,13 +382,10 @@ async function handleRaidPlan(interaction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Event permission.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!me.permissions.has(
|
!me.permissions.has(
|
||||||
PermissionFlagsBits.CreateEvents
|
PermissionFlagsBits
|
||||||
|
.CreateEvents
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
@@ -396,10 +395,6 @@ async function handleRaidPlan(interaction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Destination channel permissions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const channelPermissions =
|
const channelPermissions =
|
||||||
channel.permissionsFor(me);
|
channel.permissionsFor(me);
|
||||||
|
|
||||||
@@ -407,29 +402,65 @@ async function handleRaidPlan(interaction) {
|
|||||||
if (
|
if (
|
||||||
!channelPermissions ||
|
!channelPermissions ||
|
||||||
!channelPermissions.has([
|
!channelPermissions.has([
|
||||||
PermissionFlagsBits.ViewChannel,
|
PermissionFlagsBits
|
||||||
PermissionFlagsBits.SendMessages,
|
.ViewChannel,
|
||||||
PermissionFlagsBits.EmbedLinks,
|
|
||||||
PermissionFlagsBits.AddReactions
|
PermissionFlagsBits
|
||||||
|
.SendMessages,
|
||||||
|
|
||||||
|
PermissionFlagsBits
|
||||||
|
.EmbedLinks,
|
||||||
|
|
||||||
|
PermissionFlagsBits
|
||||||
|
.AttachFiles,
|
||||||
|
|
||||||
|
PermissionFlagsBits
|
||||||
|
.AddReactions
|
||||||
])
|
])
|
||||||
) {
|
) {
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
content:
|
content:
|
||||||
"I need **View Channel**, **Send Messages**, **Embed Links**, and **Add Reactions** in the raid planner channel."
|
"I am missing one or more permissions in the raid planner channel."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Create scheduled event.
|
* UNIQUE RAID IDENTIFIER
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let event;
|
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 {
|
try {
|
||||||
event =
|
scheduledEvent =
|
||||||
await createRaidEvent(
|
await createScheduledEvent(
|
||||||
interaction,
|
interaction,
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
@@ -442,20 +473,20 @@ async function handleRaidPlan(interaction) {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
"Raid event creation failed:",
|
"Scheduled raid event creation failed:",
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
content:
|
content:
|
||||||
"I couldn't create the scheduled event."
|
"I couldn't create the Discord scheduled event."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Create planner embed/message.
|
* CREATE PLANNER POST
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let message;
|
let message;
|
||||||
@@ -467,13 +498,14 @@ async function handleRaidPlan(interaction) {
|
|||||||
interaction,
|
interaction,
|
||||||
channel,
|
channel,
|
||||||
{
|
{
|
||||||
|
raidId,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
duty,
|
duty,
|
||||||
startDate,
|
startDate,
|
||||||
endDate
|
endDate
|
||||||
},
|
},
|
||||||
event
|
scheduledEvent
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -483,17 +515,13 @@ async function handleRaidPlan(interaction) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Remove event if planner post fails.
|
|
||||||
*/
|
|
||||||
try {
|
try {
|
||||||
await event.delete(
|
await scheduledEvent.delete(
|
||||||
"Raid planner message failed"
|
"Raid planner post failed"
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (cleanupError) {
|
} catch (cleanupError) {
|
||||||
console.error(
|
console.error(
|
||||||
"Raid event cleanup failed:",
|
"Failed to remove scheduled event:",
|
||||||
cleanupError
|
cleanupError
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -507,9 +535,78 @@ async function handleRaidPlan(interaction) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Confirmation to raid creator.
|
* 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({
|
return interaction.editReply({
|
||||||
content:
|
content:
|
||||||
`Raid created successfully!\n${message.url}`
|
`Raid created successfully!\n${message.url}`
|
||||||
@@ -521,7 +618,7 @@ async function handleRaidPlan(interaction) {
|
|||||||
* CREATE DISCORD SCHEDULED EVENT
|
* CREATE DISCORD SCHEDULED EVENT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function createRaidEvent(
|
async function createScheduledEvent(
|
||||||
interaction,
|
interaction,
|
||||||
data
|
data
|
||||||
) {
|
) {
|
||||||
@@ -533,10 +630,7 @@ async function createRaidEvent(
|
|||||||
`Organized by: ${interaction.user.displayName}`
|
`Organized by: ${interaction.user.displayName}`
|
||||||
]
|
]
|
||||||
.join("\n")
|
.join("\n")
|
||||||
.slice(
|
.slice(0, 1000);
|
||||||
0,
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return interaction.guild
|
return interaction.guild
|
||||||
@@ -582,33 +676,31 @@ async function createRaidEvent(
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* CREATE RAID PLANNER MESSAGE
|
* CREATE PLANNER EMBED
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function createRaidMessage(
|
async function createRaidMessage(
|
||||||
interaction,
|
interaction,
|
||||||
channel,
|
channel,
|
||||||
data,
|
data,
|
||||||
event
|
scheduledEvent
|
||||||
) {
|
) {
|
||||||
const startUnix =
|
const startUnix =
|
||||||
Math.floor(
|
Math.floor(
|
||||||
data.startDate
|
data.startDate.getTime() /
|
||||||
.getTime() /
|
|
||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const endUnix =
|
const endUnix =
|
||||||
Math.floor(
|
Math.floor(
|
||||||
data.endDate
|
data.endDate.getTime() /
|
||||||
.getTime() /
|
|
||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const eventUrl =
|
const eventUrl =
|
||||||
`https://discord.com/events/${interaction.guildId}/${event.id}`;
|
`https://discord.com/events/${interaction.guildId}/${scheduledEvent.id}`;
|
||||||
|
|
||||||
|
|
||||||
const icon =
|
const icon =
|
||||||
@@ -693,12 +785,19 @@ async function createRaidMessage(
|
|||||||
|
|
||||||
{
|
{
|
||||||
name:
|
name:
|
||||||
"Sign Up",
|
"Party",
|
||||||
|
|
||||||
value:
|
value:
|
||||||
`${RAID_EMOJIS.Tank.display} Tank\n` +
|
buildPartyField(
|
||||||
`${RAID_EMOJIS.Healer.display} Healer\n` +
|
data.duty,
|
||||||
`${RAID_EMOJIS.DPS.display} DPS`,
|
{
|
||||||
|
Tank: 0,
|
||||||
|
Healer: 0,
|
||||||
|
DPS: 0,
|
||||||
|
Flex: 0
|
||||||
|
},
|
||||||
|
0
|
||||||
|
),
|
||||||
|
|
||||||
inline:
|
inline:
|
||||||
false
|
false
|
||||||
@@ -707,15 +806,9 @@ async function createRaidMessage(
|
|||||||
|
|
||||||
.setFooter({
|
.setFooter({
|
||||||
text:
|
text:
|
||||||
`Created by ${interaction.user.displayName}`
|
`raidPlanner:${data.raidId}`
|
||||||
})
|
});
|
||||||
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Local duty icon attachment.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const files = [];
|
const files = [];
|
||||||
|
|
||||||
@@ -742,22 +835,16 @@ async function createRaidMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Send only into raid planner channel.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
await channel.send({
|
await channel.send({
|
||||||
embeds: [
|
embeds: [
|
||||||
embed
|
embed
|
||||||
],
|
]
|
||||||
|
|
||||||
files
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Signup reactions.
|
* Initial signup reactions.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
await message.react(
|
await message.react(
|
||||||
@@ -775,12 +862,36 @@ async function createRaidMessage(
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
await message.react(
|
||||||
|
RAID_EMOJIS.Flex.id
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* GET DUTY ICON
|
* 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) {
|
function getDutyIconFile(duty) {
|
||||||
@@ -811,7 +922,7 @@ function getDutyIconFile(duty) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* FORMAT DUTY TYPE
|
* DUTY TYPE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function formatDutyType(type) {
|
function formatDutyType(type) {
|
||||||
@@ -829,7 +940,7 @@ function formatDutyType(type) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* VALIDATE DD-MM-YYYY
|
* DATE VALIDATION
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function isValidDateString(value) {
|
function isValidDateString(value) {
|
||||||
@@ -843,21 +954,13 @@ function isValidDateString(value) {
|
|||||||
|
|
||||||
|
|
||||||
const day =
|
const day =
|
||||||
Number(
|
Number(match[1]);
|
||||||
match[1]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
const month =
|
const month =
|
||||||
Number(
|
Number(match[2]);
|
||||||
match[2]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
const year =
|
const year =
|
||||||
Number(
|
Number(match[3]);
|
||||||
match[3]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
const date =
|
const date =
|
||||||
@@ -884,11 +987,7 @@ function isValidDateString(value) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* PARSE:
|
* TIME RANGE
|
||||||
*
|
|
||||||
* 20:00 - 23:00
|
|
||||||
*
|
|
||||||
* 24-hour clock
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function parseTimeRange(value) {
|
function parseTimeRange(value) {
|
||||||
@@ -902,37 +1001,22 @@ function parseTimeRange(value) {
|
|||||||
|
|
||||||
|
|
||||||
const startHour =
|
const startHour =
|
||||||
Number(
|
Number(match[1]);
|
||||||
match[1]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
const startMinute =
|
const startMinute =
|
||||||
Number(
|
Number(match[2]);
|
||||||
match[2]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
const endHour =
|
const endHour =
|
||||||
Number(
|
Number(match[3]);
|
||||||
match[3]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
const endMinute =
|
const endMinute =
|
||||||
Number(
|
Number(match[4]);
|
||||||
match[4]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
startHour < 0 ||
|
|
||||||
startHour > 23 ||
|
startHour > 23 ||
|
||||||
startMinute < 0 ||
|
|
||||||
startMinute > 59 ||
|
startMinute > 59 ||
|
||||||
endHour < 0 ||
|
|
||||||
endHour > 23 ||
|
endHour > 23 ||
|
||||||
endMinute < 0 ||
|
|
||||||
endMinute > 59
|
endMinute > 59
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
@@ -950,8 +1034,7 @@ function parseTimeRange(value) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* CONVERT DD-MM-YYYY + HH:mm FROM
|
* TIMEZONE CONVERSION
|
||||||
* CONFIGURED TIMEZONE INTO JS DATE.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function zonedDateTimeToDate(
|
function zonedDateTimeToDate(
|
||||||
@@ -984,15 +1067,10 @@ function zonedDateTimeToDate(
|
|||||||
month - 1,
|
month - 1,
|
||||||
day,
|
day,
|
||||||
hour,
|
hour,
|
||||||
minute,
|
minute
|
||||||
0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Two passes helps account for DST.
|
|
||||||
*/
|
|
||||||
|
|
||||||
for (
|
for (
|
||||||
let i = 0;
|
let i = 0;
|
||||||
i < 2;
|
i < 2;
|
||||||
@@ -1000,9 +1078,7 @@ function zonedDateTimeToDate(
|
|||||||
) {
|
) {
|
||||||
const offset =
|
const offset =
|
||||||
getTimeZoneOffset(
|
getTimeZoneOffset(
|
||||||
new Date(
|
new Date(timestamp),
|
||||||
timestamp
|
|
||||||
),
|
|
||||||
timeZone
|
timeZone
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1013,8 +1089,7 @@ function zonedDateTimeToDate(
|
|||||||
month - 1,
|
month - 1,
|
||||||
day,
|
day,
|
||||||
hour,
|
hour,
|
||||||
minute,
|
minute
|
||||||
0
|
|
||||||
)
|
)
|
||||||
-
|
-
|
||||||
offset;
|
offset;
|
||||||
@@ -1022,9 +1097,7 @@ function zonedDateTimeToDate(
|
|||||||
|
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
new Date(
|
new Date(timestamp);
|
||||||
timestamp
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return Number.isNaN(
|
return Number.isNaN(
|
||||||
@@ -1035,10 +1108,6 @@ function zonedDateTimeToDate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* GET UTC OFFSET OF IANA TIMEZONE
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getTimeZoneOffset(
|
function getTimeZoneOffset(
|
||||||
date,
|
date,
|
||||||
timeZone
|
timeZone
|
||||||
@@ -1075,9 +1144,7 @@ function getTimeZoneOffset(
|
|||||||
|
|
||||||
const parts =
|
const parts =
|
||||||
formatter
|
formatter
|
||||||
.formatToParts(
|
.formatToParts(date)
|
||||||
date
|
|
||||||
)
|
|
||||||
.reduce(
|
.reduce(
|
||||||
(
|
(
|
||||||
result,
|
result,
|
||||||
@@ -1119,3 +1186,10 @@ function getTimeZoneOffset(
|
|||||||
date.getTime()
|
date.getTime()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getUnixTime() {
|
||||||
|
return Math.floor(
|
||||||
|
Date.now() / 1000
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,522 @@
|
|||||||
|
const {
|
||||||
|
EmbedBuilder
|
||||||
|
} = require("discord.js");
|
||||||
|
|
||||||
|
const { mClient } = require("../index");
|
||||||
|
|
||||||
|
|
||||||
|
const DATABASE =
|
||||||
|
"neo-db";
|
||||||
|
|
||||||
|
const DUTY_COLLECTION =
|
||||||
|
"items_duties";
|
||||||
|
|
||||||
|
const EVENT_COLLECTION =
|
||||||
|
"items_events";
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* RAID SIGNUP 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>"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* MAIN REACTION HANDLER
|
||||||
|
*
|
||||||
|
* added = true
|
||||||
|
* MessageReactionAdd
|
||||||
|
*
|
||||||
|
* added = false
|
||||||
|
* MessageReactionRemove
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function handleRaidReaction(
|
||||||
|
reaction,
|
||||||
|
user,
|
||||||
|
added
|
||||||
|
) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ignore bots.
|
||||||
|
*
|
||||||
|
* This prevents ShaiBot's own initial
|
||||||
|
* signup reactions from being counted.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (user.bot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Resolve partial reactions/messages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (reaction.partial) {
|
||||||
|
try {
|
||||||
|
await reaction.fetch();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Failed to fetch partial reaction:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (reaction.message.partial) {
|
||||||
|
try {
|
||||||
|
await reaction.message.fetch();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Failed to fetch partial raid planner message:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const message =
|
||||||
|
reaction.message;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Raid planner messages always contain
|
||||||
|
* an embed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (
|
||||||
|
!message.embeds ||
|
||||||
|
message.embeds.length === 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const embed =
|
||||||
|
message.embeds[0];
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Footer format:
|
||||||
|
*
|
||||||
|
* raidPlanner:<raidId>
|
||||||
|
*/
|
||||||
|
|
||||||
|
const footer =
|
||||||
|
embed.footer?.text;
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
!footer ||
|
||||||
|
!footer.startsWith(
|
||||||
|
"raidPlanner:"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const [
|
||||||
|
prefix,
|
||||||
|
raidId
|
||||||
|
] =
|
||||||
|
footer.split(":");
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
prefix !== "raidPlanner" ||
|
||||||
|
!raidId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Determine which signup role
|
||||||
|
* this reaction represents.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const role =
|
||||||
|
getRoleFromEmoji(
|
||||||
|
reaction.emoji
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ignore unrelated reactions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!role)
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
const db =
|
||||||
|
mClient.db(
|
||||||
|
DATABASE
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const events =
|
||||||
|
db.collection(
|
||||||
|
EVENT_COLLECTION
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Make sure event exists.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const raid =
|
||||||
|
await events.findOne({
|
||||||
|
raidId
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (!raid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* REACTION ADDED
|
||||||
|
*
|
||||||
|
* Each role is independent.
|
||||||
|
* Users may select multiple roles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (added) {
|
||||||
|
await events.updateOne(
|
||||||
|
{
|
||||||
|
raidId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$addToSet: {
|
||||||
|
[`signups.${role}`]:
|
||||||
|
user.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* REACTION REMOVED
|
||||||
|
*
|
||||||
|
* Remove only this role.
|
||||||
|
*/
|
||||||
|
|
||||||
|
else {
|
||||||
|
await events.updateOne(
|
||||||
|
{
|
||||||
|
raidId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$pull: {
|
||||||
|
[`signups.${role}`]:
|
||||||
|
user.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Rebuild planner from current Mongo state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
await updateRaidPlanner(
|
||||||
|
message,
|
||||||
|
raidId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* UPDATE RAID PLANNER EMBED
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function updateRaidPlanner(
|
||||||
|
message,
|
||||||
|
raidId
|
||||||
|
) {
|
||||||
|
const db =
|
||||||
|
mClient.db(
|
||||||
|
DATABASE
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fetch raid state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const raid =
|
||||||
|
await db
|
||||||
|
.collection(
|
||||||
|
EVENT_COLLECTION
|
||||||
|
)
|
||||||
|
.findOne({
|
||||||
|
raidId
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (!raid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fetch duty.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const duty =
|
||||||
|
await db
|
||||||
|
.collection(
|
||||||
|
DUTY_COLLECTION
|
||||||
|
)
|
||||||
|
.findOne({
|
||||||
|
_id:
|
||||||
|
raid.dutyId
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (!duty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Role counts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const counts = {
|
||||||
|
Tank:
|
||||||
|
raid.signups?.Tank
|
||||||
|
?.length
|
||||||
|
?? 0,
|
||||||
|
|
||||||
|
Healer:
|
||||||
|
raid.signups?.Healer
|
||||||
|
?.length
|
||||||
|
?? 0,
|
||||||
|
|
||||||
|
DPS:
|
||||||
|
raid.signups?.DPS
|
||||||
|
?.length
|
||||||
|
?? 0,
|
||||||
|
|
||||||
|
Flex:
|
||||||
|
raid.signups?.Flex
|
||||||
|
?.length
|
||||||
|
?? 0
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Count UNIQUE participants.
|
||||||
|
*
|
||||||
|
* A user who reacts Tank + DPS + Flex
|
||||||
|
* still counts as one participant.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const uniqueParticipants =
|
||||||
|
new Set([
|
||||||
|
...(
|
||||||
|
raid.signups?.Tank
|
||||||
|
?? []
|
||||||
|
),
|
||||||
|
|
||||||
|
...(
|
||||||
|
raid.signups?.Healer
|
||||||
|
?? []
|
||||||
|
),
|
||||||
|
|
||||||
|
...(
|
||||||
|
raid.signups?.DPS
|
||||||
|
?? []
|
||||||
|
),
|
||||||
|
|
||||||
|
...(
|
||||||
|
raid.signups?.Flex
|
||||||
|
?? []
|
||||||
|
)
|
||||||
|
]).size;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Clone existing embed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const oldEmbed =
|
||||||
|
message.embeds[0];
|
||||||
|
|
||||||
|
|
||||||
|
const updatedEmbed =
|
||||||
|
EmbedBuilder.from(
|
||||||
|
oldEmbed
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Replace only Party field.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fields =
|
||||||
|
oldEmbed.fields.map(
|
||||||
|
field => {
|
||||||
|
|
||||||
|
if (
|
||||||
|
field.name !==
|
||||||
|
"Party"
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
name:
|
||||||
|
field.name,
|
||||||
|
|
||||||
|
value:
|
||||||
|
field.value,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
field.inline
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
name:
|
||||||
|
"Party",
|
||||||
|
|
||||||
|
value:
|
||||||
|
buildPartyField(
|
||||||
|
duty,
|
||||||
|
counts,
|
||||||
|
uniqueParticipants
|
||||||
|
),
|
||||||
|
|
||||||
|
inline:
|
||||||
|
false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
updatedEmbed.setFields(
|
||||||
|
fields
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Update message.
|
||||||
|
*/
|
||||||
|
|
||||||
|
await message.edit({
|
||||||
|
embeds: [
|
||||||
|
updatedEmbed
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* BUILD PARTY FIELD
|
||||||
|
*/
|
||||||
|
|
||||||
|
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}**`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* EMOJI -> ROLE
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getRoleFromEmoji(
|
||||||
|
emoji
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (
|
||||||
|
emoji.id ===
|
||||||
|
RAID_EMOJIS.Tank.id
|
||||||
|
) {
|
||||||
|
return "Tank";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
emoji.id ===
|
||||||
|
RAID_EMOJIS.Healer.id
|
||||||
|
) {
|
||||||
|
return "Healer";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
emoji.id ===
|
||||||
|
RAID_EMOJIS.DPS.id
|
||||||
|
) {
|
||||||
|
return "DPS";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
emoji.id ===
|
||||||
|
RAID_EMOJIS.Flex.id
|
||||||
|
) {
|
||||||
|
return "Flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handleRaidReaction,
|
||||||
|
updateRaidPlanner
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user