Update
This commit is contained in:
@@ -1,102 +0,0 @@
|
|||||||
import { DateTime } from "luxon";
|
|
||||||
import { AccessToken, WebhookConfig } from "@/types";
|
|
||||||
import { planetCalculations } from "@/planets";
|
|
||||||
|
|
||||||
export class NotificationService {
|
|
||||||
private notificationState = new Map<string, boolean>();
|
|
||||||
private lastWebhookTime = 0;
|
|
||||||
private readonly WEBHOOK_COOLDOWN = 5000; // 5 seconds between webhooks
|
|
||||||
|
|
||||||
public checkAndNotify = async (
|
|
||||||
characters: AccessToken[],
|
|
||||||
webhookConfig: WebhookConfig
|
|
||||||
): Promise<void> => {
|
|
||||||
if (!webhookConfig.enabled) return;
|
|
||||||
|
|
||||||
const notifications: string[] = [];
|
|
||||||
const now = DateTime.now();
|
|
||||||
|
|
||||||
for (const character of characters) {
|
|
||||||
for (const planet of character.planets) {
|
|
||||||
const planetDetails = planetCalculations(planet);
|
|
||||||
|
|
||||||
// Check extractors for expiry warnings
|
|
||||||
for (const extractor of planetDetails.extractors) {
|
|
||||||
if (extractor.expiry_time) {
|
|
||||||
const expiryTime = DateTime.fromISO(extractor.expiry_time);
|
|
||||||
const identifier = `${character.character.name}-${planet.planet_id}-${extractor.pin_id}`;
|
|
||||||
|
|
||||||
// Check if already expired
|
|
||||||
if (expiryTime <= now) {
|
|
||||||
if (!this.notificationState.get(`${identifier}-expired`)) {
|
|
||||||
notifications.push(`🚨 EXTRACTOR EXPIRED - ${character.character.name} Planet ${planet.planet_id}`);
|
|
||||||
this.notificationState.set(`${identifier}-expired`, true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check if expiring soon (within the warning threshold)
|
|
||||||
const warningThreshold = this.parseDuration(webhookConfig.expiryThreshold || "P12H");
|
|
||||||
const warningTime = now.plus(warningThreshold);
|
|
||||||
|
|
||||||
if (expiryTime <= warningTime && expiryTime > now) {
|
|
||||||
if (!this.notificationState.get(`${identifier}-warning`)) {
|
|
||||||
const hoursLeft = Math.round(expiryTime.diff(now, "hours").hours);
|
|
||||||
notifications.push(`⚠️ EXTRACTOR EXPIRING SOON - ${character.character.name} Planet ${planet.planet_id} (${hoursLeft}h left)`);
|
|
||||||
this.notificationState.set(`${identifier}-warning`, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check storage capacity
|
|
||||||
for (const storage of planetDetails.storageInfo) {
|
|
||||||
const identifier = `${character.character.name}-${planet.planet_id}-${storage.type_id}`;
|
|
||||||
|
|
||||||
if (storage.fillRate >= (webhookConfig.storageCriticalThreshold || 100)) {
|
|
||||||
if (!this.notificationState.get(`${identifier}-critical`)) {
|
|
||||||
notifications.push(`🚨 STORAGE CRITICAL - ${character.character.name} Planet ${planet.planet_id} (${storage.fillRate.toFixed(1)}%)`);
|
|
||||||
this.notificationState.set(`${identifier}-critical`, true);
|
|
||||||
}
|
|
||||||
} else if (storage.fillRate >= (webhookConfig.storageWarningThreshold || 85)) {
|
|
||||||
if (!this.notificationState.get(`${identifier}-warning`)) {
|
|
||||||
notifications.push(`⚠️ STORAGE WARNING - ${character.character.name} Planet ${planet.planet_id} (${storage.fillRate.toFixed(1)}%)`);
|
|
||||||
this.notificationState.set(`${identifier}-warning`, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notifications.length > 0) {
|
|
||||||
await this.sendWebhook(notifications.join("\n"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private parseDuration(duration: string): any {
|
|
||||||
// Parse ISO 8601 duration (e.g., "P12H", "P1D", "PT2H30M")
|
|
||||||
try {
|
|
||||||
return DateTime.fromISO(`2000-01-01T00:00:00Z`).plus({ [duration.slice(-1) === 'H' ? 'hours' : duration.slice(-1) === 'D' ? 'days' : 'minutes' ]: parseInt(duration.slice(1, -1)) }).diff(DateTime.fromISO(`2000-01-01T00:00:00Z`));
|
|
||||||
} catch {
|
|
||||||
// Default to 12 hours if parsing fails
|
|
||||||
return { hours: 12 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendWebhook = async (content: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/webhook", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ content }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Webhook failed: ${response.status}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Webhook error:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const notificationService = new NotificationService();
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
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, config: WebhookConfig): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
console.log('🔔 Sending webhook:', payload);
|
|
||||||
|
|
||||||
// Include Zulip configuration in the payload
|
|
||||||
const webhookPayload = {
|
|
||||||
...payload,
|
|
||||||
zulipUrl: config.zulipUrl,
|
|
||||||
zulipEmail: config.zulipEmail,
|
|
||||||
zulipApiKey: config.zulipApiKey,
|
|
||||||
zulipStream: config.zulipStream
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch('/api/webhook', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(webhookPayload),
|
|
||||||
});
|
|
||||||
|
|
||||||
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<void> => {
|
|
||||||
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, config)) {
|
|
||||||
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, config)) {
|
|
||||||
markWebhookSent(character.character.characterId, planet.planet_id, extractor.expiry_time, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkStorageCapacity = async (
|
|
||||||
character: AccessToken,
|
|
||||||
planet: PlanetWithInfo,
|
|
||||||
storageInfo: StorageInfo[],
|
|
||||||
config: WebhookConfig
|
|
||||||
): Promise<void> => {
|
|
||||||
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, config)) {
|
|
||||||
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, config)) {
|
|
||||||
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<void> => {
|
|
||||||
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)
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user