ajutes meta sites

This commit is contained in:
Marco Gallegos
2025-12-18 00:19:31 -06:00
parent 7b05bf5eea
commit 2c1cbe65b1
2 changed files with 291 additions and 0 deletions

View File

@@ -123,6 +123,7 @@ Contiene la lógica de negocio más compleja en forma de scripts.
* **Verificación simple**: Para la mayoría de los sitios, comprueba si la URL devuelve un código de estado HTTP 200. * **Verificación simple**: Para la mayoría de los sitios, comprueba si la URL devuelve un código de estado HTTP 200.
* **Endpoints de Salud Específicos**: Para servicios como `vps_soul23` y `formbricks`, realiza peticiones a sus endpoints `/health` y analiza la respuesta JSON para un estado más detallado. * **Endpoints de Salud Específicos**: Para servicios como `vps_soul23` y `formbricks`, realiza peticiones a sus endpoints `/health` y analiza la respuesta JSON para un estado más detallado.
* **APIs de StatusPage**: Para servicios como OpenAI y Cloudflare, consulta su API de `statuspage.io` para obtener el estado oficial del servicio. * **APIs de StatusPage**: Para servicios como OpenAI y Cloudflare, consulta su API de `statuspage.io` para obtener el estado oficial del servicio.
* **MetaStatus**: Para Facebook, Instagram y WhatsApp consulta `https://metastatus.com/` y devuelve el estado oficial publicado por Meta, con fallback a un chequeo HTTP simple si la API no responde.
3. Consolida todos los resultados en un único objeto JSON. 3. Consolida todos los resultados en un único objeto JSON.
4. Si la variable de entorno `WEBHOOK_URLS` está definida (con una o más URLs separadas por comas), envía el resultado JSON a cada webhook. 4. Si la variable de entorno `WEBHOOK_URLS` está definida (con una o más URLs separadas por comas), envía el resultado JSON a cada webhook.
5. Imprime el resultado JSON en la salida estándar para que `server.js` pueda capturarlo. 5. Imprime el resultado JSON en la salida estándar para que `server.js` pueda capturarlo.

View File

