# Conflicts: # src/lang/pl.json # src/lang/uk-UA.jsonfix-weblate-conflict
commit
362a890bc3
@ -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");
|
||||
});
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.boolean("cache_bust").notNullable().defaultTo(false);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.dropColumn("cache_bust");
|
||||
});
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.text("conditions").notNullable().defaultTo("[]");
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.dropColumn("conditions");
|
||||
});
|
||||
};
|
@ -0,0 +1,71 @@
|
||||
const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("./expression");
|
||||
const { operatorMap } = require("./operators");
|
||||
|
||||
/**
|
||||
* @param {ConditionExpression} expression Expression to evaluate
|
||||
* @param {object} context Context to evaluate against; These are values for variables in the expression
|
||||
* @returns {boolean} Whether the expression evaluates true or false
|
||||
* @throws {Error}
|
||||
*/
|
||||
function evaluateExpression(expression, context) {
|
||||
/**
|
||||
* @type {import("./operators").ConditionOperator|null}
|
||||
*/
|
||||
const operator = operatorMap.get(expression.operator) || null;
|
||||
if (operator === null) {
|
||||
throw new Error("Unexpected expression operator ID '" + expression.operator + "'. Expected one of [" + operatorMap.keys().join(",") + "]");
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(context, expression.variable)) {
|
||||
throw new Error("Variable missing in context: " + expression.variable);
|
||||
}
|
||||
|
||||
return operator.test(context[expression.variable], expression.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ConditionExpressionGroup} group Group of expressions to evaluate
|
||||
* @param {object} context Context to evaluate against; These are values for variables in the expression
|
||||
* @returns {boolean} Whether the group evaluates true or false
|
||||
* @throws {Error}
|
||||
*/
|
||||
function evaluateExpressionGroup(group, context) {
|
||||
if (!group.children.length) {
|
||||
throw new Error("ConditionExpressionGroup must contain at least one child.");
|
||||
}
|
||||
|
||||
let result = null;
|
||||
|
||||
for (const child of group.children) {
|
||||
let childResult;
|
||||
|
||||
if (child instanceof ConditionExpression) {
|
||||
childResult = evaluateExpression(child, context);
|
||||
} else if (child instanceof ConditionExpressionGroup) {
|
||||
childResult = evaluateExpressionGroup(child, context);
|
||||
} else {
|
||||
throw new Error("Invalid child type in ConditionExpressionGroup. Expected ConditionExpression or ConditionExpressionGroup");
|
||||
}
|
||||
|
||||
if (result === null) {
|
||||
result = childResult; // Initialize result with the first child's result
|
||||
} else if (child.andOr === LOGICAL.OR) {
|
||||
result = result || childResult;
|
||||
} else if (child.andOr === LOGICAL.AND) {
|
||||
result = result && childResult;
|
||||
} else {
|
||||
throw new Error("Invalid logical operator in child of ConditionExpressionGroup. Expected 'and' or 'or'. Got '" + group.andOr + "'");
|
||||
}
|
||||
}
|
||||
|
||||
if (result === null) {
|
||||
throw new Error("ConditionExpressionGroup did not result in a boolean.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
evaluateExpression,
|
||||
evaluateExpressionGroup,
|
||||
};
|
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
const LOGICAL = {
|
||||
AND: "and",
|
||||
OR: "or",
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively processes an array of raw condition objects and populates the given parent group with
|
||||
* corresponding ConditionExpression or ConditionExpressionGroup instances.
|
||||
* @param {Array} conditions Array of raw condition objects, where each object represents either a group or an expression.
|
||||
* @param {ConditionExpressionGroup} parentGroup The parent group to which the instantiated ConditionExpression or ConditionExpressionGroup objects will be added.
|
||||
* @returns {void}
|
||||
*/
|
||||
function processMonitorConditions(conditions, parentGroup) {
|
||||
conditions.forEach(condition => {
|
||||
const andOr = condition.andOr === LOGICAL.OR ? LOGICAL.OR : LOGICAL.AND;
|
||||
|
||||
if (condition.type === "group") {
|
||||
const group = new ConditionExpressionGroup([], andOr);
|
||||
|
||||
// Recursively process the group's children
|
||||
processMonitorConditions(condition.children, group);
|
||||
|
||||
parentGroup.children.push(group);
|
||||
} else if (condition.type === "expression") {
|
||||
const expression = new ConditionExpression(condition.variable, condition.operator, condition.value, andOr);
|
||||
parentGroup.children.push(expression);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class ConditionExpressionGroup {
|
||||
/**
|
||||
* @type {ConditionExpressionGroup[]|ConditionExpression[]} Groups and/or expressions to test
|
||||
*/
|
||||
children = [];
|
||||
|
||||
/**
|
||||
* @type {LOGICAL} Connects group result with previous group/expression results
|
||||
*/
|
||||
andOr;
|
||||
|
||||
/**
|
||||
* @param {ConditionExpressionGroup[]|ConditionExpression[]} children Groups and/or expressions to test
|
||||
* @param {LOGICAL} andOr Connects group result with previous group/expression results
|
||||
*/
|
||||
constructor(children = [], andOr = LOGICAL.AND) {
|
||||
this.children = children;
|
||||
this.andOr = andOr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Monitor} monitor Monitor instance
|
||||
* @returns {ConditionExpressionGroup|null} A ConditionExpressionGroup with the Monitor's conditions
|
||||
*/
|
||||
static fromMonitor(monitor) {
|
||||
const conditions = JSON.parse(monitor.conditions);
|
||||
if (conditions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const root = new ConditionExpressionGroup();
|
||||
processMonitorConditions(conditions, root);
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
class ConditionExpression {
|
||||
/**
|
||||
* @type {string} ID of variable
|
||||
*/
|
||||
variable;
|
||||
|
||||
/**
|
||||
* @type {string} ID of operator
|
||||
*/
|
||||
operator;
|
||||
|
||||
/**
|
||||
* @type {string} Value to test with the operator
|
||||
*/
|
||||
value;
|
||||
|
||||
/**
|
||||
* @type {LOGICAL} Connects expression result with previous group/expression results
|
||||
*/
|
||||
andOr;
|
||||
|
||||
/**
|
||||
* @param {string} variable ID of variable to test against
|
||||
* @param {string} operator ID of operator to test the variable with
|
||||
* @param {string} value Value to test with the operator
|
||||
* @param {LOGICAL} andOr Connects expression result with previous group/expression results
|
||||
*/
|
||||
constructor(variable, operator, value, andOr = LOGICAL.AND) {
|
||||
this.variable = variable;
|
||||
this.operator = operator;
|
||||
this.value = value;
|
||||
this.andOr = andOr;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LOGICAL,
|
||||
ConditionExpressionGroup,
|
||||
ConditionExpression,
|
||||
};
|
@ -0,0 +1,318 @@
|
||||
class ConditionOperator {
|
||||
id = undefined;
|
||||
caption = undefined;
|
||||
|
||||
/**
|
||||
* @type {mixed} variable
|
||||
* @type {mixed} value
|
||||
*/
|
||||
test(variable, value) {
|
||||
throw new Error("You need to override test()");
|
||||
}
|
||||
}
|
||||
|
||||
const OP_STR_EQUALS = "equals";
|
||||
|
||||
const OP_STR_NOT_EQUALS = "not_equals";
|
||||
|
||||
const OP_CONTAINS = "contains";
|
||||
|
||||
const OP_NOT_CONTAINS = "not_contains";
|
||||
|
||||
const OP_STARTS_WITH = "starts_with";
|
||||
|
||||
const OP_NOT_STARTS_WITH = "not_starts_with";
|
||||
|
||||
const OP_ENDS_WITH = "ends_with";
|
||||
|
||||
const OP_NOT_ENDS_WITH = "not_ends_with";
|
||||
|
||||
const OP_NUM_EQUALS = "num_equals";
|
||||
|
||||
const OP_NUM_NOT_EQUALS = "num_not_equals";
|
||||
|
||||
const OP_LT = "lt";
|
||||
|
||||
const OP_GT = "gt";
|
||||
|
||||
const OP_LTE = "lte";
|
||||
|
||||
const OP_GTE = "gte";
|
||||
|
||||
/**
|
||||
* Asserts a variable is equal to a value.
|
||||
*/
|
||||
class StringEqualsOperator extends ConditionOperator {
|
||||
id = OP_STR_EQUALS;
|
||||
caption = "equals";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable === value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable is not equal to a value.
|
||||
*/
|
||||
class StringNotEqualsOperator extends ConditionOperator {
|
||||
id = OP_STR_NOT_EQUALS;
|
||||
caption = "not equals";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable !== value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable contains a value.
|
||||
* Handles both Array and String variable types.
|
||||
*/
|
||||
class ContainsOperator extends ConditionOperator {
|
||||
id = OP_CONTAINS;
|
||||
caption = "contains";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
if (Array.isArray(variable)) {
|
||||
return variable.includes(value);
|
||||
}
|
||||
|
||||
return variable.indexOf(value) !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable does not contain a value.
|
||||
* Handles both Array and String variable types.
|
||||
*/
|
||||
class NotContainsOperator extends ConditionOperator {
|
||||
id = OP_NOT_CONTAINS;
|
||||
caption = "not contains";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
if (Array.isArray(variable)) {
|
||||
return !variable.includes(value);
|
||||
}
|
||||
|
||||
return variable.indexOf(value) === -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable starts with a value.
|
||||
*/
|
||||
class StartsWithOperator extends ConditionOperator {
|
||||
id = OP_STARTS_WITH;
|
||||
caption = "starts with";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable.startsWith(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable does not start with a value.
|
||||
*/
|
||||
class NotStartsWithOperator extends ConditionOperator {
|
||||
id = OP_NOT_STARTS_WITH;
|
||||
caption = "not starts with";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return !variable.startsWith(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable ends with a value.
|
||||
*/
|
||||
class EndsWithOperator extends ConditionOperator {
|
||||
id = OP_ENDS_WITH;
|
||||
caption = "ends with";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable.endsWith(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable does not end with a value.
|
||||
*/
|
||||
class NotEndsWithOperator extends ConditionOperator {
|
||||
id = OP_NOT_ENDS_WITH;
|
||||
caption = "not ends with";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return !variable.endsWith(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a numeric variable is equal to a value.
|
||||
*/
|
||||
class NumberEqualsOperator extends ConditionOperator {
|
||||
id = OP_NUM_EQUALS;
|
||||
caption = "equals";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable === Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a numeric variable is not equal to a value.
|
||||
*/
|
||||
class NumberNotEqualsOperator extends ConditionOperator {
|
||||
id = OP_NUM_NOT_EQUALS;
|
||||
caption = "not equals";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable !== Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable is less than a value.
|
||||
*/
|
||||
class LessThanOperator extends ConditionOperator {
|
||||
id = OP_LT;
|
||||
caption = "less than";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable < Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable is greater than a value.
|
||||
*/
|
||||
class GreaterThanOperator extends ConditionOperator {
|
||||
id = OP_GT;
|
||||
caption = "greater than";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable > Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable is less than or equal to a value.
|
||||
*/
|
||||
class LessThanOrEqualToOperator extends ConditionOperator {
|
||||
id = OP_LTE;
|
||||
caption = "less than or equal to";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable <= Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable is greater than or equal to a value.
|
||||
*/
|
||||
class GreaterThanOrEqualToOperator extends ConditionOperator {
|
||||
id = OP_GTE;
|
||||
caption = "greater than or equal to";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable >= Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
const operatorMap = new Map([
|
||||
[ OP_STR_EQUALS, new StringEqualsOperator ],
|
||||
[ OP_STR_NOT_EQUALS, new StringNotEqualsOperator ],
|
||||
[ OP_CONTAINS, new ContainsOperator ],
|
||||
[ OP_NOT_CONTAINS, new NotContainsOperator ],
|
||||
[ OP_STARTS_WITH, new StartsWithOperator ],
|
||||
[ OP_NOT_STARTS_WITH, new NotStartsWithOperator ],
|
||||
[ OP_ENDS_WITH, new EndsWithOperator ],
|
||||
[ OP_NOT_ENDS_WITH, new NotEndsWithOperator ],
|
||||
[ OP_NUM_EQUALS, new NumberEqualsOperator ],
|
||||
[ OP_NUM_NOT_EQUALS, new NumberNotEqualsOperator ],
|
||||
[ OP_LT, new LessThanOperator ],
|
||||
[ OP_GT, new GreaterThanOperator ],
|
||||
[ OP_LTE, new LessThanOrEqualToOperator ],
|
||||
[ OP_GTE, new GreaterThanOrEqualToOperator ],
|
||||
]);
|
||||
|
||||
const defaultStringOperators = [
|
||||
operatorMap.get(OP_STR_EQUALS),
|
||||
operatorMap.get(OP_STR_NOT_EQUALS),
|
||||
operatorMap.get(OP_CONTAINS),
|
||||
operatorMap.get(OP_NOT_CONTAINS),
|
||||
operatorMap.get(OP_STARTS_WITH),
|
||||
operatorMap.get(OP_NOT_STARTS_WITH),
|
||||
operatorMap.get(OP_ENDS_WITH),
|
||||
operatorMap.get(OP_NOT_ENDS_WITH)
|
||||
];
|
||||
|
||||
const defaultNumberOperators = [
|
||||
operatorMap.get(OP_NUM_EQUALS),
|
||||
operatorMap.get(OP_NUM_NOT_EQUALS),
|
||||
operatorMap.get(OP_LT),
|
||||
operatorMap.get(OP_GT),
|
||||
operatorMap.get(OP_LTE),
|
||||
operatorMap.get(OP_GTE)
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
OP_STR_EQUALS,
|
||||
OP_STR_NOT_EQUALS,
|
||||
OP_CONTAINS,
|
||||
OP_NOT_CONTAINS,
|
||||
OP_STARTS_WITH,
|
||||
OP_NOT_STARTS_WITH,
|
||||
OP_ENDS_WITH,
|
||||
OP_NOT_ENDS_WITH,
|
||||
OP_NUM_EQUALS,
|
||||
OP_NUM_NOT_EQUALS,
|
||||
OP_LT,
|
||||
OP_GT,
|
||||
OP_LTE,
|
||||
OP_GTE,
|
||||
operatorMap,
|
||||
defaultStringOperators,
|
||||
defaultNumberOperators,
|
||||
ConditionOperator,
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Represents a variable used in a condition and the set of operators that can be applied to this variable.
|
||||
*
|
||||
* A `ConditionVariable` holds the ID of the variable and a list of operators that define how this variable can be evaluated
|
||||
* in conditions. For example, if the variable is a request body or a specific field in a request, the operators can include
|
||||
* operations such as equality checks, comparisons, or other custom evaluations.
|
||||
*/
|
||||
class ConditionVariable {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
id;
|
||||
|
||||
/**
|
||||
* @type {import("./operators").ConditionOperator[]}
|
||||
*/
|
||||
operators = {};
|
||||
|
||||
/**
|
||||
* @param {string} id ID of variable
|
||||
* @param {import("./operators").ConditionOperator[]} operators Operators the condition supports
|
||||
*/
|
||||
constructor(id, operators = []) {
|
||||
this.id = id;
|
||||
this.operators = operators;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ConditionVariable,
|
||||
};
|
@ -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,
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Onesender extends NotificationProvider {
|
||||
name = "Onesender";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
to: notification.onesenderReceiver,
|
||||
type: "text",
|
||||
recipient_type: "individual",
|
||||
text: {
|
||||
body: msg
|
||||
}
|
||||
};
|
||||
if (notification.onesenderTypeReceiver === "private") {
|
||||
data.to = notification.onesenderReceiver + "@s.whatsapp.net";
|
||||
} else {
|
||||
data.recipient_type = "group";
|
||||
data.to = notification.onesenderReceiver + "@g.us";
|
||||
}
|
||||
let config = {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + notification.onesenderToken,
|
||||
}
|
||||
};
|
||||
await axios.post(notification.onesenderURL, data, config);
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Onesender;
|
@ -0,0 +1,52 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { UP, DOWN } = require("../../src/util");
|
||||
|
||||
class SIGNL4 extends NotificationProvider {
|
||||
name = "SIGNL4";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
// Source system
|
||||
"X-S4-SourceSystem": "UptimeKuma",
|
||||
monitorUrl: this.extractAdress(monitorJSON),
|
||||
};
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
if (heartbeatJSON == null) {
|
||||
// Test alert
|
||||
data.title = "Uptime Kuma Alert";
|
||||
data.message = msg;
|
||||
} else if (heartbeatJSON.status === UP) {
|
||||
data.title = "Uptime Kuma Monitor ✅ Up";
|
||||
data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID;
|
||||
data["X-S4-Status"] = "resolved";
|
||||
} else if (heartbeatJSON.status === DOWN) {
|
||||
data.title = "Uptime Kuma Monitor 🔴 Down";
|
||||
data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID;
|
||||
data["X-S4-Status"] = "new";
|
||||
}
|
||||
|
||||
await axios.post(notification.webhookURL, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SIGNL4;
|
@ -0,0 +1,51 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class WPush extends NotificationProvider {
|
||||
name = "WPush";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
const context = {
|
||||
"title": this.checkStatus(heartbeatJSON, monitorJSON),
|
||||
"content": msg,
|
||||
"apikey": notification.wpushAPIkey,
|
||||
"channel": notification.wpushChannel
|
||||
};
|
||||
const result = await axios.post("https://api.wpush.cn/api/v1/send", context);
|
||||
if (result.data.code !== 0) {
|
||||
throw result.data.message;
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted title for message
|
||||
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @param {?object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @returns {string} Formatted title
|
||||
*/
|
||||
checkStatus(heartbeatJSON, monitorJSON) {
|
||||
let title = "UptimeKuma Message";
|
||||
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||
title = "UptimeKuma Monitor Up " + monitorJSON["name"];
|
||||
}
|
||||
if (heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
||||
title = "UptimeKuma Monitor Down " + monitorJSON["name"];
|
||||
}
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WPush;
|
@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div class="monitor-condition mb-3" data-testid="condition">
|
||||
<button
|
||||
v-if="!isInGroup || !isFirst || !isLast"
|
||||
class="btn btn-outline-danger remove-button"
|
||||
type="button"
|
||||
:aria-label="$t('conditionDelete')"
|
||||
data-testid="remove-condition"
|
||||
@click="remove"
|
||||
>
|
||||
<font-awesome-icon icon="trash" />
|
||||
</button>
|
||||
|
||||
<select v-if="!isFirst" v-model="model.andOr" class="form-select and-or-select" data-testid="condition-and-or">
|
||||
<option value="and">{{ $t("and") }}</option>
|
||||
<option value="or">{{ $t("or") }}</option>
|
||||
</select>
|
||||
|
||||
<select v-model="model.variable" class="form-select" data-testid="condition-variable">
|
||||
<option
|
||||
v-for="variable in conditionVariables"
|
||||
:key="variable.id"
|
||||
:value="variable.id"
|
||||
>
|
||||
{{ $t(variable.id) }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<select v-model="model.operator" class="form-select" data-testid="condition-operator">
|
||||
<option
|
||||
v-for="operator in getVariableOperators(model.variable)"
|
||||
:key="operator.id"
|
||||
:value="operator.id"
|
||||
>
|
||||
{{ $t(operator.caption) }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
v-model="model.value"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:aria-label="$t('conditionValuePlaceholder')"
|
||||
data-testid="condition-value"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "EditMonitorCondition",
|
||||
|
||||
props: {
|
||||
/**
|
||||
* The monitor condition
|
||||
*/
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether this is the first condition
|
||||
*/
|
||||
isFirst: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether this is the last condition
|
||||
*/
|
||||
isLast: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether this condition is in a group
|
||||
*/
|
||||
isInGroup: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* Variable choices
|
||||
*/
|
||||
conditionVariables: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
emits: [ "update:modelValue", "remove" ],
|
||||
|
||||
computed: {
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
remove() {
|
||||
this.$emit("remove", this.model);
|
||||
},
|
||||
|
||||
getVariableOperators(variableId) {
|
||||
return this.conditionVariables.find(v => v.id === variableId)?.operators ?? [];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.monitor-condition {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
justify-self: flex-end;
|
||||
margin-bottom: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@container (min-width: 500px) {
|
||||
.monitor-condition {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
margin-bottom: 0;
|
||||
margin-left: 10px;
|
||||
order: 100;
|
||||
}
|
||||
|
||||
.and-or-select {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div class="condition-group mb-3" data-testid="condition-group">
|
||||
<div class="d-flex">
|
||||
<select v-if="!isFirst" v-model="model.andOr" class="form-select" style="width: auto;" data-testid="condition-group-and-or">
|
||||
<option value="and">{{ $t("and") }}</option>
|
||||
<option value="or">{{ $t("or") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="condition-group-inner mt-2 pa-2">
|
||||
<div class="condition-group-conditions">
|
||||
<template v-for="(child, childIndex) in model.children" :key="childIndex">
|
||||
<EditMonitorConditionGroup
|
||||
v-if="child.type === 'group'"
|
||||
v-model="model.children[childIndex]"
|
||||
:is-first="childIndex === 0"
|
||||
:get-new-group="getNewGroup"
|
||||
:get-new-condition="getNewCondition"
|
||||
:condition-variables="conditionVariables"
|
||||
@remove="removeChild"
|
||||
/>
|
||||
<EditMonitorCondition
|
||||
v-else
|
||||
v-model="model.children[childIndex]"
|
||||
:is-first="childIndex === 0"
|
||||
:is-last="childIndex === model.children.length - 1"
|
||||
:is-in-group="true"
|
||||
:condition-variables="conditionVariables"
|
||||
@remove="removeChild"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="condition-group-actions mt-3">
|
||||
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-condition-button" @click="addCondition">
|
||||
{{ $t("conditionAdd") }}
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-group-button" @click="addGroup">
|
||||
{{ $t("conditionAddGroup") }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-danger"
|
||||
type="button"
|
||||
:aria-label="$t('conditionDeleteGroup')"
|
||||
data-testid="remove-condition-group"
|
||||
@click="remove"
|
||||
>
|
||||
<font-awesome-icon icon="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditMonitorCondition from "./EditMonitorCondition.vue";
|
||||
|
||||
export default {
|
||||
name: "EditMonitorConditionGroup",
|
||||
|
||||
components: {
|
||||
EditMonitorCondition,
|
||||
},
|
||||
|
||||
props: {
|
||||
/**
|
||||
* The condition group
|
||||
*/
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether this is the first condition
|
||||
*/
|
||||
isFirst: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to generate a new group model
|
||||
*/
|
||||
getNewGroup: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to generate a new condition model
|
||||
*/
|
||||
getNewCondition: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Variable choices
|
||||
*/
|
||||
conditionVariables: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
emits: [ "update:modelValue", "remove" ],
|
||||
|
||||
computed: {
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addGroup() {
|
||||
const conditions = [ ...this.model.children ];
|
||||
conditions.push(this.getNewGroup());
|
||||
this.model.children = conditions;
|
||||
},
|
||||
|
||||
addCondition() {
|
||||
const conditions = [ ...this.model.children ];
|
||||
conditions.push(this.getNewCondition());
|
||||
this.model.children = conditions;
|
||||
},
|
||||
|
||||
remove() {
|
||||
this.$emit("remove", this.model);
|
||||
},
|
||||
|
||||
removeChild(child) {
|
||||
const idx = this.model.children.indexOf(child);
|
||||
if (idx !== -1) {
|
||||
this.model.children.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.condition-group-inner {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dark .condition-group-inner {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.condition-group-conditions {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.condition-group-actions {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
// Delete button
|
||||
.condition-group-actions > :last-child {
|
||||
margin-left: auto;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@container (min-width: 400px) {
|
||||
.condition-group-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
// Delete button
|
||||
.condition-group-actions > :last-child {
|
||||
margin-left: auto;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.btn-delete-group {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="monitor-conditions">
|
||||
<label class="form-label">{{ $t("Conditions") }}</label>
|
||||
<div class="monitor-conditions-conditions">
|
||||
<template v-for="(condition, conditionIndex) in model" :key="conditionIndex">
|
||||
<EditMonitorConditionGroup
|
||||
v-if="condition.type === 'group'"
|
||||
v-model="model[conditionIndex]"
|
||||
:is-first="conditionIndex === 0"
|
||||
:get-new-group="getNewGroup"
|
||||
:get-new-condition="getNewCondition"
|
||||
:condition-variables="conditionVariables"
|
||||
@remove="removeCondition"
|
||||
/>
|
||||
<EditMonitorCondition
|
||||
v-else
|
||||
v-model="model[conditionIndex]"
|
||||
:is-first="conditionIndex === 0"
|
||||
:is-last="conditionIndex === model.length - 1"
|
||||
:condition-variables="conditionVariables"
|
||||
@remove="removeCondition"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="monitor-conditions-buttons">
|
||||
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-condition-button" @click="addCondition">
|
||||
{{ $t("conditionAdd") }}
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-group-button" @click="addGroup">
|
||||
{{ $t("conditionAddGroup") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditMonitorConditionGroup from "./EditMonitorConditionGroup.vue";
|
||||
import EditMonitorCondition from "./EditMonitorCondition.vue";
|
||||
|
||||
export default {
|
||||
name: "EditMonitorConditions",
|
||||
|
||||
components: {
|
||||
EditMonitorConditionGroup,
|
||||
EditMonitorCondition,
|
||||
},
|
||||
|
||||
props: {
|
||||
/**
|
||||
* The monitor conditions
|
||||
*/
|
||||
modelValue: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
conditionVariables: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
emits: [ "update:modelValue" ],
|
||||
|
||||
computed: {
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.model.length === 0) {
|
||||
this.addCondition();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getNewGroup() {
|
||||
return {
|
||||
type: "group",
|
||||
children: [ this.getNewCondition() ],
|
||||
andOr: "and",
|
||||
};
|
||||
},
|
||||
|
||||
getNewCondition() {
|
||||
const firstVariable = this.conditionVariables[0]?.id || null;
|
||||
const firstOperator = this.getVariableOperators(firstVariable)[0] || null;
|
||||
return {
|
||||
type: "expression",
|
||||
variable: firstVariable,
|
||||
operator: firstOperator?.id || null,
|
||||
value: "",
|
||||
andOr: "and",
|
||||
};
|
||||
},
|
||||
|
||||
addGroup() {
|
||||
const conditions = [ ...this.model ];
|
||||
conditions.push(this.getNewGroup());
|
||||
this.$emit("update:modelValue", conditions);
|
||||
},
|
||||
|
||||
addCondition() {
|
||||
const conditions = [ ...this.model ];
|
||||
conditions.push(this.getNewCondition());
|
||||
this.$emit("update:modelValue", conditions);
|
||||
},
|
||||
|
||||
removeCondition(condition) {
|
||||
const conditions = [ ...this.model ];
|
||||
const idx = conditions.indexOf(condition);
|
||||
if (idx !== -1) {
|
||||
conditions.splice(idx, 1);
|
||||
this.$emit("update:modelValue", conditions);
|
||||
}
|
||||
},
|
||||
|
||||
getVariableOperators(variableId) {
|
||||
return this.conditionVariables.find(v => v.id === variableId)?.operators ?? [];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.monitor-conditions,
|
||||
.monitor-conditions-conditions {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.monitor-conditions-buttons {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@container (min-width: 400px) {
|
||||
.monitor-conditions-buttons {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="host-onesender" class="form-label">{{ $t("Host Onesender") }}</label>
|
||||
<input
|
||||
id="host-onesender"
|
||||
v-model="$parent.notification.onesenderURL"
|
||||
type="url"
|
||||
placeholder="https://xxxxxxxxxxx.com/api/v1/messages"
|
||||
pattern="https?://.+"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="receiver-onesender" class="form-label">{{ $t("Token Onesender") }}</label>
|
||||
<HiddenInput id="receiver-onesender" v-model="$parent.notification.onesenderToken" :required="true" autocomplete="false"></HiddenInput>
|
||||
<i18n-t tag="div" keypath="wayToGetOnesenderUrlandToken" class="form-text">
|
||||
<a href="https://onesender.net/" target="_blank">{{ $t("here") }}</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="webhook-request-body" class="form-label">{{ $t("Recipient Type") }}</label>
|
||||
<select
|
||||
id="webhook-request-body"
|
||||
v-model="$parent.notification.onesenderTypeReceiver"
|
||||
class="form-select"
|
||||
required
|
||||
>
|
||||
<option value="private">{{ $t("Private Number") }}</option>
|
||||
<option value="group">{{ $t("Group ID") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="$parent.notification.onesenderTypeReceiver == 'private'" class="form-text">{{ $t("privateOnesenderDesc", ['"application/json"']) }}</div>
|
||||
<div v-else class="form-text">{{ $t("groupOnesenderDesc") }}</div>
|
||||
<div class="mb-3">
|
||||
<input
|
||||
id="type-receiver-onesender"
|
||||
v-model="$parent.notification.onesenderReceiver"
|
||||
type="text"
|
||||
placeholder="628123456789 or 628123456789-34534"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input
|
||||
id="type-receiver-onesender"
|
||||
v-model="computedReceiverResult"
|
||||
type="text"
|
||||
class="form-control"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
computedReceiverResult() {
|
||||
let receiver = this.$parent.notification.onesenderReceiver;
|
||||
return this.$parent.notification.onesenderTypeReceiver === "private" ? receiver + "@s.whatsapp.net" : receiver + "@g.us";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
textarea {
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="signl4-webhook-url" class="form-label">{{ $t("SIGNL4 Webhook URL") }}</label>
|
||||
<input
|
||||
id="signl4-webhook-url"
|
||||
v-model="$parent.notification.webhookURL"
|
||||
type="url"
|
||||
pattern="https?://.+"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
<i18n-t tag="div" keypath="signl4Docs" class="form-text">
|
||||
<a href="https://docs.signl4.com/integrations/uptime-kuma/uptime-kuma.html" target="_blank">SIGNL4 Docs</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="wpush-apikey" class="form-label">WPush {{ $t("API Key") }}</label>
|
||||
<HiddenInput id="wpush-apikey" v-model="$parent.notification.wpushAPIkey" :required="true" autocomplete="new-password" placeholder="WPushxxxxx"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="wpush-channel" class="form-label">发送通道</label>
|
||||
<select id="wpush-channel" v-model="$parent.notification.wpushChannel" class="form-select" required>
|
||||
<option value="wechat">微信</option>
|
||||
<option value="sms">短信</option>
|
||||
<option value="mail">邮件</option>
|
||||
<option value="feishu">飞书</option>
|
||||
<option value="dingtalk">钉钉</option>
|
||||
<option value="wechat_work">企业微信</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<i18n-t tag="p" keypath="More info on:">
|
||||
<a href="https://wpush.cn/" rel="noopener noreferrer" target="_blank">https://wpush.cn/</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,20 +0,0 @@
|
||||
// Check Node.js version
|
||||
const semver = require("semver");
|
||||
const childProcess = require("child_process");
|
||||
|
||||
const nodeVersion = process.versions.node;
|
||||
console.log("Node.js version: " + nodeVersion);
|
||||
|
||||
|
||||
|
||||
// Node.js version >= 18
|
||||
if (semver.satisfies(nodeVersion, ">= 18")) {
|
||||
console.log("Use the native test runner: `node --test`");
|
||||
childProcess.execSync("npm run test-backend:18", { stdio: "inherit" });
|
||||
} else {
|
||||
// 14 - 16 here
|
||||
console.log("Use `test` package: `node--test`")
|
||||
childProcess.execSync("npm run test-backend:14", { stdio: "inherit" });
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,46 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js");
|
||||
const { evaluateExpressionGroup, evaluateExpression } = require("../../../server/monitor-conditions/evaluator.js");
|
||||
|
||||
test("Test evaluateExpression", async (t) => {
|
||||
const expr = new ConditionExpression("record", "contains", "mx1.example.com");
|
||||
assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" }));
|
||||
assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.example.com" }));
|
||||
});
|
||||
|
||||
test("Test evaluateExpressionGroup with logical AND", async (t) => {
|
||||
const group = new ConditionExpressionGroup([
|
||||
new ConditionExpression("record", "contains", "mx1."),
|
||||
new ConditionExpression("record", "contains", "example.com", LOGICAL.AND),
|
||||
]);
|
||||
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" }));
|
||||
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." }));
|
||||
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" }));
|
||||
});
|
||||
|
||||
test("Test evaluateExpressionGroup with logical OR", async (t) => {
|
||||
const group = new ConditionExpressionGroup([
|
||||
new ConditionExpression("record", "contains", "example.com"),
|
||||
new ConditionExpression("record", "contains", "example.org", LOGICAL.OR),
|
||||
]);
|
||||
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.com" }));
|
||||
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.org" }));
|
||||
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" }));
|
||||
});
|
||||
|
||||
test("Test evaluateExpressionGroup with nested group", async (t) => {
|
||||
const group = new ConditionExpressionGroup([
|
||||
new ConditionExpression("record", "contains", "mx1."),
|
||||
new ConditionExpressionGroup([
|
||||
new ConditionExpression("record", "contains", "example.com"),
|
||||
new ConditionExpression("record", "contains", "example.org", LOGICAL.OR),
|
||||
]),
|
||||
]);
|
||||
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." }));
|
||||
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" }));
|
||||
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.org" }));
|
||||
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" }));
|
||||
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.org" }));
|
||||
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1.example.net" }));
|
||||
});
|
@ -0,0 +1,55 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { ConditionExpressionGroup, ConditionExpression } = require("../../../server/monitor-conditions/expression.js");
|
||||
|
||||
test("Test ConditionExpressionGroup.fromMonitor", async (t) => {
|
||||
const monitor = {
|
||||
conditions: JSON.stringify([
|
||||
{
|
||||
"type": "expression",
|
||||
"andOr": "and",
|
||||
"operator": "contains",
|
||||
"value": "foo",
|
||||
"variable": "record"
|
||||
},
|
||||
{
|
||||
"type": "group",
|
||||
"andOr": "and",
|
||||
"children": [
|
||||
{
|
||||
"type": "expression",
|
||||
"andOr": "and",
|
||||
"operator": "contains",
|
||||
"value": "bar",
|
||||
"variable": "record"
|
||||
},
|
||||
{
|
||||
"type": "group",
|
||||
"andOr": "and",
|
||||
"children": [
|
||||
{
|
||||
"type": "expression",
|
||||
"andOr": "and",
|
||||
"operator": "contains",
|
||||
"value": "car",
|
||||
"variable": "record"
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]),
|
||||
};
|
||||
const root = ConditionExpressionGroup.fromMonitor(monitor);
|
||||
assert.strictEqual(true, root.children.length === 2);
|
||||
assert.strictEqual(true, root.children[0] instanceof ConditionExpression);
|
||||
assert.strictEqual(true, root.children[0].value === "foo");
|
||||
assert.strictEqual(true, root.children[1] instanceof ConditionExpressionGroup);
|
||||
assert.strictEqual(true, root.children[1].children.length === 2);
|
||||
assert.strictEqual(true, root.children[1].children[0] instanceof ConditionExpression);
|
||||
assert.strictEqual(true, root.children[1].children[0].value === "bar");
|
||||
assert.strictEqual(true, root.children[1].children[1] instanceof ConditionExpressionGroup);
|
||||
assert.strictEqual(true, root.children[1].children[1].children.length === 1);
|
||||
assert.strictEqual(true, root.children[1].children[1].children[0] instanceof ConditionExpression);
|
||||
assert.strictEqual(true, root.children[1].children[1].children[0].value === "car");
|
||||
});
|
@ -0,0 +1,108 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { operatorMap, OP_CONTAINS, OP_NOT_CONTAINS, OP_LT, OP_GT, OP_LTE, OP_GTE, OP_STR_EQUALS, OP_STR_NOT_EQUALS, OP_NUM_EQUALS, OP_NUM_NOT_EQUALS, OP_STARTS_WITH, OP_ENDS_WITH, OP_NOT_STARTS_WITH, OP_NOT_ENDS_WITH } = require("../../../server/monitor-conditions/operators.js");
|
||||
|
||||
test("Test StringEqualsOperator", async (t) => {
|
||||
const op = operatorMap.get(OP_STR_EQUALS);
|
||||
assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com"));
|
||||
assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org"));
|
||||
assert.strictEqual(false, op.test("1", 1)); // strict equality
|
||||
});
|
||||
|
||||
test("Test StringNotEqualsOperator", async (t) => {
|
||||
const op = operatorMap.get(OP_STR_NOT_EQUALS);
|
||||
assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org"));
|
||||
assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com"));
|
||||
assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality)
|
||||
});
|
||||
|
||||
test("Test ContainsOperator with scalar", async (t) => {
|
||||
const op = operatorMap.get(OP_CONTAINS);
|
||||
assert.strictEqual(true, op.test("mx1.example.org", "example.org"));
|
||||
assert.strictEqual(false, op.test("mx1.example.org", "example.com"));
|
||||
});
|
||||
|
||||
test("Test ContainsOperator with array", async (t) => {
|
||||
const op = operatorMap.get(OP_CONTAINS);
|
||||
assert.strictEqual(true, op.test([ "example.org" ], "example.org"));
|
||||
assert.strictEqual(false, op.test([ "example.org" ], "example.com"));
|
||||
});
|
||||
|
||||
test("Test NotContainsOperator with scalar", async (t) => {
|
||||
const op = operatorMap.get(OP_NOT_CONTAINS);
|
||||
assert.strictEqual(true, op.test("example.org", ".com"));
|
||||
assert.strictEqual(false, op.test("example.org", ".org"));
|
||||
});
|
||||
|
||||
test("Test NotContainsOperator with array", async (t) => {
|
||||
const op = operatorMap.get(OP_NOT_CONTAINS);
|
||||
assert.strictEqual(true, op.test([ "example.org" ], "example.com"));
|
||||
assert.strictEqual(false, op.test([ "example.org" ], "example.org"));
|
||||
});
|
||||
|
||||
test("Test StartsWithOperator", async (t) => {
|
||||
const op = operatorMap.get(OP_STARTS_WITH);
|
||||
assert.strictEqual(true, op.test("mx1.example.com", "mx1"));
|
||||
assert.strictEqual(false, op.test("mx1.example.com", "mx2"));
|
||||
});
|
||||
|
||||
test("Test NotStartsWithOperator", async (t) => {
|
||||
const op = operatorMap.get(OP_NOT_STARTS_WITH);
|
||||
assert.strictEqual(true, op.test("mx1.example.com", "mx2"));
|
||||
assert.strictEqual(false, op.test("mx1.example.com", "mx1"));
|
||||
});
|
||||
|
||||
test("Test EndsWithOperator", async (t) => {
|
||||
const op = operatorMap.get(OP_ENDS_WITH);
|
||||
assert.strictEqual(true, op.test("mx1.example.com", "example.com"));
|
||||
assert.strictEqual(false, op.test("mx1.example.com", "example.net"));
|
||||
});
|
||||
|
||||
test("Test NotEndsWithOperator", async (t) => {
|
||||
const op = operatorMap.get(OP_NOT_ENDS_WITH);
|
||||
assert.strictEqual(true, op.test("mx1.example.com", "example.net"));
|
||||
assert.strictEqual(false, op.test("mx1.example.com", "example.com"));
|
||||
});
|
||||
|
||||
test("Test NumberEqualsOperator", async (t) => {
|
||||
const op = operatorMap.get(OP_NUM_EQUALS);
|
||||
assert.strictEqual(true, op.test(1, 1));
|
||||
assert.strictEqual(true, op.test(1, "1"));
|
||||
assert.strictEqual(false, op.test(1, "2"));
|
||||
});
|
||||
|
||||
test("Test NumberNotEqualsOperator", async (t) => {
|
||||
const op = operatorMap.get(OP_NUM_NOT_EQUALS);
|
||||
assert.strictEqual(true, op.test(1, "2"));
|
||||
assert.strictEqual(false, op.test(1, "1"));
|
||||
});
|
||||
|
||||
test("Test LessThanOperator", async (t) => {
|
||||
const op = operatorMap.get(OP_LT);
|
||||
assert.strictEqual(true, op.test(1, 2));
|
||||
assert.strictEqual(true, op.test(1, "2"));
|
||||
assert.strictEqual(false, op.test(1, 1));
|
||||
});
|
||||
|
||||
test("Test GreaterThanOperator", async (t) => {
|
||||
const op = operatorMap.get(OP_GT);
|
||||
assert.strictEqual(true, op.test(2, 1));
|
||||
assert.strictEqual(true, op.test(2, "1"));
|
||||
assert.strictEqual(false, op.test(1, 1));
|
||||
});
|
||||
|
||||
test("Test LessThanOrEqualToOperator", async (t) => {
|
||||
const op = operatorMap.get(OP_LTE);
|
||||
assert.strictEqual(true, op.test(1, 1));
|
||||
assert.strictEqual(true, op.test(1, 2));
|
||||
assert.strictEqual(true, op.test(1, "2"));
|
||||
assert.strictEqual(false, op.test(1, 0));
|
||||
});
|
||||
|
||||
test("Test GreaterThanOrEqualToOperator", async (t) => {
|
||||
const op = operatorMap.get(OP_GTE);
|
||||
assert.strictEqual(true, op.test(1, 1));
|
||||
assert.strictEqual(true, op.test(2, 1));
|
||||
assert.strictEqual(true, op.test(2, "2"));
|
||||
assert.strictEqual(false, op.test(2, 3));
|
||||
});
|
@ -1,43 +0,0 @@
|
||||
import { test } from "@playwright/test";
|
||||
import { login, screenshot } from "./util-test";
|
||||
|
||||
/*
|
||||
* Setup
|
||||
*/
|
||||
|
||||
test("setup sqlite", async ({ page }, testInfo) => {
|
||||
await page.goto("./");
|
||||
await page.getByText("SQLite").click();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
test("setup admin", async ({ page }, testInfo) => {
|
||||
await page.goto("./");
|
||||
await page.getByPlaceholder("Username").click();
|
||||
await page.getByPlaceholder("Username").fill("admin");
|
||||
await page.getByPlaceholder("Username").press("Tab");
|
||||
await page.getByPlaceholder("Password", { exact: true }).fill("admin123");
|
||||
await page.getByPlaceholder("Password", { exact: true }).press("Tab");
|
||||
await page.getByPlaceholder("Repeat Password").fill("admin123");
|
||||
await page.getByRole("button", { name: "Create" }).click();
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
/*
|
||||
* All other tests should be run after setup
|
||||
*/
|
||||
|
||||
test("login", async ({ page }, testInfo) => {
|
||||
await page.goto("./dashboard");
|
||||
await login(page);
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
test("logout", async ({ page }, testInfo) => {
|
||||
await page.goto("./dashboard");
|
||||
await login(page);
|
||||
await page.getByText("A", { exact: true }).click();
|
||||
await page.getByRole("button", { name: "Log out" }).click();
|
||||
await screenshot(testInfo, page);
|
||||
});
|
@ -0,0 +1,38 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
|
||||
|
||||
test.describe("Example Spec", () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await restoreSqliteSnapshot(page);
|
||||
});
|
||||
|
||||
test("dashboard", async ({ page }, testInfo) => {
|
||||
await page.goto("./dashboard");
|
||||
await login(page);
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
test("change display timezone", async ({ page }, testInfo) => {
|
||||
await page.goto("./settings/general");
|
||||
await login(page);
|
||||
await page.getByLabel("Display Timezone").selectOption("Pacific/Fiji");
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await screenshot(testInfo, page);
|
||||
|
||||
await page.goto("./dashboard");
|
||||
await page.goto("./settings/general");
|
||||
await expect(page.getByLabel("Display Timezone")).toHaveValue("Pacific/Fiji");
|
||||
});
|
||||
|
||||
test("database is reset after previous test", async ({ page }, testInfo) => {
|
||||
await page.goto("./settings/general");
|
||||
await login(page);
|
||||
|
||||
const timezoneEl = page.getByLabel("Display Timezone");
|
||||
await expect(timezoneEl).toBeVisible();
|
||||
await expect(timezoneEl).toHaveValue("auto");
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,109 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
|
||||
|
||||
test.describe("Monitor Form", () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await restoreSqliteSnapshot(page);
|
||||
});
|
||||
|
||||
test("condition ui", async ({ page }, testInfo) => {
|
||||
await page.goto("./add");
|
||||
await login(page);
|
||||
await screenshot(testInfo, page);
|
||||
|
||||
const monitorTypeSelect = page.getByTestId("monitor-type-select");
|
||||
await expect(monitorTypeSelect).toBeVisible();
|
||||
|
||||
await monitorTypeSelect.selectOption("dns");
|
||||
const selectedValue = await monitorTypeSelect.evaluate(select => select.value);
|
||||
expect(selectedValue).toBe("dns");
|
||||
|
||||
// Add Conditions & verify:
|
||||
await page.getByTestId("add-condition-button").click();
|
||||
expect(await page.getByTestId("condition").count()).toEqual(2); // 1 added by default + 1 explicitly added
|
||||
|
||||
// Add a Condition Group & verify:
|
||||
await page.getByTestId("add-group-button").click();
|
||||
expect(await page.getByTestId("condition-group").count()).toEqual(1);
|
||||
expect(await page.getByTestId("condition").count()).toEqual(3); // 2 solo conditions + 1 condition in group
|
||||
|
||||
await screenshot(testInfo, page);
|
||||
|
||||
// Remove a condition & verify:
|
||||
await page.getByTestId("remove-condition").first().click();
|
||||
expect(await page.getByTestId("condition").count()).toEqual(2); // 1 solo condition + 1 condition in group
|
||||
|
||||
// Remove a condition group & verify:
|
||||
await page.getByTestId("remove-condition-group").first().click();
|
||||
expect(await page.getByTestId("condition-group").count()).toEqual(0);
|
||||
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
test("successful condition", async ({ page }, testInfo) => {
|
||||
await page.goto("./add");
|
||||
await login(page);
|
||||
await screenshot(testInfo, page);
|
||||
|
||||
const monitorTypeSelect = page.getByTestId("monitor-type-select");
|
||||
await expect(monitorTypeSelect).toBeVisible();
|
||||
|
||||
await monitorTypeSelect.selectOption("dns");
|
||||
const selectedValue = await monitorTypeSelect.evaluate(select => select.value);
|
||||
expect(selectedValue).toBe("dns");
|
||||
|
||||
const friendlyName = "Example DNS NS";
|
||||
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||
await page.getByTestId("hostname-input").fill("example.com");
|
||||
|
||||
// Vue-Multiselect component
|
||||
const resolveTypeSelect = page.getByTestId("resolve-type-select");
|
||||
await resolveTypeSelect.click();
|
||||
await resolveTypeSelect.getByRole("option", { name: "NS" }).click();
|
||||
|
||||
await page.getByTestId("add-condition-button").click();
|
||||
expect(await page.getByTestId("condition").count()).toEqual(2); // 1 added by default + 1 explicitly added
|
||||
await page.getByTestId("condition-value").nth(0).fill("a.iana-servers.net");
|
||||
await page.getByTestId("condition-and-or").nth(0).selectOption("or");
|
||||
await page.getByTestId("condition-value").nth(1).fill("b.iana-servers.net");
|
||||
await screenshot(testInfo, page);
|
||||
|
||||
await page.getByTestId("save-button").click();
|
||||
await page.waitForURL("/dashboard/*"); // wait for the monitor to be created
|
||||
await expect(page.getByTestId("monitor-status")).toHaveText("up", { ignoreCase: true });
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
test("failing condition", async ({ page }, testInfo) => {
|
||||
await page.goto("./add");
|
||||
await login(page);
|
||||
await screenshot(testInfo, page);
|
||||
|
||||
const monitorTypeSelect = page.getByTestId("monitor-type-select");
|
||||
await expect(monitorTypeSelect).toBeVisible();
|
||||
|
||||
await monitorTypeSelect.selectOption("dns");
|
||||
const selectedValue = await monitorTypeSelect.evaluate(select => select.value);
|
||||
expect(selectedValue).toBe("dns");
|
||||
|
||||
const friendlyName = "Example DNS NS";
|
||||
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||
await page.getByTestId("hostname-input").fill("example.com");
|
||||
|
||||
// Vue-Multiselect component
|
||||
const resolveTypeSelect = page.getByTestId("resolve-type-select");
|
||||
await resolveTypeSelect.click();
|
||||
await resolveTypeSelect.getByRole("option", { name: "NS" }).click();
|
||||
|
||||
expect(await page.getByTestId("condition").count()).toEqual(1); // 1 added by default
|
||||
await page.getByTestId("condition-value").nth(0).fill("definitely-not.net");
|
||||
await screenshot(testInfo, page);
|
||||
|
||||
await page.getByTestId("save-button").click();
|
||||
await page.waitForURL("/dashboard/*"); // wait for the monitor to be created
|
||||
await expect(page.getByTestId("monitor-status")).toHaveText("down", { ignoreCase: true });
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,56 @@
|
||||
import { test } from "@playwright/test";
|
||||
import { getSqliteDatabaseExists, login, screenshot, takeSqliteSnapshot } from "../util-test";
|
||||
|
||||
test.describe("Uptime Kuma Setup", () => {
|
||||
|
||||
test.skip(() => getSqliteDatabaseExists(), "Must only run once per session");
|
||||
|
||||
/*
|
||||
* Setup
|
||||
*/
|
||||
|
||||
test("setup sqlite", async ({ page }, testInfo) => {
|
||||
await page.goto("./");
|
||||
await page.getByText("SQLite").click();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await screenshot(testInfo, page);
|
||||
await page.waitForURL("/setup"); // ensures the server is ready to continue to the next test
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
test("setup admin", async ({ page }, testInfo) => {
|
||||
await page.goto("./");
|
||||
await page.getByPlaceholder("Username").click();
|
||||
await page.getByPlaceholder("Username").fill("admin");
|
||||
await page.getByPlaceholder("Username").press("Tab");
|
||||
await page.getByPlaceholder("Password", { exact: true }).fill("admin123");
|
||||
await page.getByPlaceholder("Password", { exact: true }).press("Tab");
|
||||
await page.getByPlaceholder("Repeat Password").fill("admin123");
|
||||
await page.getByRole("button", { name: "Create" }).click();
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
/*
|
||||
* All other tests should be run after setup
|
||||
*/
|
||||
|
||||
test("login", async ({ page }, testInfo) => {
|
||||
await page.goto("./dashboard");
|
||||
await login(page);
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
test("logout", async ({ page }, testInfo) => {
|
||||
await page.goto("./dashboard");
|
||||
await login(page);
|
||||
await page.getByText("A", { exact: true }).click();
|
||||
await page.getByRole("button", { name: "Log out" }).click();
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
test("take sqlite snapshot", async ({ page }, testInfo) => {
|
||||
await takeSqliteSnapshot(page);
|
||||
await screenshot(testInfo, page);
|
||||
});
|
||||
|
||||
});
|
Loading…
Reference in new issue