Stop spamming notifications
This commit is contained in:
@@ -32,9 +32,9 @@ export default async function handler(
|
|||||||
// Send webhook to external URL
|
// Send webhook to external URL
|
||||||
console.log('🔔 Sending webhook to external URL:', webhookUrl);
|
console.log('🔔 Sending webhook to external URL:', webhookUrl);
|
||||||
|
|
||||||
// Use simple format as requested
|
// Use simple format with all the details
|
||||||
const webhookPayload = {
|
const webhookPayload = {
|
||||||
content: payload.message
|
content: `${payload.message}\n**Character:** ${payload.characterName}\n**Planet:** ${payload.planetName}\n**Time:** ${new Date(payload.timestamp).toLocaleString()}`
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('📤 Sending payload:', JSON.stringify(webhookPayload, null, 2));
|
console.log('📤 Sending payload:', JSON.stringify(webhookPayload, null, 2));
|
||||||
|
@@ -3,7 +3,7 @@ import { Pin, PlanetWithInfo, AccessToken } from '@/types';
|
|||||||
import { StorageInfo } from '@/types/planet';
|
import { StorageInfo } from '@/types/planet';
|
||||||
import { WebhookConfig, WebhookPayload } from '@/types/webhook';
|
import { WebhookConfig, WebhookPayload } from '@/types/webhook';
|
||||||
import { STORAGE_IDS, LAUNCHPAD_IDS, PI_TYPES_MAP, STORAGE_CAPACITIES, PI_PRODUCT_VOLUMES } from '@/const';
|
import { STORAGE_IDS, LAUNCHPAD_IDS, PI_TYPES_MAP, STORAGE_CAPACITIES, PI_PRODUCT_VOLUMES } from '@/const';
|
||||||
import { shouldSendWebhook, markWebhookSent, resetExtractorWebhookStates } from './webhookTracker';
|
import { shouldSendWebhook, markWebhookSent } from './webhookTracker';
|
||||||
|
|
||||||
const sendWebhook = async (payload: WebhookPayload): Promise<boolean> => {
|
const sendWebhook = async (payload: WebhookPayload): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
@@ -61,7 +61,8 @@ export const checkExtractorExpiry = async (
|
|||||||
// Check if extractor has expired
|
// Check if extractor has expired
|
||||||
if (expiryTime <= now) {
|
if (expiryTime <= now) {
|
||||||
console.log(`⏰ Extractor expired: ${extractorType} on ${planet.infoUniverse.name}`);
|
console.log(`⏰ Extractor expired: ${extractorType} on ${planet.infoUniverse.name}`);
|
||||||
if (shouldSendWebhook(character.character.characterId, planet.planet_id, 'extractor_expired', extractor.pin_id)) {
|
const event = 'done';
|
||||||
|
if (shouldSendWebhook(character.character.characterId, planet.planet_id, extractor.expiry_time, event)) {
|
||||||
const payload: WebhookPayload = {
|
const payload: WebhookPayload = {
|
||||||
type: 'extractor_expired',
|
type: 'extractor_expired',
|
||||||
message: `Extractor producing ${extractorType} has expired on ${planet.infoUniverse.name}`,
|
message: `Extractor producing ${extractorType} has expired on ${planet.infoUniverse.name}`,
|
||||||
@@ -75,14 +76,15 @@ export const checkExtractorExpiry = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (await sendWebhook(payload)) {
|
if (await sendWebhook(payload)) {
|
||||||
markWebhookSent(character.character.characterId, planet.planet_id, 'extractor_expired', extractor.pin_id);
|
markWebhookSent(character.character.characterId, planet.planet_id, extractor.expiry_time, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check if extractor is about to expire
|
// Check if extractor is about to expire
|
||||||
else if (timeUntilExpiry <= expiryThreshold) {
|
else if (timeUntilExpiry <= expiryThreshold) {
|
||||||
console.log(`⚠️ Extractor expiring soon: ${extractorType} on ${planet.infoUniverse.name} (${hoursRemaining.toFixed(1)}h remaining)`);
|
console.log(`⚠️ Extractor expiring soon: ${extractorType} on ${planet.infoUniverse.name} (${hoursRemaining.toFixed(1)}h remaining)`);
|
||||||
if (shouldSendWebhook(character.character.characterId, planet.planet_id, 'extractor_expiring', extractor.pin_id)) {
|
const event = 'nearly done';
|
||||||
|
if (shouldSendWebhook(character.character.characterId, planet.planet_id, extractor.expiry_time, event)) {
|
||||||
const payload: WebhookPayload = {
|
const payload: WebhookPayload = {
|
||||||
type: 'extractor_expiring',
|
type: 'extractor_expiring',
|
||||||
message: `Extractor producing ${extractorType} will expire in ${hoursRemaining.toFixed(1)} hours on ${planet.infoUniverse.name}`,
|
message: `Extractor producing ${extractorType} will expire in ${hoursRemaining.toFixed(1)} hours on ${planet.infoUniverse.name}`,
|
||||||
@@ -96,14 +98,10 @@ export const checkExtractorExpiry = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (await sendWebhook(payload)) {
|
if (await sendWebhook(payload)) {
|
||||||
markWebhookSent(character.character.characterId, planet.planet_id, 'extractor_expiring', extractor.pin_id);
|
markWebhookSent(character.character.characterId, planet.planet_id, extractor.expiry_time, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If extractor has been reset (duration is "full" again), reset webhook states
|
|
||||||
else if (timeUntilExpiry > expiryThreshold.plus({ hours: 1 })) {
|
|
||||||
resetExtractorWebhookStates(character.character.characterId, planet.planet_id, extractor.pin_id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,103 +1,122 @@
|
|||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
export interface WebhookState {
|
interface WebhookState {
|
||||||
lastSent: string;
|
sent: boolean;
|
||||||
type: string;
|
lastSent: number;
|
||||||
planetId: number;
|
|
||||||
characterId: number;
|
|
||||||
extractorPinId?: number;
|
|
||||||
storageTypeId?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-memory storage for webhook states (in production, you might want to use localStorage or a database)
|
// Create hash for unique events: character-planet-extractorEndTimestamp-event
|
||||||
const webhookStates = new Map<string, WebhookState>();
|
export const createWebhookHash = (
|
||||||
|
|
||||||
export const generateWebhookKey = (
|
|
||||||
characterId: number,
|
characterId: number,
|
||||||
planetId: number,
|
planetId: number,
|
||||||
type: string,
|
extractorEndTimestamp: string,
|
||||||
extractorPinId?: number,
|
event: string
|
||||||
storageTypeId?: number
|
|
||||||
): string => {
|
): string => {
|
||||||
const parts = [characterId, planetId, type];
|
return `${characterId}-${planetId}-${extractorEndTimestamp}-${event}`;
|
||||||
if (extractorPinId !== undefined) parts.push(`extractor-${extractorPinId}`);
|
};
|
||||||
if (storageTypeId !== undefined) parts.push(`storage-${storageTypeId}`);
|
|
||||||
return parts.join('-');
|
// Get webhook state from localStorage
|
||||||
|
const getWebhookState = (hash: string): WebhookState => {
|
||||||
|
if (typeof window === 'undefined') return { sent: false, lastSent: 0 };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(`webhook_${hash}`);
|
||||||
|
if (stored) {
|
||||||
|
return JSON.parse(stored);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to parse webhook state from localStorage:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sent: false, lastSent: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set webhook state in localStorage
|
||||||
|
const setWebhookState = (hash: string, state: WebhookState): void => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(`webhook_${hash}`, JSON.stringify(state));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to save webhook state to localStorage:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shouldSendWebhook = (
|
export const shouldSendWebhook = (
|
||||||
characterId: number,
|
characterId: number,
|
||||||
planetId: number,
|
planetId: number,
|
||||||
type: string,
|
extractorEndTimestamp: string,
|
||||||
extractorPinId?: number,
|
event: string
|
||||||
storageTypeId?: number
|
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const key = generateWebhookKey(characterId, planetId, type, extractorPinId, storageTypeId);
|
const hash = createWebhookHash(characterId, planetId, extractorEndTimestamp, event);
|
||||||
const state = webhookStates.get(key);
|
const state = getWebhookState(hash);
|
||||||
|
|
||||||
if (!state) {
|
return !state.sent;
|
||||||
return true; // First time, send webhook
|
};
|
||||||
|
|
||||||
|
// Storage webhook tracking with 1-hour cooldown
|
||||||
|
export const shouldSendStorageWebhook = (
|
||||||
|
characterId: number,
|
||||||
|
planetId: number,
|
||||||
|
storageTypeId: number,
|
||||||
|
webhookType: string
|
||||||
|
): boolean => {
|
||||||
|
const hash = `storage-${characterId}-${planetId}-${storageTypeId}-${webhookType}`;
|
||||||
|
const state = getWebhookState(hash);
|
||||||
|
|
||||||
|
if (!state.sent) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastSent = DateTime.fromISO(state.lastSent);
|
// Check if 1 hour has passed since last notification
|
||||||
const now = DateTime.now();
|
const oneHourAgo = Date.now() - (60 * 60 * 1000);
|
||||||
|
return state.lastSent < oneHourAgo;
|
||||||
|
};
|
||||||
|
|
||||||
// For expired extractors, resend every hour
|
export const markStorageWebhookSent = (
|
||||||
if (type === 'extractor_expired') {
|
characterId: number,
|
||||||
return now.diff(lastSent, 'hours').hours >= 1;
|
planetId: number,
|
||||||
}
|
storageTypeId: number,
|
||||||
|
webhookType: string
|
||||||
// For other alerts, resend every 4 hours
|
): void => {
|
||||||
return now.diff(lastSent, 'hours').hours >= 4;
|
const hash = `storage-${characterId}-${planetId}-${storageTypeId}-${webhookType}`;
|
||||||
|
setWebhookState(hash, { sent: true, lastSent: Date.now() });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const markWebhookSent = (
|
export const markWebhookSent = (
|
||||||
characterId: number,
|
characterId: number,
|
||||||
planetId: number,
|
planetId: number,
|
||||||
type: string,
|
extractorEndTimestamp: string,
|
||||||
extractorPinId?: number,
|
event: string
|
||||||
storageTypeId?: number
|
|
||||||
): void => {
|
): void => {
|
||||||
const key = generateWebhookKey(characterId, planetId, type, extractorPinId, storageTypeId);
|
const hash = createWebhookHash(characterId, planetId, extractorEndTimestamp, event);
|
||||||
webhookStates.set(key, {
|
setWebhookState(hash, { sent: true, lastSent: Date.now() });
|
||||||
lastSent: DateTime.now().toISO(),
|
|
||||||
type,
|
|
||||||
planetId,
|
|
||||||
characterId,
|
|
||||||
extractorPinId,
|
|
||||||
storageTypeId
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resetWebhookState = (
|
// Cleanup old webhook states (older than 7 days)
|
||||||
characterId: number,
|
|
||||||
planetId: number,
|
|
||||||
type: string,
|
|
||||||
extractorPinId?: number,
|
|
||||||
storageTypeId?: number
|
|
||||||
): void => {
|
|
||||||
const key = generateWebhookKey(characterId, planetId, type, extractorPinId, storageTypeId);
|
|
||||||
webhookStates.delete(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reset extractor webhook states when extractors are refreshed (duration is "full" again)
|
|
||||||
export const resetExtractorWebhookStates = (
|
|
||||||
characterId: number,
|
|
||||||
planetId: number,
|
|
||||||
extractorPinId: number
|
|
||||||
): void => {
|
|
||||||
resetWebhookState(characterId, planetId, 'extractor_expiring', extractorPinId);
|
|
||||||
resetWebhookState(characterId, planetId, 'extractor_expired', extractorPinId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clear old webhook states (older than 7 days)
|
|
||||||
export const cleanupOldWebhookStates = (): void => {
|
export const cleanupOldWebhookStates = (): void => {
|
||||||
const cutoff = DateTime.now().minus({ days: 7 });
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
for (const [key, state] of webhookStates.entries()) {
|
const cutoff = Date.now() - (7 * 24 * 60 * 60 * 1000); // 7 days ago
|
||||||
const lastSent = DateTime.fromISO(state.lastSent);
|
|
||||||
if (lastSent < cutoff) {
|
try {
|
||||||
webhookStates.delete(key);
|
const keysToRemove: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
if (key && key.startsWith('webhook_')) {
|
||||||
|
const state = JSON.parse(localStorage.getItem(key) || '{}');
|
||||||
|
if (state.lastSent && state.lastSent < cutoff) {
|
||||||
|
keysToRemove.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||||
|
|
||||||
|
if (keysToRemove.length > 0) {
|
||||||
|
console.log(`🧹 Cleaned up ${keysToRemove.length} old webhook states`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to cleanup old webhook states:', error);
|
||||||
|
}
|
||||||
};
|
};
|
Reference in New Issue
Block a user