Merge branch 'master' into master

pull/4939/head
Frank Elsinga 4 months ago committed by GitHub
commit 1822b55846
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,16 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.string("snmp_oid").defaultTo(null);
table.enum("snmp_version", [ "1", "2c", "3" ]).defaultTo("2c");
table.string("json_path_operator").defaultTo(null);
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("snmp_oid");
table.dropColumn("snmp_version");
table.dropColumn("json_path_operator");
});
};

31
package-lock.json generated

@ -54,6 +54,7 @@
"mssql": "~11.0.0",
"mysql2": "~3.9.6",
"nanoid": "~3.3.4",
"net-snmp": "^3.11.2",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.9.13",
@ -5525,6 +5526,11 @@
"safer-buffer": "~2.1.0"
}
},
"node_modules/asn1-ber": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/asn1-ber/-/asn1-ber-1.2.2.tgz",
"integrity": "sha512-CbNem/7hxrjSiOAOOTX4iZxu+0m3jiLqlsERQwwPM1IDR/22M8IPpA1VVndCLw5KtjRYyRODbvAEIfuTogNDng=="
},
"node_modules/astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
@ -11284,6 +11290,31 @@
"node": ">= 0.6"
}
},
"node_modules/net-snmp": {
"version": "3.11.2",
"resolved": "https://registry.npmjs.org/net-snmp/-/net-snmp-3.11.2.tgz",
"integrity": "sha512-QKy2JQHIBsSK344dUxYRZv7tU0ANk8f8fzKD/Mmq/cCxm/cPbtiT7009QEgxdViW/gGjqGIOiLHxkCc+JhZltg==",
"dependencies": {
"asn1-ber": "^1.2.1",
"smart-buffer": "^4.1.0"
}
},
"node_modules/node-abi": {
"version": "3.62.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz",
"integrity": "sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-abort-controller": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="
},
"node_modules/node-cloudflared-tunnel": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/node-cloudflared-tunnel/-/node-cloudflared-tunnel-1.0.10.tgz",

@ -119,6 +119,7 @@
"mssql": "~11.0.0",
"mysql2": "~3.9.6",
"nanoid": "~3.3.4",
"net-snmp": "^3.11.2",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.9.13",

