ts6-botflow/flow-values.mjs
Bxio 1be8a45dde
All checks were successful
Testar e publicar imagem Docker / publish (push) Successful in 44s
Expand flow editor with TeamSpeak events, permissions and execution controls
2026-10-10 20:38:31 +01:00

309 lines
9.1 KiB
JavaScript

const forbidden = new Set(["__proto__", "prototype", "constructor"]);
export function valueAt(context, path) {
return String(path)
.split(".")
.reduce((value, key) => {
if (forbidden.has(key) || value == null) return undefined;
if (typeof value === "string" && key) {
try {
value = JSON.parse(value);
} catch {
return undefined;
}
}
return Object.hasOwn(value, key) ? value[key] : undefined;
}, context);
}
const number = (value) => {
if (
!["number", "string"].includes(typeof value) ||
String(value).trim() === "" ||
!Number.isFinite(Number(value))
)
throw new Error("O cálculo precisa de um valor numérico.");
return Number(value);
};
const functions = {
round: (v) => Math.round(number(v)),
floor: (v) => Math.floor(number(v)),
ceil: (v) => Math.ceil(number(v)),
abs: (v) => Math.abs(number(v)),
min: (...v) => Math.min(...v.map(number)),
max: (...v) => Math.max(...v.map(number)),
lower: (v) => String(v ?? "").toLowerCase(),
upper: (v) => String(v ?? "").toUpperCase(),
length: (v) => v?.length ?? 0,
contains: (a, b) => String(a ?? "").includes(String(b ?? "")),
startsWith: (a, b) => String(a ?? "").startsWith(String(b ?? "")),
endsWith: (a, b) => String(a ?? "").endsWith(String(b ?? "")),
};
// A small expression parser: no eval, property calls, assignments or JavaScript execution.
export function evaluate(expression, context = {}) {
const source = String(expression).trim();
if (source.length > 2000) throw new Error("Expressão demasiado longa.");
const tokens = [];
let offset = 0;
while (offset < source.length) {
const match =
/^(\s+|(?:\d+(?:\.\d*)?|\.\d+)|[A-Za-z_][\w]*(?:\.[\w]+)*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|===|!==|==|!=|>=|<=|&&|\|\||[+*/%(),!<>-])/.exec(
source.slice(offset),
);
if (!match) throw new Error("Expressão inválida.");
offset += match[0].length;
if (!/^\s+$/.test(match[0])) tokens.push(match[0]);
}
if (tokens.length > 250) throw new Error("Expressão demasiado complexa.");
let i = 0,
depth = 0;
const precedence = {
"||": 1,
"&&": 2,
"==": 3,
"!=": 3,
"===": 3,
"!==": 3,
">": 4,
"<": 4,
">=": 4,
"<=": 4,
"+": 5,
"-": 5,
"*": 6,
"/": 6,
"%": 6,
};
const binary = (a, op, b) => {
switch (op) {
case "+":
return number(a) + number(b);
case "-":
return number(a) - number(b);
case "*":
return number(a) * number(b);
case "/":
if (number(b) === 0) throw new Error("Divisão por zero.");
return number(a) / number(b);
case "%":
if (number(b) === 0) throw new Error("Divisão por zero.");
return number(a) % number(b);
case "==":
case "===":
return String(a) === String(b);
case "!=":
case "!==":
return String(a) !== String(b);
case ">":
return number(a) > number(b);
case "<":
return number(a) < number(b);
case ">=":
return number(a) >= number(b);
case "<=":
return number(a) <= number(b);
case "&&":
return Boolean(a) && Boolean(b);
case "||":
return Boolean(a) || Boolean(b);
}
};
function primary() {
if (++depth > 40) throw new Error("Expressão demasiado complexa.");
try {
const t = tokens[i++];
if (!t) throw new Error("Expressão incompleta.");
if (t === "!" || t === "-" || t === "+") {
const get = primary();
return () =>
t === "!" ? !get() : t === "-" ? -number(get()) : number(get());
}
if (t === "(") {
const get = parse(1);
if (tokens[i++] !== ")") throw new Error("Falta fechar parênteses.");
return get;
}
if (/^\d|^\.\d/.test(t)) return () => Number(t);
if (t[0] === '"') {
const value = JSON.parse(t);
return () => value;
}
if (t[0] === "'") {
const value = t.slice(1, -1).replace(/\\(['\\])/g, "$1");
return () => value;
}
if (t === "true") return () => true;
if (t === "false") return () => false;
if (tokens[i] === "(") {
if (!Object.hasOwn(functions, t))
throw new Error("Função não suportada.");
i++;
const args = [];
if (tokens[i] !== ")") {
do {
args.push(parse(1));
if (tokens[i] !== ",") break;
i++;
} while (true);
}
if (tokens[i++] !== ")") throw new Error("Falta fechar função.");
return () => functions[t](...args.map((get) => get()));
}
return () => valueAt(context, t);
} finally {
depth--;
}
}
function parse(min) {
let left = primary();
while ((precedence[tokens[i]] || 0) >= min) {
const before = left,
op = tokens[i++],
right = parse(precedence[op] + 1);
left =
op === "&&"
? () => Boolean(before()) && Boolean(right())
: op === "||"
? () => Boolean(before()) || Boolean(right())
: () => binary(before(), op, right());
}
return left;
}
if (!tokens.length) return "";
const result = parse(1);
if (i !== tokens.length) throw new Error("Expressão inválida.");
return result();
}
export function timeValues(now = new Date(), timezone = "Europe/Lisbon") {
const parts = Object.fromEntries(
new Intl.DateTimeFormat("en-GB", {
timeZone: timezone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
weekday: "short",
})
.formatToParts(now)
.map((p) => [p.type, p.value]),
);
return {
time: `${parts.hour}:${parts.minute}`,
seconds: parts.second,
hours: parts.hour,
minutes: parts.minute,
date: `${parts.day}/${parts.month}/${parts.year}`,
timestamp: Math.floor(now.getTime() / 1000),
day: parts.day,
month: parts.month,
year: parts.year,
dayOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(
parts.weekday,
),
};
}
const display = (value) =>
value == null
? ""
: typeof value === "object"
? JSON.stringify(value)
: String(value);
function splitFilters(raw) {
const parts = [];
let start = 0,
quote = "",
escaped = false;
for (let i = 0; i < raw.length; i++) {
const c = raw[i];
if (escaped) {
escaped = false;
continue;
}
if (c === "\\" && quote) {
escaped = true;
continue;
}
if (quote) {
if (c === quote) quote = "";
continue;
}
if (c === '"' || c === "'") {
quote = c;
continue;
}
if (c === "|" && raw[i - 1] !== "|" && raw[i + 1] !== "|") {
parts.push(raw.slice(start, i).trim());
start = i + 1;
}
}
parts.push(raw.slice(start).trim());
return parts;
}
export function interpolate(text, context) {
return String(text ?? "").replace(/\{\{([^{}]+)\}\}/g, (_, raw) => {
const parts = splitFilters(raw);
const path = parts.shift();
let value = path === "time" ? context.time : valueAt(context, path);
if (value === undefined) {
try {
value = evaluate(path, {
...context,
time: context.clock || context.time,
});
} catch (error) {
if (/^[\w.]+$/.test(path)) return "";
throw error;
}
}
for (const filter of parts) {
if (filter === "json") value = JSON.stringify(value);
else if (filter === "uptime") {
const seconds = Math.max(0, number(value));
value = `${Math.floor(seconds / 86400)}d ${Math.floor(seconds / 3600) % 24}h ${Math.floor(seconds / 60) % 60}m`;
} else if (Object.hasOwn(functions, filter))
value = functions[filter](value);
else throw new Error(`Filtro desconhecido: ${filter}`);
}
return display(value);
});
}
export function resolveObject(object, context) {
if (typeof object === "string") return interpolate(object, context);
if (Array.isArray(object))
return object.map((v) => resolveObject(v, context));
if (object && typeof object === "object")
return Object.fromEntries(
Object.entries(object)
.filter(([key]) => !forbidden.has(key))
.map(([k, v]) => [k, resolveObject(v, context)]),
);
return object;
}
export function compare(value, operator, other) {
switch (operator) {
case "greater":
return number(value) > number(other);
case "less":
return number(value) < number(other);
case "atLeast":
return number(value) >= number(other);
case "atMost":
return number(value) <= number(other);
case "contains":
return String(value ?? "").includes(String(other));
case "startsWith":
return String(value ?? "").startsWith(String(other));
case "endsWith":
return String(value ?? "").endsWith(String(other));
case "exists":
return value !== undefined && value !== null && value !== "";
case "notEquals":
return String(value) !== String(other);
case "equals":
return String(value) === String(other);
default:
throw new Error("Comparação inválida.");
}
}