import { DateTime, Duration } from 'luxon'; import { Pin, PlanetWithInfo, AccessToken } from '@/types'; import { StorageInfo } from '@/types/planet'; import { WebhookConfig, WebhookPayload } from '@/types/webhook'; import { STORAGE_IDS, LAUNCHPAD_IDS, PI_TYPES_MAP, STORAGE_CAPACITIES, PI_PRODUCT_VOLUMES } from '@/const'; import { shouldSendWebhook, markWebhookSent } from './webhookTracker'; const sendWebhook = async (payload: WebhookPayload): Promise => { try { console.log('🔔 Sending webhook:', payload); const response = await fetch('/api/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); if (response.ok) { const result = await response.json(); console.log('✅ Webhook sent successfully:', result.message); } else { const error = await response.text(); console.log('❌ Webhook failed:', response.status, response.statusText, error); } return response.ok; } catch (error) { console.error('❌ Failed to send webhook:', error); return false; } }; export const checkExtractorExpiry = async ( character: AccessToken, planet: PlanetWithInfo, extractors: Pin[], config: WebhookConfig ): Promise => { if (!config.enabled) return; if (extractors.length === 0) { console.log(`📊 No extractors found on ${planet.infoUniverse.name}`); return; } console.log(`⏰ Checking ${extractors.length} extractors on ${planet.infoUniverse.name}`); const now = DateTime.now(); const expiryThreshold = Duration.fromISO(config.expiryThreshold); for (const extractor of extractors) { if (!extractor.expiry_time || !extractor.extractor_details?.product_type_id) continue; const expiryTime = DateTime.fromISO(extractor.expiry_time); const timeUntilExpiry = expiryTime.diff(now); const hoursRemaining = timeUntilExpiry.as('hours'); const productType = PI_TYPES_MAP[extractor.extractor_details.product_type_id]; const extractorType = productType?.name || `Type ${extractor.extractor_details.product_type_id}`; // Check if extractor has expired if (expiryTime <= now) { console.log(`⏰ Extractor expired: ${extractorType} on ${planet.infoUniverse.name}`); const event = 'done'; if (shouldSendWebhook(character.character.characterId, planet.planet_id, extractor.expiry_time, event)) { const payload: WebhookPayload = { type: 'extractor_expired', message: `Extractor producing ${extractorType} has expired on ${planet.infoUniverse.name}`, characterName: character.character.name, planetName: planet.infoUniverse.name, details: { extractorType, hoursRemaining: 0 }, timestamp: now.toISO() }; if (await sendWebhook(payload)) { markWebhookSent(character.character.characterId, planet.planet_id, extractor.expiry_time, event); } } } // Check if extractor is about to expire else if (timeUntilExpiry <= expiryThreshold) { console.log(`⚠️ Extractor expiring soon: ${extractorType} on ${planet.infoUniverse.name} (${hoursRemaining.toFixed(1)}h remaining)`); const event = 'nearly done'; if (shouldSendWebhook(character.character.characterId, planet.planet_id, extractor.expiry_time, event)) { const payload: WebhookPayload = { type: 'extractor_expiring', message: `Extractor producing ${extractorType} will expire in ${hoursRemaining.toFixed(1)} hours on ${planet.infoUniverse.name}`, characterName: character.character.name, planetName: planet.infoUniverse.name, details: { extractorType, hoursRemaining: Math.max(0, hoursRemaining) }, timestamp: now.toISO() }; if (await sendWebhook(payload)) { markWebhookSent(character.character.characterId, planet.planet_id, extractor.expiry_time, event); } } } } }; export const checkStorageCapacity = async ( character: AccessToken, planet: PlanetWithInfo, storageInfo: StorageInfo[], config: WebhookConfig ): Promise => { if (!config.enabled) return; if (storageInfo.length === 0) { console.log(`📦 No storage facilities found on ${planet.infoUniverse.name}`); return; } console.log(`📦 Checking ${storageInfo.length} storage facilities on ${planet.infoUniverse.name}`); const now = DateTime.now(); for (const storage of storageInfo) { const fillPercentage = (storage.used / storage.capacity) * 100; const isLaunchpad = LAUNCHPAD_IDS.includes(storage.type_id); const storageTypeName = PI_TYPES_MAP[storage.type_id]?.name || `Storage ${storage.type_id}`; // Check for critical (100%) storage if (fillPercentage >= config.storageCriticalThreshold) { const webhookType = isLaunchpad ? 'launchpad_full' : 'storage_full'; console.log(`🚨 ${storageTypeName} is ${fillPercentage.toFixed(1)}% full on ${planet.infoUniverse.name}`); if (shouldSendWebhook(character.character.characterId, planet.planet_id, webhookType, undefined, storage.type_id)) { const payload: WebhookPayload = { type: webhookType, message: `${storageTypeName} is ${fillPercentage.toFixed(1)}% full on ${planet.infoUniverse.name}`, characterName: character.character.name, planetName: planet.infoUniverse.name, details: { storageUsed: storage.used, storageCapacity: storage.capacity, fillPercentage: fillPercentage }, timestamp: now.toISO() }; if (await sendWebhook(payload)) { markWebhookSent(character.character.characterId, planet.planet_id, webhookType, undefined, storage.type_id); } } } // Check for warning threshold else if (fillPercentage >= config.storageWarningThreshold) { const webhookType = isLaunchpad ? 'launchpad_almost_full' : 'storage_almost_full'; console.log(`⚠️ ${storageTypeName} is ${fillPercentage.toFixed(1)}% full on ${planet.infoUniverse.name}`); if (shouldSendWebhook(character.character.characterId, planet.planet_id, webhookType, undefined, storage.type_id)) { const payload: WebhookPayload = { type: webhookType, message: `${storageTypeName} is ${fillPercentage.toFixed(1)}% full on ${planet.infoUniverse.name}`, characterName: character.character.name, planetName: planet.infoUniverse.name, details: { storageUsed: storage.used, storageCapacity: storage.capacity, fillPercentage: fillPercentage }, timestamp: now.toISO() }; if (await sendWebhook(payload)) { markWebhookSent(character.character.characterId, planet.planet_id, webhookType, undefined, storage.type_id); } } } } }; const calculateStorageInfo = (planet: PlanetWithInfo): StorageInfo[] => { const storageFacilities = planet.info.pins.filter((pin: Pin) => STORAGE_IDS().some(storage => storage.type_id === pin.type_id) ); return storageFacilities.map((storage: Pin): StorageInfo => { const storageType = PI_TYPES_MAP[storage.type_id]?.name || 'Unknown'; const storageCapacity = STORAGE_CAPACITIES[storage.type_id] || 0; const totalVolume = (storage.contents || []) .reduce((sum: number, item) => { const volume = PI_PRODUCT_VOLUMES[item.type_id] || 0; return sum + (item.amount * volume); }, 0); const fillRate = storageCapacity > 0 ? (totalVolume / storageCapacity) * 100 : 0; return { type: storageType, type_id: storage.type_id, capacity: storageCapacity, used: totalVolume, fillRate: fillRate, value: 0 // We don't need value for webhook checks }; }); }; export const runWebhookChecks = async ( character: AccessToken, planet: PlanetWithInfo, extractors: Pin[], config: WebhookConfig ): Promise => { if (!config.enabled) { console.log('🔕 Webhooks disabled, skipping checks'); return; } console.log(`🔍 Running webhook checks for ${character.character.name} - ${planet.infoUniverse.name}`); const storageInfo = calculateStorageInfo(planet); await Promise.all([ checkExtractorExpiry(character, planet, extractors, config), checkStorageCapacity(character, planet, storageInfo, config) ]); };