@ -2,7 +2,7 @@ const dayjs = require("dayjs");
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT
SQL_DATETIME_FORMAT, evaluateJsonQuery
} = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
@ -17,7 +17,6 @@ const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { DockerHost } = require("../docker");
const Gamedig = require("gamedig");
const jsonata = require("jsonata");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const { UptimeCalculator } = require("../uptime-calculator");
@ -161,6 +160,9 @@ class Monitor extends BeanModel {
kafkaProducerMessage: this.kafkaProducerMessage,
screenshot,
remote_browser: this.remote_browser,
snmpOid: this.snmpOid,
jsonPathOperator: this.jsonPathOperator,
snmpVersion: this.snmpVersion,
};
if (includeSensitiveData) {
@ -598,25 +600,15 @@ class Monitor extends BeanModel {
} else if (this.type === "json-query") {
let data = res.data;
// convert data to object
if (typeof data === "string" && res.headers["content-type"] !== "application/json") {
try {
data = JSON.parse(data);
} catch (_) {
// Failed to parse as JSON, just process it as a string
}
}
const { status, response } = await evaluateJsonQuery(data, this.jsonPath, this.jsonPathOperator, this.expectedValue);
let expression = jsonata(this.jsonPath);
let result = await expression.evaluate(data);
if (result.toString() === this.expectedValue) {
bean.msg += ", expected value is found";
if (status) {
bean.status = UP;
bean.msg = `JSON query passes (comparing ${response} ${this.jsonPathOperator} ${this.expectedValue})`;
} else {
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
throw new Error(`JSON query does not pass (comparing ${response} ${this.jsonPathOperator} ${this.expectedValue})`);
}
}
} else if (this.type === "port") {

@ -0,0 +1,63 @@
const { MonitorType } = require("./monitor-type");
const { UP, log, evaluateJsonQuery } = require("../../src/util");
const snmp = require("net-snmp");
class SNMPMonitorType extends MonitorType {
name = "snmp";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
let session;
try {
const sessionOptions = {
port: monitor.port || "161",
retries: monitor.maxretries,
timeout: monitor.timeout * 1000,
version: snmp.Version[monitor.snmpVersion],
};
session = snmp.createSession(monitor.hostname, monitor.radiusPassword, sessionOptions);
// Handle errors during session creation
session.on("error", (error) => {
throw new Error(`Error creating SNMP session: ${error.message}`);
});
const varbinds = await new Promise((resolve, reject) => {
session.get([ monitor.snmpOid ], (error, varbinds) => {
error ? reject(error) : resolve(varbinds);
});
});
log.debug("monitor", `SNMP: Received varbinds (Type: ${snmp.ObjectType[varbinds[0].type]} Value: ${varbinds[0].value})`);
if (varbinds.length === 0) {
throw new Error(`No varbinds returned from SNMP session (OID: ${monitor.snmpOid})`);
}
if (varbinds[0].type === snmp.ObjectType.NoSuchInstance) {
throw new Error(`The SNMP query returned that no instance exists for OID ${monitor.snmpOid}`);
}
// We restrict querying to one OID per monitor, therefore `varbinds[0]` will always contain the value we're interested in.
const value = varbinds[0].value;
const { status, response } = await evaluateJsonQuery(value, monitor.jsonPath, monitor.jsonPathOperator, monitor.expectedValue);
if (status) {
heartbeat.status = UP;
heartbeat.msg = `JSON query passes (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`;
} else {
throw new Error(`JSON query does not pass (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`);
}
} finally {
if (session) {
session.close();
}
}
}
}
module.exports = {
SNMPMonitorType,
};

@ -831,6 +831,10 @@ let needSetup = false;
monitor.kafkaProducerAllowAutoTopicCreation;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.remote_browser = monitor.remote_browser;
bean.snmpVersion = monitor.snmpVersion;
bean.snmpOid = monitor.snmpOid;
bean.jsonPathOperator = monitor.jsonPathOperator;
bean.timeout = monitor.timeout;
bean.validate();

@ -113,6 +113,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
// Allow all CORS origins (polling) in development
@ -517,4 +518,5 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor
const { TailscalePing } = require("./monitor-types/tailscale-ping");
const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");

@ -802,7 +802,6 @@
"twilioApiKey": "API ключ (по избор)",
"Expected Value": "Очаквана стойност",
"Json Query": "Заявка тип JSON",
"jsonQueryDescription": "Прави JSON заявка срещу отговора и проверява за очаквана стойност (Върнатата стойност ще бъде преобразувана в низ за сравнение). Разгледайте {0} за документация относно езика на заявката. Имате възможност да тествате {1}.",
"Badge Duration (in hours)": "Времетраене на баджа (в часове)",
"Badge Preview": "Преглед на баджа",
"Notify Channel": "Канал за известяване",

@ -823,7 +823,6 @@
"Enable Kafka Producer Auto Topic Creation": "Povolit Kafka zprostředkovateli automatické vytváření vláken",
"Kafka Producer Message": "Zpráva Kafka zprostředkovatele",
"tailscalePingWarning": "Abyste mohli používat Tailscale Ping monitor, je nutné Uptime Kuma nainstalovat mimo Docker, a dále na váš server nainstalovat Tailscale klienta.",
"jsonQueryDescription": "Proveďte JSON dotaz vůči odpovědi a zkontrolujte očekávaný výstup (za účelem porovnání bude návratová hodnota převedena na řetězec). Dokumentaci k dotazovacímu jazyku naleznete na {0}, a využít můžete též {1}.",
"Select": "Vybrat",
"selectedMonitorCount": "Vybráno: {0}",
"Check/Uncheck": "Vybrat/Zrušit výběr",

@ -812,7 +812,6 @@
"Json Query": "Json-Abfrage",
"filterActive": "Aktiv",
"filterActivePaused": "Pausiert",
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf {0} findest du die Dokumentation zur Abfragesprache. {1} kannst du Abfragen üben.",
"Badge Duration (in hours)": "Abzeichen Dauer (in Stunden)",
"Badge Preview": "Abzeichen Vorschau",
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",

@ -817,7 +817,6 @@
"filterActivePaused": "Pausiert",
"Expected Value": "Erwarteter Wert",
"Json Query": "Json-Abfrage",
"jsonQueryDescription": "Führe eine JSON-Abfrage gegen die Antwort durch und prüfe den erwarteten Wert (der Rückgabewert wird zum Vergleich in eine Zeichenkette umgewandelt). Auf {0} findest du die Dokumentation zur Abfragesprache. {1} kannst du Abfragen üben.",
"tailscalePingWarning": "Um den Tailscale Ping Monitor nutzen zu können, musst du Uptime Kuma ohne Docker installieren und den Tailscale Client auf dem Server installieren.",
"Server URL should not contain the nfty topic": "Die Server-URL sollte das nfty-Thema nicht enthalten",
"pushDeerServerDescription": "Leer lassen um den offiziellen Server zu verwenden",

@ -59,7 +59,7 @@
"Keyword": "Keyword",
"Invert Keyword": "Invert Keyword",
"Expected Value": "Expected Value",
"Json Query": "Json Query",
"Json Query Expression": "Json Query Expression",
"Friendly Name": "Friendly Name",
"URL": "URL",
"Hostname": "Hostname",
@ -588,7 +588,7 @@
"notificationDescription": "Notifications must be assigned to a monitor to function.",
"keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
"invertKeywordDescription": "Look for the keyword to be absent rather than present.",
"jsonQueryDescription": "Do a json Query against the response and check for expected value (Return value will get converted into string for comparison). Check out {0} for the documentation about the query language. A playground can be found {1}.",
"jsonQueryDescription": "Parse and extract specific data from the server's JSON response using JSON query or use \"$\" for the raw response, if not expecting JSON. The result is then compared to the expected value, as strings. See {0} for documentation and use {1} to experiment with queries.",
"backupDescription": "You can backup all monitors and notifications into a JSON file.",
"backupDescription2": "Note: history and event data is not included.",
"backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",
@ -943,6 +943,13 @@
"cellsyntSplitLongMessages": "Split long messages into up to 6 parts. 153 x 6 = 918 characters.",
"max 15 digits": "max 15 digits",
"max 11 alphanumeric characters": "max 11 alphanumeric characters",
"Community String": "Community String",
"snmpCommunityStringHelptext": "This string functions as a password to authenticate and control access to SNMP-enabled devices. Match it with your SNMP device's configuration.",
"OID (Object Identifier)": "OID (Object Identifier)",
"snmpOIDHelptext": "Enter the OID for the sensor or status you want to monitor. Use network management tools like MIB browsers or SNMP software if you're unsure about the OID.",
"Condition": "Condition",
"SNMP Version": "SNMP Version",
"Please enter a valid OID.": "Please enter a valid OID.",
"wayToGetThreemaGateway": "You can register for Threema Gateway {0}.",
"threemaRecipient": "Recipient",
"threemaRecipientType": "Recipient Type",

@ -771,7 +771,6 @@
"Json Query": "Consulta Json",
"invertKeywordDescription": "Comprobar si la palabra clave está ausente en vez de presente.",
"enableNSCD": "Habilitar NSCD (Demonio de Caché de Servicio de Nombres) para almacenar en caché todas las solicitudes DNS",
"jsonQueryDescription": "Realiza una consulta JSON contra la respuesta y verifica el valor esperado (el valor de retorno se convertirá a una cadena para la comparación). Consulta {0} para obtener documentación sobre el lenguaje de consulta. Puede encontrar un espacio de prueba {1}.",
"Request Timeout": "Tiempo de espera máximo de petición",
"timeoutAfter": "Expirar después de {0} segundos",
"chromeExecutableDescription": "Para usuarios de Docker, si Chromium no está instalado, puede que tarde unos minutos en ser instalado y mostrar el resultado de la prueba. Usa 1GB de espacio.",

@ -759,7 +759,6 @@
"filterActive": "فعال",
"webhookCustomBodyDesc": "یک بدنه HTTP سفارشی برای ریکوئست تعریف کنید. متغیر های قابل استفاده: {msg}, {heartbeat}, {monitor}.",
"tailscalePingWarning": "برای استفاده از Tailscale Ping monitor، شما باید آپتایم کوما را بدون استفاده از داکر و همچنین Tailscale client را نیز بر روی سرور خود نصب داشته باشید.",
"jsonQueryDescription": "یک کوئری json در برابر پاسخ انجام دهید و مقدار مورد انتظار را (مقدار برگشتی برای مقایسه به رشته تبدیل می شود). برای مستندات درباره زبان کوئری، {0} مشاهده کنید. همچنین محیط تست را میتوانید در {1} پیدا کنید.",
"Enter the list of brokers": "لیست بروکر هارا وارد کنید",
"Enable Kafka Producer Auto Topic Creation": "فعال سازی ایجاپ موضوع اتوماتیک تهیه کننده",
"Secret AccessKey": "کلید محرمانه AccessKey",

@ -792,7 +792,6 @@
"emailTemplateLimitedToUpDownNotification": "saatavilla vain YLÖS/ALAS sydämensykkeille, muulloin null",
"Your User ID": "Käyttäjätunnuksesi",
"invertKeywordDescription": "Etsi puuttuvaa avainsanaa.",
"jsonQueryDescription": "Suorita JSON-kysely vastaukselle ja tarkista odotettu arvo (Paluuarvo muutetaan merkkijonoksi vertailua varten). Katso kyselykielen ohjeita osoitteesta {0}. Leikkikenttä löytyy osoitteesta {1}.",
"Bark API Version": "Bark API-versio",
"Notify Channel": "Ilmoitus kanavalle",
"aboutNotifyChannel": "Ilmoitus kanavalle antaa työpöytä- tai mobiili-ilmoituksen kaikille kanavan jäsenille; riippumatta ovatko he paikalla vai poissa.",

@ -797,7 +797,6 @@
"twilioApiKey": "Clé API (facultatif)",
"Expected Value": "Valeur attendue",
"Json Query": "Requête Json",
"jsonQueryDescription": "Faites une requête json contre la réponse et vérifiez la valeur attendue (la valeur de retour sera convertie en chaîne pour comparaison). Consultez {0} pour la documentation sur le langage de requête. Une aire de jeux peut être trouvée {1}.",
"Badge Duration (in hours)": "Durée du badge (en heures)",
"Badge Preview": "Aperçu du badge",
"aboutNotifyChannel": "Notifier le canal déclenchera une notification de bureau ou mobile pour tous les membres du canal, que leur disponibilité soit active ou absente.",

@ -682,7 +682,6 @@
"confirmDisableTwoFAMsg": "An bhfuil tú cinnte gur mhaith leat 2FA a dhíchumasú?",
"affectedStatusPages": "Taispeáin an teachtaireacht cothabhála seo ar leathanaigh stádais roghnaithe",
"keywordDescription": "Cuardaigh eochairfhocal i ngnáthfhreagra HTML nó JSON. Tá an cuardach cás-íogair.",
"jsonQueryDescription": "Déan Iarratas json in aghaidh an fhreagra agus seiceáil an luach a bhfuiltear ag súil leis (Déanfar an luach fillte a thiontú ina theaghrán le haghaidh comparáide). Seiceáil {0} le haghaidh na gcáipéisí faoin teanga iarratais. Is féidir clós súgartha a aimsiú {1}.",
"backupDescription": "Is féidir leat gach monatóir agus fógra a chúltaca isteach i gcomhad JSON.",
"backupDescription2": "Nóta: níl sonraí staire agus imeachta san áireamh.",
"octopushAPIKey": "\"Eochair API\" ó dhintiúir API HTTP sa phainéal rialaithe",

@ -799,7 +799,6 @@
"affectedStatusPages": "Prikazuje poruku o održavanju na odabranim statusnim stranicama",
"atLeastOneMonitor": "Odaberite barem jedan zahvaćeni Monitor",
"invertKeywordDescription": "Postavi da ključna riječ mora biti odsutna umjesto prisutna.",
"jsonQueryDescription": "Izvršite JSON upit nad primljenim odgovorom i provjerite očekivanu povrtanu vrijednost. Ona će se za usporedbu pretvoriti u niz znakova (string). Pogledajte stranicu {0} za dokumentaciju o jeziku upita. Testno okruženje možete pronaći {1}.",
"Strategy": "Strategija",
"Free Mobile User Identifier": "Besplatni mobilni korisnički identifikator",
"Free Mobile API Key": "Besplatni mobilni ključ za API",

@ -797,7 +797,6 @@
"emailTemplateHeartbeatJSON": "a szívverést leíró objektum",
"emailTemplateMsg": "az értesítés üzenete",
"emailTemplateLimitedToUpDownNotification": "csak FEL/LE szívverés esetén érhető el, egyébként null érték",
"jsonQueryDescription": "Végezzen JSON-lekérdezést a válasz alapján, és ellenőrizze a várt értéket (a visszatérési értéket a rendszer karakterlánccá alakítja az összehasonlításhoz). Nézze meg a {0} webhelyet a lekérdezés paramétereivel kapcsolatos dokumentációért. A test környezet itt található: {1}.",
"pushoverMessageTtl": "TTL üzenet (másodperc)",
"Platform": "Platform",
"aboutNotifyChannel": "A Csatorna értesítése opció, értesítést fog küldeni a csatorna összes tagjának, függetlenül a tagok elérhetőségétől.",

@ -838,7 +838,6 @@
"emailTemplateHeartbeatJSON": "objek yang menggambarkan heartbeat",
"emailTemplateMsg": "pesan pemberitahuan",
"emailCustomBody": "Kustomisasi Body",
"jsonQueryDescription": "Lakukan Query json terhadap respons dan periksa nilai yang diharapkan (Nilai yang dikembalikan akan diubah menjadi string untuk perbandingan). Lihat {0} untuk dokumentasi tentang bahasa kueri. Taman bermain dapat ditemukan {1}.",
"Notify Channel": "Beritahu Saluran",
"Server URL should not contain the nfty topic": "URL server tidak boleh berisi topik nfty",
"PushDeer Server": "Server PushDeer",

@ -620,7 +620,6 @@
"enableNSCD": "Abilita NSCD (Name Service Cache Daemon) per abilitare la cache su tutte le richieste DNS",
"recurringIntervalMessage": "Esegui una volta al giorno | Esegui una volta ogni {0} giorni",
"affectedMonitorsDescription": "Seleziona i monitoraggi che sono influenzati da questa manutenzione",
"jsonQueryDescription": "Fai una query JSON verso la risposta e controlla se è presente il valore richiesto. (Il valore di ritorno verrà convertito in stringa ai fini della comparazione). Puoi controllare la documentazione su <a href='https://jsonata.org/'>jsonata.org</a> per conoscere come scrivere una query. Un area dimostrativa può essere trovata <a href='https://try.jsonata.org/'>qui</a>.",
"For safety, must use secret key": "Per sicurezza, devi usare una chiave segreta",
"Proxy server has authentication": "Il server Proxy ha una autenticazione",
"smseaglePriority": "Priorità messaggio (0-9, default = 0)",

@ -804,7 +804,6 @@
"Reconnecting...": "Opnieuw verbinden...",
"Expected Value": "Verwachte waarde",
"Json Query": "Json zoekopdracht",
"jsonQueryDescription": "Voer een JSON-query uit op de respons en controleer de verwachte waarde (De retourwaarde wordt omgezet naar een string voor vergelijking). Bekijk {0} voor de documentatie over de querytaal. Een speelplaats is beschikbaar {1}.",
"pushViewCode": "Hoe gebruik je Push monitor?(View Code)",
"setupDatabaseChooseDatabase": "Welke database wil je gebruiken?",
"setupDatabaseEmbeddedMariaDB": "Je hoeft niks in te stellen. Dit docker image heeft een ingebouwde en geconfigureerde MariaDB instantie. Uptime Kuma verbindt met deze database via een unix socket.",

@ -793,7 +793,6 @@
"styleElapsedTime": "Czas, który upłynął pod paskiem bicia serca",
"tailscalePingWarning": "Aby korzystać z monitora Tailscale Ping, należy zainstalować Uptime Kuma bez Dockera, a także zainstalować klienta Tailscale na serwerze.",
"invertKeywordDescription": "Słowo kluczowe powinno być raczej nieobecne niż obecne.",
"jsonQueryDescription": "Wykonaj zapytanie json względem odpowiedzi i sprawdź oczekiwaną wartość (wartość zwracana zostanie przekonwertowana na ciąg znaków do porównania). Sprawdź {0}, aby uzyskać dokumentację dotyczącą języka zapytań. Plac zabaw można znaleźć {1}.",
"Server URL should not contain the nfty topic": "Adres URL serwera nie powinien zawierać tematu nfty",
"Badge Duration (in hours)": "Czas trwania odznaki (w godzinach)",
"Enter the list of brokers": "Wprowadź listę brokerów",

@ -605,7 +605,6 @@
"wayToGetLineChannelToken": "Primeiro acesse o {0}, crie um provedor e um canal (API de Mensagens), então você pode obter o token de acesso do canal e o ID do usuário nos itens de menu mencionados acima.",
"aboutMattermostChannelName": "Você pode substituir o canal padrão para o qual o Webhook envia postagens, inserindo o nome do canal no campo \"Nome do Canal\". Isso precisa ser habilitado nas configurações do Webhook do Mattermost. Por exemplo: #outro-canal",
"invertKeywordDescription": "Procure pela palavra-chave estar ausente em vez de presente.",
"jsonQueryDescription": "Faça uma consulta JSON na resposta e verifique o valor esperado (o valor de retorno será convertido em uma string para comparação). Confira {0} para a documentação sobre a linguagem de consulta. Você pode encontrar um playground {1}.",
"octopushTypePremium": "Premium (Rápido - recomendado para alertas)",
"octopushTypeLowCost": "Baixo Custo (Lento - às vezes bloqueado pelo operador)",
"octopushSMSSender": "Nome do Remetente de SMS: 3-11 caracteres alfanuméricos e espaço (a-zA-Z0-9)",

@ -689,7 +689,6 @@
"emailTemplateLimitedToUpDownNotification": "disponibil numai pentru heartbeat-uri UP/DOWN, altfel nul",
"emailTemplateStatus": "Stare",
"invertKeywordDescription": "Căutați după cuvântul cheie să fie absent și nu prezent.",
"jsonQueryDescription": "Efectuați o interogare json după răspuns și verificați valoarea așteptată (valoarea returnată va fi convertită în șir pentru comparație). Consultați {0} pentru documentația despre limbajul de interogare. Un playground poate fi găsit {1}.",
"goAlertInfo": "GoAlert este o aplicație open source pentru programarea apelurilor, escalări automate și notificări (cum ar fi SMS-uri sau apeluri vocale). Angajați automat persoana potrivită, în modul potrivit și la momentul potrivit! {0}",
"goAlertIntegrationKeyInfo": "Obțineți cheia generică de integrare API pentru serviciu în formatul \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" de obicei valoarea parametrului token al URL-ului copiat.",
"SecretAccessKey": "Secret AccessKey",

@ -812,7 +812,6 @@
"AccessKey Id": "AccessKey Id",
"Secret AccessKey": "Секретный ключ доступа",
"Session Token": "Токен сессии",
"jsonQueryDescription": "Выполните json-запрос к ответу и проверьте наличие ожидаемого значения (возвращаемое значение будет преобразовано в строку для сравнения). Посмотрите {0} для получения документации по языку запросов. A Потренироваться вы можете {1}.",
"Notify Channel": "Канал оповещений",
"aboutNotifyChannel": "Уведомление о канале вызовет настольное или мобильное уведомление для всех участников канала, независимо от того, установлена ли их доступность как активная или отсутствующая.",
"Enter the list of brokers": "Введите список брокеров",

@ -795,7 +795,6 @@
"pushoverDesc1": "Nödprioritet (2) har 30 sekunders timeout mellan försök och löper ut efter 1 timme som standard.",
"octopushTypePremium": "Premium (Snabb - rekommenderas för varningar)",
"octopushTypeLowCost": "Låg kostnad (långsam - blockeras ibland av operatören)",
"jsonQueryDescription": "Gör en json-förfrågan mot svaret och kontrollera det förväntade värdet (returvärde konverteras till en sträng för jämförelse). Se {0} för dokumentation angående frågespråket. En lekplats kan hittas här {1}.",
"Check octopush prices": "Kontrollera octopush priser {0}.",
"octopushSMSSender": "SMS avsändarnamn: 3-11 alfanumeriska tecken och mellanslag (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea enhetsid",

@ -794,7 +794,6 @@
"webhookBodyPresetOption": "Ön ayar - {0}",
"webhookBodyCustomOption": "Özel Gövde",
"Request Body": "İstek Gövdesi",
"jsonQueryDescription": "Yanıta karşı bir json sorgusu yapın ve beklenen değeri kontrol edin (Dönüş değeri, karşılaştırma için dizgeye dönüştürülür). Sorgu diliyle ilgili belgeler için {0}'a bakın. Bir oyun alanı {1} bulunabilir.",
"twilioApiKey": "Api Anahtarı (isteğe bağlı)",
"Expected Value": "Beklenen Değer",
"Json Query": "Json Sorgusu",

@ -802,7 +802,6 @@
"Request Body": "Тіло запиту",
"Badge Preview": "Попередній перегляд бейджа",
"Badge Duration (in hours)": "Тривалість бейджа (у годинах)",
"jsonQueryDescription": "Виконувати json-запит до відповіді та перевірити очікуване значення (значення, що повертається, буде перетворено в рядок для порівняння). Зверніться до {0} щоб ознайомитися з документацією про мову запитів. Навчальний майданчик можна знайти {1}.",
"twilioApiKey": "Api ключ (необов'язково)",
"Expected Value": "Очікуване значення",
"Json Query": "Json-запит",

@ -802,7 +802,6 @@
"webhookCustomBodyDesc": "为 webhook 设定一个自定义 HTTP 请求体。可在模板内使用 {msg}、{heartbeat}和{monitor} 变量。",
"webhookBodyPresetOption": "预设 - {0}",
"Request Body": "请求体",
"jsonQueryDescription": "对响应结果执行一次 JSON 查询,其返回值将会被转换为字符串,再与期望值进行比较。可访问 {0} 阅读 JSON 查询语言的文档,或在{1}测试查询语句。",
"Json Query": "JSON 查询",
"twilioApiKey": "API Key可选",
"Expected Value": "预期值",

@ -772,7 +772,6 @@
"Check/Uncheck": "選中/取消選中",
"tailscalePingWarning": "如需使用 Tailscale Ping 客戶端,您需要以非 docker 方式安裝 Uptime Kuma並同時安裝 Tailscale 客戶端。",
"invertKeywordDescription": "出現關鍵詞將令檢測結果設為失敗,而非成功。",
"jsonQueryDescription": "對回應結果執行一次 JSON 查詢,其返回值將會被轉換為字串,再與期望值進行比較。可造訪{0}閱讀JSON 查詢語言的文件,或在{1}測試查詢語句。",
"wayToGetKookGuildID": "在 Kook 設定中打開“開發者模式”,然後右鍵點選頻道可取得其 ID",
"Notify Channel": "通知該頻道",
"aboutNotifyChannel": "勾選“通知該頻道”,會令該頻道內所有成員都收到一條桌面端或移動端通知,無論其狀態是在線或離開。",

@ -16,7 +16,7 @@
</div>
</div>
<p class="url">
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'mp-health' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a>
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'mp-health' || monitor.type === 'real-browser' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a>
<span v-if="monitor.type === 'port'">TCP Port {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'">

@ -24,6 +24,9 @@
<option value="ping">
Ping
</option>
<option value="snmp">
SNMP
</option>
<option value="keyword">
HTTP(s) - {{ $t("Keyword") }}
</option>
@ -168,21 +171,6 @@
</div>
</div>
<!-- Json Query -->
<div v-if="monitor.type === 'json-query'" class="my-3">
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required>
<i18n-t tag="div" class="form-text" keypath="jsonQueryDescription">
<a href="https://jsonata.org/">jsonata.org</a>
<a href="https://try.jsonata.org/">{{ $t('here') }}</a>
</i18n-t>
<br>
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
<input id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
</div>
<!-- Game -->
<!-- GameDig only -->
<div v-if="monitor.type === 'gamedig'" class="my-3">
@ -246,19 +234,79 @@
</template>
<!-- Hostname -->
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' ||monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping'" class="my-3">
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'snmp'" class="my-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`" required>
</div>
<!-- Port -->
<!-- For TCP Port / Steam / MQTT / Radius Type -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
<!-- For TCP Port / Steam / MQTT / Radius Type / SNMP -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'snmp'" class="my-3">
<label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div>
<!-- SNMP Monitor Type -->
<div v-if="monitor.type === 'snmp'" class="my-3">
<label for="snmp_community_string" class="form-label">{{ $t("Community String") }}</label>
<!-- TODO: Rename monitor.radiusPassword to monitor.password for general use -->
<HiddenInput id="snmp_community_string" v-model="monitor.radiusPassword" autocomplete="false" required="true" placeholder="public"></HiddenInput>
<div class="form-text">{{ $t('snmpCommunityStringHelptext') }}</div>
</div>
<div v-if="monitor.type === 'snmp'" class="my-3">
<label for="snmp_oid" class="form-label">{{ $t("OID (Object Identifier)") }}</label>
<input id="snmp_oid" v-model="monitor.snmpOid" :title="$t('Please enter a valid OID.') + ' ' + $t('Example:', ['1.3.6.1.4.1.9.6.1.101'])" type="text" class="form-control" pattern="^([0-2])((\.0)|(\.[1-9][0-9]*))*$" placeholder="1.3.6.1.4.1.9.6.1.101" required>
<div class="form-text">{{ $t('snmpOIDHelptext') }} </div>
</div>
<div v-if="monitor.type === 'snmp'" class="my-3">
<label for="snmp_version" class="form-label">{{ $t("SNMP Version") }}</label>
<select id="snmp_version" v-model="monitor.snmpVersion" class="form-select">
<option value="1">
SNMPv1
</option>
<option value="2c">
SNMPv2c
</option>
</select>
</div>
<!-- Json Query -->
<!-- For Json Query / SNMP -->
<div v-if="monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
<div class="my-2">
<label for="jsonPath" class="form-label mb-0">{{ $t("Json Query Expression") }}</label>
<i18n-t tag="div" class="form-text mb-2" keypath="jsonQueryDescription">
<a href="https://jsonata.org/">jsonata.org</a>
<a href="https://try.jsonata.org/">{{ $t('playground') }}</a>
</i18n-t>
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" placeholder="$" required>
</div>
<div class="d-flex align-items-start">
<div class="me-2">
<label for="json_path_operator" class="form-label">{{ $t("Condition") }}</label>
<select id="json_path_operator" v-model="monitor.jsonPathOperator" class="form-select me-3" required>
<option value=">">&gt;</option>
<option value=">=">&gt;=</option>
<option value="<">&lt;</option>
<option value="<=">&lt;=</option>
<option value="!=">&#33;=</option>
<option value="==">==</option>
<option value="contains">contains</option>
</select>
</div>
<div class="flex-grow-1">
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
<input v-if="monitor.jsonPathOperator !== 'contains' && monitor.jsonPathOperator !== '==' && monitor.jsonPathOperator !== '!='" id="expectedValue" v-model="monitor.expectedValue" type="number" class="form-control" required step=".01">
<input v-else id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
</div>
</div>
</div>
<!-- DNS Resolver Server -->
<!-- For DNS Type -->
<template v-if="monitor.type === 'dns'">
@ -483,8 +531,8 @@
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required :min="minInterval" step="1">
</div>
<!-- Timeout: HTTP / Keyword only -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query'" class="my-3">
<!-- Timeout: HTTP / Keyword / SNMP only -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
</div>
@ -946,7 +994,6 @@ const monitorDefaults = {
retryInterval: 60,
resendInterval: 0,
maxretries: 0,
timeout: 48,
notificationIDList: {},
ignoreTls: false,
upsideDown: false,
@ -1158,8 +1205,8 @@ message HealthCheckResponse {
// Only groups, not itself, not a decendant
result = result.filter(
monitor => monitor.type === "group" &&
monitor.id !== this.monitor.id &&
!this.monitor.childrenIDs?.includes(monitor.id)
monitor.id !== this.monitor.id &&
!this.monitor.childrenIDs?.includes(monitor.id)
);
// Filter result by active state, weight and alphabetical
@ -1291,11 +1338,35 @@ message HealthCheckResponse {
this.monitor.port = "53";
} else if (this.monitor.type === "radius") {
this.monitor.port = "1812";
} else if (this.monitor.type === "snmp") {
this.monitor.port = "161";
} else {
this.monitor.port = undefined;
}
}
if (this.monitor.type === "snmp") {
// snmp is not expected to be executed via the internet => we can choose a lower default timeout
this.monitor.timeout = 5;
} else {
this.monitor.timeout = 48;
}
// Set default SNMP version
if (!this.monitor.snmpVersion) {
this.monitor.snmpVersion = "2c";
}
// Set default jsonPath
if (!this.monitor.jsonPath) {
this.monitor.jsonPath = "$";
}
// Set default condition for for jsonPathOperator
if (!this.monitor.jsonPathOperator) {
this.monitor.jsonPathOperator = "==";
}
// Get the game list from server
if (this.monitor.type === "gamedig") {
this.$root.getSocket().emit("getGameList", (res) => {

@ -14,8 +14,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = void 0;
exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = void 0;
const dayjs_1 = __importDefault(require("dayjs"));
const dayjs = require("dayjs");
const jsonata = require("jsonata");
exports.isDev = process.env.NODE_ENV === "development";
exports.isNode = typeof process !== "undefined" && ((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node);
exports.appName = "Uptime Kuma";
@ -399,3 +402,59 @@ function intHash(str, length = 10) {
return (hash % length + length) % length;
}
exports.intHash = intHash;
async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue) {
let response;
try {
response = JSON.parse(data);
}
catch (_a) {
response = (typeof data === "object" || typeof data === "number") && !Buffer.isBuffer(data) ? data : data.toString();
}
try {
response = (jsonPath) ? await jsonata(jsonPath).evaluate(response) : response;
if (response === null || response === undefined) {
throw new Error("Empty or undefined response. Check query syntax and response structure");
}
if (typeof response === "object" || response instanceof Date || typeof response === "function") {
throw new Error(`The post-JSON query evaluated response from the server is of type ${typeof response}, which cannot be directly compared to the expected value`);
}
let jsonQueryExpression;
switch (jsonPathOperator) {
case ">":
case ">=":
case "<":
case "<=":
jsonQueryExpression = `$number($.value) ${jsonPathOperator} $number($.expected)`;
break;
case "!=":
jsonQueryExpression = "$.value != $.expected";
break;
case "==":
jsonQueryExpression = "$.value = $.expected";
break;
case "contains":
jsonQueryExpression = "$contains($.value, $.expected)";
break;
default:
throw new Error(`Invalid condition ${jsonPathOperator}`);
}
const expression = jsonata(jsonQueryExpression);
const status = await expression.evaluate({
value: response.toString(),
expected: expectedValue.toString()
});
if (status === undefined) {
throw new Error("Query evaluation returned undefined. Check query syntax and the structure of the response data");
}
return {
status,
response
};
}
catch (err) {
response = JSON.stringify(response);
response = (response && response.length > 50) ? `${response.substring(0, 100)}… (truncated)` : response;
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
}
}
exports.evaluateJsonQuery = evaluateJsonQuery;

@ -17,6 +17,8 @@ import * as timezone from "dayjs/plugin/timezone";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as utc from "dayjs/plugin/utc";
import * as jsonata from "jsonata";
export const isDev = process.env.NODE_ENV === "development";
export const isNode = typeof process !== "undefined" && process?.versions?.node;
export const appName = "Uptime Kuma";
@ -643,3 +645,76 @@ export function intHash(str : string, length = 10) : number {
return (hash % length + length) % length; // Ensure the result is non-negative
}
/**
* Evaluate a JSON query expression against the provided data.
* @param data The data to evaluate the JSON query against.
* @param jsonPath The JSON path or custom JSON query expression.
* @param jsonPathOperator The operator to use for comparison.
* @param expectedValue The expected value to compare against.
* @returns An object containing the status and the evaluation result.
* @throws Error if the evaluation returns undefined.
*/
export async function evaluateJsonQuery(data: any, jsonPath: string, jsonPathOperator: string, expectedValue: any): Promise<{ status: boolean; response: any }> {
// Attempt to parse data as JSON; if unsuccessful, handle based on data type.
let response: any;
try {
response = JSON.parse(data);
} catch {
response = (typeof data === "object" || typeof data === "number") && !Buffer.isBuffer(data) ? data : data.toString();
}
try {
// If a JSON path is provided, pre-evaluate the data using it.
response = (jsonPath) ? await jsonata(jsonPath).evaluate(response) : response;
if (response === null || response === undefined) {
throw new Error("Empty or undefined response. Check query syntax and response structure");
}
if (typeof response === "object" || response instanceof Date || typeof response === "function") {
throw new Error(`The post-JSON query evaluated response from the server is of type ${typeof response}, which cannot be directly compared to the expected value`);
}
// Perform the comparison logic using the chosen operator
let jsonQueryExpression;
switch (jsonPathOperator) {
case ">":
case ">=":
case "<":
case "<=":
jsonQueryExpression = `$number($.value) ${jsonPathOperator} $number($.expected)`;
break;
case "!=":
jsonQueryExpression = "$.value != $.expected";
break;
case "==":
jsonQueryExpression = "$.value = $.expected";
break;
case "contains":
jsonQueryExpression = "$contains($.value, $.expected)";
break;
default:
throw new Error(`Invalid condition ${jsonPathOperator}`);
}
// Evaluate the JSON Query Expression
const expression = jsonata(jsonQueryExpression);
const status = await expression.evaluate({
value: response.toString(),
expected: expectedValue.toString()
});
if (status === undefined) {
throw new Error("Query evaluation returned undefined. Check query syntax and the structure of the response data");
}
return {
status, // The evaluation of the json query
response // The response from the server or result from initial json-query evaluation
};
} catch (err: any) {
response = JSON.stringify(response); // Ensure the response is treated as a string for the console
response = (response && response.length > 50) ? `${response.substring(0, 100)}… (truncated)` : response;// Truncate long responses to the console
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
}
}

Loading…
Cancel
Save