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.
* **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.
* **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.
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.

View File

@@ -3,6 +3,20 @@ const path = require("path");
const STATUSPAGE_SERVICES = new Set(["openai", "canva", "cloudflare"]);
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 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 url = `${baseUrl.replace(/\/$/, "")}/api/v2/summary.json`;
try {
@@ -145,6 +425,16 @@ const buildSection = async (dictionary) => {
statusMessage = await checkFormbricksHealth(urlOrIp);
output[`${name}_status`] = 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 {
const statusCode = await checkUrl(urlOrIp);
output[`${name}_status`] = statusCode;