363 lines
13 KiB
JavaScript
363 lines
13 KiB
JavaScript
// _opt/schangar.js
|
|
import { SlashCommandBuilder } from 'discord.js';
|
|
|
|
// Export commands array for the centralized handler
|
|
export const commands = [
|
|
{
|
|
data: new SlashCommandBuilder()
|
|
.setName('hangarsync')
|
|
.setDescription('Mark the moment all five lights turn green, for use with hangarstatus')
|
|
.addStringOption(option =>
|
|
option.setName('timestamp')
|
|
.setDescription('Custom timestamp (Unix time in seconds or ISO 8601 format). Leave empty for current time.')
|
|
.setRequired(false)),
|
|
|
|
execute: async (interaction, client) => {
|
|
const customTimestamp = interaction.options.getString('timestamp');
|
|
let syncEpoch;
|
|
|
|
// Attempt to validate custom timestamp
|
|
if (customTimestamp) {
|
|
try {
|
|
if (/^\d+$/.test(customTimestamp)) {
|
|
const timestampInSeconds = parseInt(customTimestamp);
|
|
if (timestampInSeconds < 0 || timestampInSeconds > Math.floor(Date.now() / 1000)) {
|
|
return interaction.reply({
|
|
content: 'Invalid timestamp. Please provide a Unix time in seconds that is not in the future.',
|
|
ephemeral: true
|
|
});
|
|
}
|
|
syncEpoch = timestampInSeconds * 1000;
|
|
} else {
|
|
const date = new Date(customTimestamp);
|
|
syncEpoch = date.getTime();
|
|
if (isNaN(syncEpoch) || syncEpoch < 0) {
|
|
return interaction.reply({
|
|
content: 'Invalid timestamp format. Please use Unix time in seconds or a valid ISO 8601 string.',
|
|
ephemeral: true
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
client.logger.error(`[cmd:hangarsync] Failed to parse timestamp: ${error.message}`);
|
|
return interaction.reply({
|
|
content: 'Failed to parse timestamp. Please use Unix time in seconds or a valid ISO 8601 string.',
|
|
ephemeral: true
|
|
});
|
|
}
|
|
} else {
|
|
syncEpoch = Date.now();
|
|
}
|
|
|
|
// Check PocketBase connection status
|
|
if (!isPocketBaseConnected(client)) {
|
|
client.logger.error('[cmd:hangarsync] PocketBase not connected');
|
|
|
|
// Try to reconnect if available
|
|
if (typeof client.pb.ensureConnection === 'function') {
|
|
await client.pb.ensureConnection();
|
|
|
|
// Check if reconnection worked
|
|
if (!isPocketBaseConnected(client)) {
|
|
return interaction.reply({
|
|
content: 'Database connection unavailable. Please try again later.',
|
|
ephemeral: true
|
|
});
|
|
}
|
|
} else {
|
|
return interaction.reply({
|
|
content: 'Database connection unavailable. Please try again later.',
|
|
ephemeral: true
|
|
});
|
|
}
|
|
}
|
|
|
|
// Create or update timestamp for guild
|
|
try {
|
|
let record = null;
|
|
|
|
try {
|
|
// First try the enhanced method if available
|
|
if (typeof client.pb.getFirst === 'function') {
|
|
record = await client.pb.getFirst('command_hangarsync', `guildId = "${interaction.guildId}"`);
|
|
} else {
|
|
// Fall back to standard PocketBase method
|
|
const records = await client.pb.collection('command_hangarsync').getList(1, 1, {
|
|
filter: `guildId = "${interaction.guildId}"`
|
|
});
|
|
if (records.items.length > 0) {
|
|
record = records.items[0];
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Handle case where collection might not exist
|
|
client.logger.warn(`Error retrieving hangarsync record: ${error.message}`);
|
|
}
|
|
|
|
if (record) {
|
|
// Update existing record
|
|
if (typeof client.pb.updateOne === 'function') {
|
|
await client.pb.updateOne('command_hangarsync', record.id, {
|
|
userId: `${interaction.user.id}`,
|
|
epoch: `${syncEpoch}`,
|
|
});
|
|
} else {
|
|
await client.pb.collection('command_hangarsync').update(record.id, {
|
|
userId: `${interaction.user.id}`,
|
|
epoch: `${syncEpoch}`,
|
|
});
|
|
}
|
|
client.logger.info(`[cmd:hangarsync] Updated hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`);
|
|
} else {
|
|
// Create new record
|
|
if (typeof client.pb.createOne === 'function') {
|
|
await client.pb.createOne('command_hangarsync', {
|
|
guildId: `${interaction.guildId}`,
|
|
userId: `${interaction.user.id}`,
|
|
epoch: `${syncEpoch}`,
|
|
});
|
|
} else {
|
|
await client.pb.collection('command_hangarsync').create({
|
|
guildId: `${interaction.guildId}`,
|
|
userId: `${interaction.user.id}`,
|
|
epoch: `${syncEpoch}`,
|
|
});
|
|
}
|
|
client.logger.info(`[cmd:hangarsync] Created new hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`);
|
|
}
|
|
|
|
await interaction.reply(`Executive hangar status has been synced: <t:${Math.ceil(syncEpoch / 1000)}>`);
|
|
} catch (error) {
|
|
client.logger.error(`[cmd:hangarsync] Error: ${error.message}`);
|
|
await interaction.reply({
|
|
content: `Error syncing hangar status. Please try again later.`,
|
|
ephemeral: true
|
|
});
|
|
}
|
|
}
|
|
},
|
|
{
|
|
data: new SlashCommandBuilder()
|
|
.setName('hangarstatus')
|
|
.setDescription('Check the status of contested zone executive hangars')
|
|
.addBooleanOption(option =>
|
|
option.setName('verbose')
|
|
.setDescription('Extra output, mainly for debugging.')
|
|
.setRequired(false)),
|
|
|
|
execute: async (interaction, client) => {
|
|
const verbose = interaction.options.getBoolean('verbose');
|
|
|
|
// Check PocketBase connection status
|
|
if (!isPocketBaseConnected(client)) {
|
|
client.logger.error('[cmd:hangarstatus] PocketBase not connected');
|
|
|
|
// Try to reconnect if available
|
|
if (typeof client.pb.ensureConnection === 'function') {
|
|
await client.pb.ensureConnection();
|
|
|
|
// Check if reconnection worked
|
|
if (!isPocketBaseConnected(client)) {
|
|
return interaction.reply({
|
|
content: 'Database connection unavailable. Please try again later.',
|
|
ephemeral: true
|
|
});
|
|
}
|
|
} else {
|
|
return interaction.reply({
|
|
content: 'Database connection unavailable. Please try again later.',
|
|
ephemeral: true
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Get hangarsync data for guild
|
|
let hangarSync = null;
|
|
|
|
try {
|
|
// First try the enhanced method if available
|
|
if (typeof client.pb.getFirst === 'function') {
|
|
hangarSync = await client.pb.getFirst('command_hangarsync', `guildId = "${interaction.guildId}"`);
|
|
} else {
|
|
// Fall back to standard PocketBase methods
|
|
try {
|
|
hangarSync = await client.pb.collection('command_hangarsync').getFirstListItem(`guildId = "${interaction.guildId}"`);
|
|
} catch (error) {
|
|
// getFirstListItem throws if no items found
|
|
if (error.status !== 404) throw error;
|
|
}
|
|
}
|
|
|
|
if (!hangarSync) {
|
|
client.logger.info(`[cmd:hangarstatus] No sync data found for guild ${interaction.guildId}`);
|
|
return interaction.reply({
|
|
content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.',
|
|
ephemeral: true
|
|
});
|
|
}
|
|
} catch (error) {
|
|
client.logger.error(`[cmd:hangarstatus] Error retrieving sync data for guild ${interaction.guildId}: ${error.message}`);
|
|
return interaction.reply({
|
|
content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.',
|
|
ephemeral: true
|
|
});
|
|
}
|
|
|
|
const currentTime = Date.now();
|
|
|
|
// 5 minutes (all off) + 5*24 minutes (turning green) + 5*12 minutes (turning off) = 185 minutes
|
|
const cycleDuration = 5 + (5 * 24) + (5 * 12);
|
|
|
|
// Key positions in the cycle
|
|
const allOffDuration = 5;
|
|
const turningGreenDuration = 5 * 24;
|
|
const turningOffDuration = 5 * 12;
|
|
|
|
// Calculate how much time has passed since the epoch
|
|
const timeSinceEpoch = (currentTime - hangarSync.epoch) / (60 * 1000);
|
|
|
|
// Calculate where we are in the full-cycle relative to the epoch
|
|
const cyclePosition = ((timeSinceEpoch % cycleDuration) + cycleDuration) % cycleDuration;
|
|
|
|
// Initialize stuff and things
|
|
const lights = [":black_circle:", ":black_circle:", ":black_circle:", ":black_circle:", ":black_circle:"];
|
|
let minutesUntilNextPhase = 0;
|
|
let currentPhase = "";
|
|
|
|
// If the epoch is now, we should be at the all-green position.
|
|
// From there, we need to determine where we are in the cycle.
|
|
|
|
// Case 1: We're in the unlocked phase, right after epoch
|
|
if (cyclePosition < turningOffDuration) {
|
|
currentPhase = "Unlocked";
|
|
|
|
// All lights start as green
|
|
lights.fill(":green_circle:");
|
|
|
|
// Calculate how many lights have turned off
|
|
const offLights = Math.floor(cyclePosition / 12);
|
|
|
|
// Set the appropriate number of lights to off
|
|
for (let i = 0; i < offLights; i++) {
|
|
lights[i] = ":black_circle:";
|
|
}
|
|
|
|
// Calculate time until next light turns off
|
|
const timeUntilNextLight = 12 - (cyclePosition % 12);
|
|
minutesUntilNextPhase = timeUntilNextLight;
|
|
}
|
|
|
|
// Case 2: We're in the reset phase
|
|
else if (cyclePosition < turningOffDuration + allOffDuration) {
|
|
currentPhase = "Resetting";
|
|
|
|
// Lights are initialized "off", so do nothing with them
|
|
|
|
// Calculate time until all lights turn red
|
|
const timeIntoPhase = cyclePosition - turningOffDuration;
|
|
minutesUntilNextPhase = allOffDuration - timeIntoPhase;
|
|
}
|
|
|
|
// Case 3: We're in the locked phase
|
|
else {
|
|
currentPhase = "Locked";
|
|
|
|
// All lights start as red
|
|
lights.fill(":red_circle:");
|
|
|
|
// Calculate how many lights have turned green
|
|
const timeIntoPhase = cyclePosition - (turningOffDuration + allOffDuration);
|
|
const greenLights = Math.floor(timeIntoPhase / 24);
|
|
|
|
// Set the appropriate number of lights to green
|
|
for (let i = 0; i < greenLights; i++) {
|
|
lights[i] = ":green_circle:";
|
|
}
|
|
|
|
// Calculate time until next light turns green
|
|
const timeUntilNextLight = 24 - (timeIntoPhase % 24);
|
|
minutesUntilNextPhase = timeUntilNextLight;
|
|
}
|
|
|
|
// Calculate a timestamp for Discord's formatting and reply
|
|
const expiration = Math.ceil((Date.now() / 1000) + (minutesUntilNextPhase * 60));
|
|
// Determine time to next Lock/Unlock phase for inline display
|
|
const isUnlocked = currentPhase === 'Unlocked';
|
|
const label = isUnlocked ? 'Lock' : 'Unlock';
|
|
const minutesToPhase = isUnlocked
|
|
? (turningOffDuration + allOffDuration) - cyclePosition
|
|
: cycleDuration - cyclePosition;
|
|
const phaseEpoch = Math.ceil(Date.now() / 1000 + (minutesToPhase * 60));
|
|
// Reply with lights and inline time to phase
|
|
await interaction.reply(
|
|
`### ${lights[0]} ${lights[1]} ${lights[2]} ${lights[3]} ${lights[4]} — Time to ${label}: <t:${phaseEpoch}:R>`
|
|
);
|
|
|
|
if (verbose) {
|
|
// Replace user mention with displayName for last sync
|
|
const syncMember = await interaction.guild.members.fetch(hangarSync.userId).catch(() => null);
|
|
const syncName = syncMember ? syncMember.displayName : `<@${hangarSync.userId}>`;
|
|
|
|
// Calculate time until next Lock/Unlock phase
|
|
const isUnlocked = currentPhase === 'Unlocked';
|
|
const label = isUnlocked ? 'Lock' : 'Unlock';
|
|
const minutesToPhase = isUnlocked
|
|
? (turningOffDuration + allOffDuration) - cyclePosition
|
|
: cycleDuration - cyclePosition;
|
|
const phaseEpoch = Math.ceil(Date.now() / 1000 + (minutesToPhase * 60));
|
|
|
|
await interaction.followUp(
|
|
`- **Phase**: ${currentPhase}\n` +
|
|
`- **Time to ${label}**: <t:${phaseEpoch}:R>\n` +
|
|
`- **Status Expiration**: <t:${expiration}:R>\n` +
|
|
`- **Epoch**: <t:${Math.ceil(hangarSync.epoch / 1000)}:R>\n` +
|
|
`- **Sync**: <t:${Math.floor(new Date(hangarSync.updated).getTime() / 1000)}:R> by ${syncName}`
|
|
);
|
|
|
|
// Add additional debug info to logs
|
|
client.logger.debug(`Hangarstatus for guild ${interaction.guildId}: Phase=${currentPhase}, CyclePosition=${cyclePosition}, TimeSinceEpoch=${timeSinceEpoch}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
client.logger.error(`Error in hangarstatus command: ${error.message}`);
|
|
await interaction.reply({
|
|
content: `Error retrieving hangar status. Please try again later.`,
|
|
ephemeral: true
|
|
});
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
// Function to check PocketBase connection status
|
|
function isPocketBaseConnected(client) {
|
|
// Check multiple possible status indicators to be safe
|
|
return client.pb && (
|
|
// Check status object (original code style)
|
|
(client.pb.status && client.pb.status.connected) ||
|
|
// Check isConnected property (pbutils module style)
|
|
client.pb.isConnected === true ||
|
|
// Last resort: check if authStore is valid
|
|
client.pb.authStore?.isValid === true
|
|
);
|
|
}
|
|
|
|
// Initialize module
|
|
export const init = async (client, config) => {
|
|
client.logger.info('Initializing Star Citizen Hangar Status module');
|
|
|
|
// Check PocketBase connection
|
|
if (!isPocketBaseConnected(client)) {
|
|
client.logger.warn('PocketBase not connected at initialization');
|
|
|
|
// Try to reconnect if available
|
|
if (typeof client.pb.ensureConnection === 'function') {
|
|
await client.pb.ensureConnection();
|
|
}
|
|
} else {
|
|
client.logger.info('PocketBase connection confirmed');
|
|
}
|
|
|
|
client.logger.info('Star Citizen Hangar Status module initialized');
|
|
}; |