@@ -3,6 +3,20 @@ const path = require("path");
const STATUSPAGE_SERVICES = new Set(["openai", "canva", "cloudflare"]); const STATUSPAGE_SERVICES = new Set(["openai", "canva", "cloudflare"]);
const DEFAULT_TIMEOUT_MS = 10_000; const DEFAULT_TIMEOUT_MS = 10_000;
const META_STATUS_ENDPOINTS = [
"https://metastatus.com/api/status",
"https://metastatus.com/api/statuses",
];
const META_STATUS_PAGE_URL = "https://metastatus.com/";
const META_APPS = new Map([
["facebook", "facebook"],
["instagram", "instagram"],
["whatsapp", "whatsapp"],
]);
const META_STATUS_CACHE_TTL_MS = 60_000;
const META_SLUGS = [...new Set(META_APPS.values())];
let metaStatusCache = { expiresAt: 0, map: null };
const getWebhookUrls = () => { const getWebhookUrls = () => {
const value = process.env.WEBHOOK_URLS || ""; const value = process.env.WEBHOOK_URLS || "";
@@ -70,6 +84,272 @@ const checkFormbricksHealth = async (url) => {
} }
}; };
const getNestedValue = (obj, path) => {
if (!obj || typeof obj !== "object") return undefined;
return path.split(".").reduce((acc, key) => {
if (acc === undefined || acc === null) return undefined;
return acc[key];
}, obj);
};
const pickFirstString = (source, paths) => {
for (const path of paths) {
const value = path ? getNestedValue(source, path) : source;
if (typeof value === "string" && value.trim()) {
return value.trim();
}
if (Array.isArray(value)) {
for (const entry of value) {
if (typeof entry === "string" && entry.trim()) {
return entry.trim();
}
if (entry && typeof entry === "object") {
for (const candidate of Object.values(entry)) {
if (typeof candidate === "string" && candidate.trim()) {
return candidate.trim();
}
}
}
}
}
if (value && typeof value === "object") {
for (const candidate of Object.values(value)) {
if (typeof candidate === "string" && candidate.trim()) {
return candidate.trim();
}
}
}
}
return "";
};
const detectMetaSlug = (value) => {
if (typeof value !== "string") return null;
const normalized = value.trim().toLowerCase();
if (!normalized) return null;
return META_SLUGS.find((slug) => normalized.includes(slug)) || null;
};
const determineMetaSeverity = (text = "") => {
const normalized = text.toLowerCase();
if (/(major|outage|down|unavailable|disruption|incident|critical|severe)/.test(normalized)) {
return "down";
}
if (
/(minor|partial|degrad|latenc|slow|investigating|issue|maintenance|notice|degraded)/.test(
normalized
)
) {
return "warn";
}
if (
normalized &&
/(healthy|operational|available|up|restored|resolved|normal|no issues|stable)/.test(normalized)
) {
return "ok";
}
return normalized ? "warn" : "warn";
};
const composeMetaStatusMessage = (severity, statusText, detailText) => {
const descriptorParts = [];
if (statusText) descriptorParts.push(statusText);
if (detailText && detailText !== statusText) descriptorParts.push(detailText);
const descriptor = descriptorParts.join(" - ") || "Sin detalles oficiales";
if (severity === "ok") return `🟢 OK (MetaStatus: ${descriptor})`;
if (severity === "down") return `🔴 Caído (MetaStatus: ${descriptor})`;
return `🟡 Advertencia (MetaStatus: ${descriptor})`;
};
const normalizeMetaStatusEntry = (entry) => {
const slugPaths = ["slug", "service.slug", "service", "platform", "product", "name", "title"];
let slug = null;
for (const path of slugPaths) {
const value = getNestedValue(entry, path);
const detected = detectMetaSlug(typeof value === "string" ? value : "");
if (detected) {
slug = detected;
break;
}
}
if (!slug) return null;
const statusText =
pickFirstString(entry, [
"status.description",
"status.title",
"status.state",
"status.status",
"status",
"status_text",
"statusDescription",
"status_description",
"indicator",
"state",
"current_status",
"currentStatus",
]) || "Sin información oficial";
const detailText =
pickFirstString(entry, [
"latest_update.title",
"latest_update.description",
"latestUpdate.title",
"latestUpdate.description",
"last_incident.title",
"lastIncident.title",
"incident.title",
"incident.description",
"message",
"subtitle",
"description",
"body",
]) || "";
const severity = determineMetaSeverity(`${statusText} ${detailText}`.trim());
const message = composeMetaStatusMessage(severity, statusText, detailText);
return { slug, severity, statusText, detailText, message };
};
const collectMetaStatusEntries = (payload) => {
if (!payload || typeof payload !== "object") return [];
const collected = [];
const seen = new Set();
const visit = (value) => {
if (!value || typeof value !== "object") return;
if (seen.has(value)) return;
seen.add(value);
const keys = Object.keys(value);
if (keys.some((key) => /status|incident|indicator|state/i.test(key))) {
const normalized = normalizeMetaStatusEntry(value);
if (normalized) {
collected.push(normalized);
}
}
for (const child of Object.values(value)) {
if (child && typeof child === "object") {
visit(child);
}
}
};
visit(payload);
return collected;
};
const buildMetaStatusMap = (payload) => {
const entries = collectMetaStatusEntries(payload);
if (!entries.length) return null;
const map = new Map();
for (const entry of entries) {
const current = map.get(entry.slug);
if (!current || entry.detailText.length > (current.detailText || "").length) {
map.set(entry.slug, entry);
}
}
return map;
};
const extractJsonFromHtml = (html) => {
if (typeof html !== "string") return null;
const inlineJsonPatterns = [
/window\.__APOLLO_STATE__\s*=\s*(\{.*?\});/s,
/window\.__NEXT_DATA__\s*=\s*(\{.*?\});/s,
/window\.__NUXT__\s*=\s*(\{.*?\});/s,
];
for (const pattern of inlineJsonPatterns) {
const match = html.match(pattern);
if (match) {
try {
return JSON.parse(match[1]);
} catch {
// ignorar y probar el siguiente patrón
}
}
}
const nextDataScript = html.match(/<script[^>]+id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
if (nextDataScript) {
try {
return JSON.parse(nextDataScript[1]);
} catch {
return null;
}
}
return null;
};
const fetchMetaStatusPayload = async () => {
const headers = {
"User-Agent": "HealthCheckMonitor/1.0",
Accept: "application/json,text/html;q=0.9,*/*;q=0.8",
};
for (const endpoint of META_STATUS_ENDPOINTS) {
try {
const response = await fetchWithTimeout(endpoint, { headers }, 8_000);
if (response.status === 200) {
const text = await response.text();
try {
return JSON.parse(text);
} catch {
// si el endpoint devuelve HTML accidentalmente, seguimos con el fallback
}
}
} catch {
// Intentaremos con el siguiente endpoint
}
}
try {
const response = await fetchWithTimeout(META_STATUS_PAGE_URL, { headers }, 8_000);
if (response.status === 200) {
const html = await response.text();
return extractJsonFromHtml(html);
}
} catch {
// sin conexión al sitio principal de Meta
}
return null;
};
const loadMetaStatusMap = async () => {
if (metaStatusCache.map && metaStatusCache.expiresAt > Date.now()) {
return metaStatusCache.map;
}
const payload = await fetchMetaStatusPayload();
const map = buildMetaStatusMap(payload);
if (map && map.size) {
metaStatusCache = { map, expiresAt: Date.now() + META_STATUS_CACHE_TTL_MS };
return map;
}
metaStatusCache = { map: null, expiresAt: Date.now() + 15_000 };
return null;
};
const getMetaStatusForApp = async (appKey) => {
const slug = META_APPS.get(appKey);
if (!slug) return null;
try {
const map = await loadMetaStatusMap();
if (!map) return null;
return map.get(slug)?.message || null;
} catch (error) {
console.error("MetaStatus: error al obtener estado oficial:", error.message);
return null;
}
};
const getStatusPageStatus = async (baseUrl) => { const getStatusPageStatus = async (baseUrl) => {
const url = `${baseUrl.replace(/\/$/, "")}/api/v2/summary.json`; const url = `${baseUrl.replace(/\/$/, "")}/api/v2/summary.json`;
try { try {
@@ -145,6 +425,16 @@ const buildSection = async (dictionary) => {
statusMessage = await checkFormbricksHealth(urlOrIp); statusMessage = await checkFormbricksHealth(urlOrIp);
output[`${name}_status`] = statusMessage; output[`${name}_status`] = statusMessage;
output[`${name}_state`] = statusMessage; output[`${name}_state`] = statusMessage;
} else if (META_APPS.has(name)) {
statusMessage = await getMetaStatusForApp(name);
if (statusMessage) {
output[`${name}_status`] = statusMessage;
output[`${name}_state`] = statusMessage;
} else {
const fallbackStatus = await checkUrl(urlOrIp);
output[`${name}_status`] = fallbackStatus;
output[`${name}_state`] = humanState(fallbackStatus);
}
} else { } else {
const statusCode = await checkUrl(urlOrIp); const statusCode = await checkUrl(urlOrIp);
output[`${name}_status`] = statusCode; output[`${name}_status`] = statusCode;