mirror of
https://github.com/marcogll/soul23_placeholder_site_server.git
synced 2026-01-13 13:25:18 +00:00
ajutes meta sites
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user