All checks were successful
Testar e publicar imagem Docker / publish (push) Successful in 44s
799 lines
22 KiB
JavaScript
799 lines
22 KiB
JavaScript
const field = (key, label, type = "text", values) => [key, label, type, values];
|
|
const f = field,
|
|
client = f("clientId", "Cliente / {{event.clid}}", "client"),
|
|
dbclient = f("databaseId", "Cliente (ID da base de dados)", "dbclient"),
|
|
channel = f("channelId", "Canal / {{event.cid}}", "channel"),
|
|
group = f("groupId", "Grupo de servidor", "serverGroup"),
|
|
permission = f("permission", "Permissão", "permission");
|
|
const type = (title, category, fields = [], defaults = {}, extra = {}) => ({
|
|
title,
|
|
group: category,
|
|
color: ["Eventos", "Canais"].includes(category)
|
|
? "green"
|
|
: ["Clientes", "Grupos"].includes(category)
|
|
? "orange"
|
|
: category === "Permissões"
|
|
? "red"
|
|
: ["Integrações", "Dados"].includes(category)
|
|
? "cyan"
|
|
: "purple",
|
|
fields,
|
|
defaults,
|
|
outputs: ["out", "error"],
|
|
...extra,
|
|
});
|
|
const action = (title, category, command, fields, params, defaults = {}) =>
|
|
type(title, category, fields, defaults, { command, params });
|
|
export const nodeTypes = {
|
|
timer: type(
|
|
"Temporizador",
|
|
"Eventos",
|
|
[f("seconds", "Intervalo (segundos)", "number")],
|
|
{ seconds: 30 },
|
|
{ trigger: true, outputs: ["out"] },
|
|
),
|
|
cron: type(
|
|
"Calendário / Cron",
|
|
"Eventos",
|
|
[f("expression", "Cron: 5 ou 6 campos"), f("timezone", "Fuso horário")],
|
|
{ expression: "*/30 * * * * *", timezone: "Europe/Lisbon" },
|
|
{ trigger: true, outputs: ["out"] },
|
|
),
|
|
manual: type(
|
|
"Início manual",
|
|
"Eventos",
|
|
[],
|
|
{},
|
|
{ trigger: true, outputs: ["out"] },
|
|
),
|
|
event: type(
|
|
"Evento TeamSpeak",
|
|
"Eventos",
|
|
[
|
|
f("eventName", "Evento", "select", [
|
|
"notifycliententerview",
|
|
"notifyclientleftview",
|
|
"notifyclientmoved",
|
|
"notifytextmessage",
|
|
"notifychannelcreated",
|
|
"notifychanneledited",
|
|
"notifychanneldeleted",
|
|
"notifyserveredited",
|
|
]),
|
|
f("filters", "Filtros de campos (JSON)", "json"),
|
|
],
|
|
{ eventName: "notifycliententerview", filters: {} },
|
|
{ trigger: true, outputs: ["out"] },
|
|
),
|
|
command: type(
|
|
"Comando de chat",
|
|
"Eventos",
|
|
[
|
|
f("command", "Comando (com prefixo)"),
|
|
f("channelId", "Canal de escuta (vazio: privado/servidor)", "channel"),
|
|
f("allowedGroup", "Grupo autorizado (opcional)", "serverGroup"),
|
|
],
|
|
{ command: "!info", channelId: "", allowedGroup: "" },
|
|
{ trigger: true, outputs: ["out"] },
|
|
),
|
|
webhook: type(
|
|
"Webhook de entrada",
|
|
"Eventos",
|
|
[
|
|
f("secret", "Segredo do webhook", "password"),
|
|
f("filters", "Filtros do corpo (JSON)", "json"),
|
|
],
|
|
{ secret: "", filters: {} },
|
|
{ trigger: true, outputs: ["out"] },
|
|
),
|
|
serverinfo: type("Informações do servidor", "Dados"),
|
|
clients: type(
|
|
"Listar clientes",
|
|
"Dados",
|
|
[f("includeQuery", "Incluir clientes Query", "select", ["false", "true"])],
|
|
{ includeQuery: "false" },
|
|
),
|
|
channels: action(
|
|
"Listar canais",
|
|
"Dados",
|
|
"channellist",
|
|
[f("storeAs", "Guardar em temp…")],
|
|
{},
|
|
{ storeAs: "channels" },
|
|
),
|
|
groups: action(
|
|
"Listar grupos",
|
|
"Dados",
|
|
"servergrouplist",
|
|
[f("storeAs", "Guardar em temp…")],
|
|
{},
|
|
{ storeAs: "groups" },
|
|
),
|
|
channelGroups: action(
|
|
"Listar grupos de canal",
|
|
"Dados",
|
|
"channelgrouplist",
|
|
[f("storeAs", "Guardar em temp…")],
|
|
{},
|
|
{ storeAs: "channelGroups" },
|
|
),
|
|
permissions: action(
|
|
"Listar permissões",
|
|
"Dados",
|
|
"permissionlist",
|
|
[f("storeAs", "Guardar em temp…")],
|
|
{},
|
|
{ storeAs: "permissions" },
|
|
),
|
|
offlineClients: action(
|
|
"Clientes da base de dados",
|
|
"Dados",
|
|
"clientdblist",
|
|
[
|
|
f("start", "Início", "number"),
|
|
f("duration", "Quantidade", "number"),
|
|
f("storeAs", "Guardar em temp…"),
|
|
],
|
|
{ start: "start", duration: "duration" },
|
|
{ start: 0, duration: 50, storeAs: "offlineClients" },
|
|
),
|
|
clientInfo: action(
|
|
"Informações do cliente",
|
|
"Dados",
|
|
"clientinfo",
|
|
[client, f("storeAs", "Guardar em temp…")],
|
|
{ clid: "clientId" },
|
|
{ clientId: "{{event.clid}}", storeAs: "client" },
|
|
),
|
|
channelInfo: action(
|
|
"Informações do canal",
|
|
"Dados",
|
|
"channelinfo",
|
|
[channel, f("storeAs", "Guardar em temp…")],
|
|
{ cid: "channelId" },
|
|
{ channelId: 21, storeAs: "channel" },
|
|
),
|
|
condition: type(
|
|
"Condição",
|
|
"Lógica",
|
|
[
|
|
f("field", "Variável / caminho"),
|
|
f("operator", "Comparação", "select", [
|
|
"equals",
|
|
"notEquals",
|
|
"greater",
|
|
"less",
|
|
"atLeast",
|
|
"atMost",
|
|
"contains",
|
|
"startsWith",
|
|
"endsWith",
|
|
"exists",
|
|
]),
|
|
f("value", "Valor"),
|
|
],
|
|
{ field: "count", operator: "greater", value: "0" },
|
|
{ outputs: ["true", "false", "error"] },
|
|
),
|
|
expression: type(
|
|
"Condição por expressão",
|
|
"Lógica",
|
|
[f("expression", "Expressão")],
|
|
{ expression: "count > 0" },
|
|
{ outputs: ["true", "false", "error"] },
|
|
),
|
|
sort: type(
|
|
"Ordenar",
|
|
"Lógica",
|
|
[
|
|
f("list", "Lista / caminho"),
|
|
f("field", "Campo"),
|
|
f("direction", "Ordem", "select", ["desc", "asc"]),
|
|
],
|
|
{ list: "clients", field: "client_lastconnected", direction: "asc" },
|
|
),
|
|
limit: type("Top / limite", "Lógica", [f("count", "Quantidade", "number")], {
|
|
count: 5,
|
|
}),
|
|
filter: type(
|
|
"Filtrar lista",
|
|
"Lógica",
|
|
[
|
|
f("list", "Lista / caminho"),
|
|
f("field", "Campo"),
|
|
f("operator", "Comparação", "select", [
|
|
"equals",
|
|
"notEquals",
|
|
"greater",
|
|
"less",
|
|
"contains",
|
|
"exists",
|
|
]),
|
|
f("value", "Valor"),
|
|
f("storeAs", "Guardar em temp…"),
|
|
],
|
|
{
|
|
list: "clients",
|
|
field: "client_idle_time",
|
|
operator: "greater",
|
|
value: "300000",
|
|
storeAs: "filtered",
|
|
},
|
|
),
|
|
foreach: type(
|
|
"Para cada item",
|
|
"Lógica",
|
|
[f("list", "Lista / caminho"), f("limit", "Máximo de itens", "number")],
|
|
{ list: "clients", limit: 100 },
|
|
{ outputs: ["each", "done", "error"] },
|
|
),
|
|
variable: type(
|
|
"Definir variável",
|
|
"Lógica",
|
|
[
|
|
f("name", "Nome"),
|
|
f("scope", "Guardar", "select", ["temp", "persistent"]),
|
|
f("operation", "Operação", "select", [
|
|
"set",
|
|
"increment",
|
|
"append",
|
|
"delete",
|
|
]),
|
|
f("valueType", "Tipo do valor", "select", [
|
|
"text",
|
|
"number",
|
|
"boolean",
|
|
"json",
|
|
]),
|
|
f("value", "Valor", "textarea"),
|
|
],
|
|
{
|
|
name: "resultado",
|
|
scope: "temp",
|
|
operation: "set",
|
|
valueType: "text",
|
|
value: "",
|
|
},
|
|
),
|
|
calculate: type(
|
|
"Calcular",
|
|
"Lógica",
|
|
[f("expression", "Expressão"), f("storeAs", "Guardar em temp…")],
|
|
{
|
|
expression:
|
|
"server.virtualserver_clientsonline - server.virtualserver_queryclientsonline",
|
|
storeAs: "online",
|
|
},
|
|
),
|
|
random: type(
|
|
"Gerar código",
|
|
"Lógica",
|
|
[
|
|
f("length", "Comprimento", "number"),
|
|
f("alphabet", "Tipo", "select", ["numbers", "letters", "mixed"]),
|
|
f("storeAs", "Guardar em temp…"),
|
|
],
|
|
{ length: 6, alphabet: "numbers", storeAs: "code" },
|
|
),
|
|
template: type(
|
|
"Preparar texto",
|
|
"Lógica",
|
|
[f("text", "Texto e variáveis", "textarea")],
|
|
{ text: "{{top}}" },
|
|
),
|
|
delay: type("Esperar", "Lógica", [f("seconds", "Segundos", "number")], {
|
|
seconds: 1,
|
|
}),
|
|
stop: type("Terminar ramo", "Lógica", [], {}, { outputs: [] }),
|
|
log: type(
|
|
"Registar mensagem",
|
|
"Lógica",
|
|
[f("text", "Mensagem", "textarea")],
|
|
{ text: "Executado às {{time}}" },
|
|
),
|
|
message: type(
|
|
"Enviar mensagem",
|
|
"Clientes",
|
|
[
|
|
f("mode", "Destino", "select", ["client", "channel", "server"]),
|
|
f("target", "Destino (ID / variável)", "client"),
|
|
f("text", "Mensagem", "textarea"),
|
|
],
|
|
{
|
|
mode: "client",
|
|
target: "{{event.clid}}",
|
|
text: "Olá {{event.client_nickname}}!",
|
|
},
|
|
),
|
|
poke: action(
|
|
"Poke cliente",
|
|
"Clientes",
|
|
"clientpoke",
|
|
[client, f("text", "Mensagem", "textarea")],
|
|
{ clid: "clientId", msg: "text" },
|
|
{ clientId: "{{event.clid}}", text: "Olá!" },
|
|
),
|
|
move: action(
|
|
"Mover cliente",
|
|
"Clientes",
|
|
"clientmove",
|
|
[
|
|
client,
|
|
channel,
|
|
f("password", "Password do canal (opcional)", "password"),
|
|
],
|
|
{ clid: "clientId", cid: "channelId", cpw: "password" },
|
|
{ clientId: "{{event.clid}}", channelId: 21 },
|
|
),
|
|
kick: action(
|
|
"Expulsar cliente",
|
|
"Clientes",
|
|
"clientkick",
|
|
[
|
|
client,
|
|
f("reasonId", "Destino", "select", ["5", "4"]),
|
|
f("reason", "Motivo"),
|
|
],
|
|
{ clid: "clientId", reasonid: "reasonId", reasonmsg: "reason" },
|
|
{ clientId: "{{event.clid}}", reasonId: "5", reason: "Regra automática" },
|
|
),
|
|
ban: action(
|
|
"Banir cliente",
|
|
"Clientes",
|
|
"banclient",
|
|
[
|
|
client,
|
|
f("seconds", "Duração (0: permanente)", "number"),
|
|
f("reason", "Motivo"),
|
|
],
|
|
{ clid: "clientId", time: "seconds", banreason: "reason" },
|
|
{ clientId: "{{event.clid}}", seconds: 60, reason: "Regra automática" },
|
|
),
|
|
unban: action(
|
|
"Remover ban",
|
|
"Clientes",
|
|
"bandel",
|
|
[f("banId", "ID do ban")],
|
|
{ banid: "banId" },
|
|
{ banId: "" },
|
|
),
|
|
channelCreate: type(
|
|
"Criar canal",
|
|
"Canais",
|
|
[
|
|
f("name", "Nome"),
|
|
f("parentId", "Canal pai (0: raiz)", "channel"),
|
|
f("topic", "Tópico"),
|
|
f("description", "Descrição", "textarea"),
|
|
f("password", "Password", "password"),
|
|
f("permanent", "Permanente", "select", ["false", "true"]),
|
|
f("params", "Campos adicionais (JSON)", "json"),
|
|
f("storeAs", "Guardar resultado em temp…"),
|
|
],
|
|
{
|
|
name: "Novo canal",
|
|
parentId: 0,
|
|
permanent: "false",
|
|
params: {},
|
|
storeAs: "createdChannel",
|
|
},
|
|
),
|
|
channelEdit: type(
|
|
"Editar canal",
|
|
"Canais",
|
|
[
|
|
channel,
|
|
f("name", "Nome (vazio: manter)"),
|
|
f("topic", "Tópico (vazio: manter)"),
|
|
f("description", "Descrição (vazio: manter)", "textarea"),
|
|
f("password", "Password (vazio: manter)", "password"),
|
|
f("params", "Campos adicionais (JSON)", "json"),
|
|
],
|
|
{ channelId: 21, description: "{{text}}", params: {} },
|
|
),
|
|
channelMove: action(
|
|
"Mover canal",
|
|
"Canais",
|
|
"channelmove",
|
|
[
|
|
channel,
|
|
f("parentId", "Novo canal pai (0: raiz)", "channel"),
|
|
f("order", "Depois do canal (0: primeiro)", "channel"),
|
|
],
|
|
{ cid: "channelId", cpid: "parentId", order: "order" },
|
|
{ channelId: 21, parentId: 0, order: 0 },
|
|
),
|
|
channelDelete: action(
|
|
"Apagar canal",
|
|
"Canais",
|
|
"channeldelete",
|
|
[channel, f("force", "Forçar (inclui subcanais)", "select", ["0", "1"])],
|
|
{ cid: "channelId", force: "force" },
|
|
{ channelId: 21, force: "0" },
|
|
),
|
|
groupAdd: action(
|
|
"Adicionar ao grupo",
|
|
"Grupos",
|
|
"servergroupaddclient",
|
|
[dbclient, group],
|
|
{ cldbid: "databaseId", sgid: "groupId" },
|
|
{ databaseId: "{{event.client_database_id}}", groupId: 9 },
|
|
),
|
|
groupRemove: action(
|
|
"Remover do grupo",
|
|
"Grupos",
|
|
"servergroupdelclient",
|
|
[dbclient, group],
|
|
{ cldbid: "databaseId", sgid: "groupId" },
|
|
{ databaseId: "{{event.client_database_id}}", groupId: 9 },
|
|
),
|
|
channelGroupSet: action(
|
|
"Definir grupo de canal",
|
|
"Grupos",
|
|
"setclientchannelgroup",
|
|
[dbclient, channel, f("groupId", "Grupo de canal", "channelGroup")],
|
|
{ cldbid: "databaseId", cid: "channelId", cgid: "groupId" },
|
|
{ databaseId: "{{event.client_database_id}}", channelId: 21, groupId: "" },
|
|
),
|
|
groupClients: action(
|
|
"Clientes de um grupo",
|
|
"Grupos",
|
|
"servergroupclientlist",
|
|
[group, f("storeAs", "Guardar em temp…")],
|
|
{ sgid: "groupId" },
|
|
{ groupId: 9, storeAs: "groupClients" },
|
|
),
|
|
clientGroups: action(
|
|
"Grupos de um cliente",
|
|
"Grupos",
|
|
"servergroupsbyclientid",
|
|
[dbclient, f("storeAs", "Guardar em temp…")],
|
|
{ cldbid: "databaseId" },
|
|
{ databaseId: "{{event.client_database_id}}", storeAs: "clientGroups" },
|
|
),
|
|
serverEdit: type(
|
|
"Editar servidor",
|
|
"Servidor",
|
|
[f("params", "Campos do servidor (JSON)", "json")],
|
|
{ params: {} },
|
|
),
|
|
http: type(
|
|
"Pedido HTTP",
|
|
"Integrações",
|
|
[
|
|
f("url", "Endereço"),
|
|
f("method", "Método", "select", [
|
|
"GET",
|
|
"POST",
|
|
"PUT",
|
|
"PATCH",
|
|
"DELETE",
|
|
]),
|
|
f("headers", "Cabeçalhos (JSON)", "json"),
|
|
f("body", "Corpo", "textarea"),
|
|
f("storeAs", "Guardar resposta em temp…"),
|
|
],
|
|
{
|
|
url: "https://example.com",
|
|
method: "GET",
|
|
headers: {},
|
|
body: "",
|
|
storeAs: "http",
|
|
},
|
|
),
|
|
discord: type(
|
|
"Webhook Discord",
|
|
"Integrações",
|
|
[
|
|
f("url", "URL do webhook", "password"),
|
|
f("text", "Mensagem", "textarea"),
|
|
f("username", "Nome (opcional)"),
|
|
],
|
|
{ url: "", text: "{{text}}", username: "XanaX Botflow" },
|
|
),
|
|
webquery: type(
|
|
"WebQuery avançado",
|
|
"Integrações",
|
|
[
|
|
f("command", "Operação WebQuery", "queryCommand"),
|
|
f("params", "Parâmetros (JSON)", "json"),
|
|
f("storeAs", "Guardar em temp…"),
|
|
],
|
|
{ command: "serverinfo", params: {}, storeAs: "result" },
|
|
),
|
|
};
|
|
for (const [prefix, title, groupType, parameter] of [
|
|
["servergroup", "servidor", "serverGroup", "sgid"],
|
|
["channelgroup", "canal", "channelGroup", "cgid"],
|
|
]) {
|
|
nodeTypes[`${prefix}Create`] = action(
|
|
`Criar grupo de ${title}`,
|
|
"Grupos",
|
|
`${prefix}add`,
|
|
[
|
|
f("name", "Nome"),
|
|
f("type", "Tipo", "select", ["1", "2"]),
|
|
f("storeAs", "Guardar resultado em temp…"),
|
|
],
|
|
{ name: "name", type: "type" },
|
|
{ name: "Novo grupo", type: "1", storeAs: "createdGroup" },
|
|
);
|
|
nodeTypes[`${prefix}Rename`] = action(
|
|
`Renomear grupo de ${title}`,
|
|
"Grupos",
|
|
`${prefix}rename`,
|
|
[f("groupId", `Grupo de ${title}`, groupType), f("name", "Novo nome")],
|
|
{ [parameter]: "groupId", name: "name" },
|
|
{ groupId: 9, name: "Novo nome" },
|
|
);
|
|
nodeTypes[`${prefix}Delete`] = action(
|
|
`Apagar grupo de ${title}`,
|
|
"Grupos",
|
|
`${prefix}del`,
|
|
[
|
|
f("groupId", `Grupo de ${title}`, groupType),
|
|
f("force", "Forçar", "select", ["0", "1"]),
|
|
],
|
|
{ [parameter]: "groupId", force: "force" },
|
|
{ groupId: 9, force: "0" },
|
|
);
|
|
}
|
|
for (const [scope, scopeTitle, idFields, idParams] of [
|
|
["client", "Cliente", [dbclient], { cldbid: "databaseId" }],
|
|
["channel", "Canal", [channel], { cid: "channelId" }],
|
|
["servergroup", "Grupo de servidor", [group], { sgid: "groupId" }],
|
|
[
|
|
"channelgroup",
|
|
"Grupo de canal",
|
|
[f("groupId", "Grupo de canal", "channelGroup")],
|
|
{ cgid: "groupId" },
|
|
],
|
|
[
|
|
"channelclient",
|
|
"Cliente num canal",
|
|
[dbclient, channel],
|
|
{ cldbid: "databaseId", cid: "channelId" },
|
|
],
|
|
]) {
|
|
nodeTypes[`${scope}PermissionSet`] = action(
|
|
`Permissão: ${scopeTitle}`,
|
|
"Permissões",
|
|
`${scope}addperm`,
|
|
[
|
|
...idFields,
|
|
permission,
|
|
f("value", "Valor"),
|
|
...(["client", "servergroup"].includes(scope)
|
|
? [f("skip", "Ignorar grupo de canal (skip)", "select", ["0", "1"])]
|
|
: []),
|
|
...(scope === "servergroup"
|
|
? [f("negated", "Negado (negated)", "select", ["0", "1"])]
|
|
: []),
|
|
],
|
|
{
|
|
...idParams,
|
|
permsid: "permission",
|
|
permvalue: "value",
|
|
permskip: "skip",
|
|
permnegated: "negated",
|
|
},
|
|
{
|
|
databaseId: "{{event.client_database_id}}",
|
|
channelId: 21,
|
|
groupId: 9,
|
|
permission: "i_client_talk_power",
|
|
value: "0",
|
|
skip: "0",
|
|
negated: "0",
|
|
},
|
|
);
|
|
nodeTypes[`${scope}PermissionRemove`] = action(
|
|
`Remover permissão: ${scopeTitle}`,
|
|
"Permissões",
|
|
`${scope}delperm`,
|
|
[...idFields, permission],
|
|
{ ...idParams, permsid: "permission" },
|
|
{
|
|
databaseId: "{{event.client_database_id}}",
|
|
channelId: 21,
|
|
groupId: 9,
|
|
permission: "i_client_talk_power",
|
|
},
|
|
);
|
|
nodeTypes[`${scope}PermissionList`] = action(
|
|
`Consultar permissões: ${scopeTitle}`,
|
|
"Permissões",
|
|
`${scope}permlist`,
|
|
[...idFields, f("storeAs", "Guardar em temp…")],
|
|
idParams,
|
|
{
|
|
databaseId: "{{event.client_database_id}}",
|
|
channelId: 21,
|
|
groupId: 9,
|
|
storeAs: "permissions",
|
|
},
|
|
);
|
|
}
|
|
export const queryCommands = [
|
|
...new Set([
|
|
"serverinfo",
|
|
"serverlist",
|
|
"hostinfo",
|
|
"whoami",
|
|
"version",
|
|
"clientgetdbidfromuid",
|
|
"clientgetids",
|
|
"clientgetnamefromdbid",
|
|
"clientfind",
|
|
"channelfind",
|
|
"banlist",
|
|
"banadd",
|
|
"tokenlist",
|
|
"tokenadd",
|
|
"tokendelete",
|
|
"messageadd",
|
|
"messagelist",
|
|
"messageget",
|
|
"messagedel",
|
|
"complainlist",
|
|
"complainadd",
|
|
"complaindel",
|
|
"logview",
|
|
"connectioninfo",
|
|
...Object.values(nodeTypes)
|
|
.map((t) => t.command)
|
|
.filter(Boolean),
|
|
"channeledit",
|
|
"channelcreate",
|
|
"serveredit",
|
|
"servergroupcopy",
|
|
"channelgroupcopy",
|
|
"clientedit",
|
|
"sendtextmessage",
|
|
"clientupdate",
|
|
]),
|
|
].sort();
|
|
export function validateGraph(graph) {
|
|
if (
|
|
!graph ||
|
|
!Array.isArray(graph.nodes) ||
|
|
!Array.isArray(graph.edges) ||
|
|
graph.nodes.length > 150 ||
|
|
graph.edges.length > 400
|
|
)
|
|
throw new Error("Usa até 150 blocos e 400 ligações.");
|
|
const ids = new Map();
|
|
for (const n of graph.nodes) {
|
|
if (
|
|
!nodeTypes[n.type] ||
|
|
typeof n.id !== "string" ||
|
|
!/^[\w-]{1,100}$/.test(n.id) ||
|
|
ids.has(n.id) ||
|
|
!Number.isFinite(n.x) ||
|
|
!Number.isFinite(n.y) ||
|
|
!n.config ||
|
|
typeof n.config !== "object" ||
|
|
Array.isArray(n.config)
|
|
)
|
|
throw new Error("Bloco inválido no flow.");
|
|
ids.set(n.id, n);
|
|
n.config = { ...structuredClone(nodeTypes[n.type].defaults), ...n.config };
|
|
for (const [key, label, fieldType] of nodeTypes[n.type].fields) {
|
|
if (fieldType !== "json" || n.config[key] == null) continue;
|
|
let value = n.config[key];
|
|
if (typeof value === "string") {
|
|
try {
|
|
value = JSON.parse(value);
|
|
} catch {
|
|
throw new Error(
|
|
`${n.label || nodeTypes[n.type].title}: JSON inválido em ${label}.`,
|
|
);
|
|
}
|
|
}
|
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
throw new Error(`${label}: usa um objeto JSON.`);
|
|
n.config[key] = value;
|
|
}
|
|
for (const [parameter, key] of Object.entries(
|
|
nodeTypes[n.type].params || {},
|
|
)) {
|
|
if (
|
|
!["clid", "cldbid", "cid", "sgid", "cgid", "banid"].includes(parameter)
|
|
)
|
|
continue;
|
|
const value = n.config[key];
|
|
if (typeof value === "string" && /\{\{[^{}]+\}\}/.test(value)) continue;
|
|
if (!Number.isInteger(Number(value)) || Number(value) < 1)
|
|
throw new Error(
|
|
`${n.label || nodeTypes[n.type].title}: configura ${key} com um ID ou variável.`,
|
|
);
|
|
}
|
|
if (
|
|
n.type === "webquery" &&
|
|
!/\{\{/.test(n.config.command) &&
|
|
!queryCommands.includes(n.config.command)
|
|
)
|
|
throw new Error("Operação WebQuery não suportada.");
|
|
if (
|
|
n.type === "timer" &&
|
|
(!Number.isFinite(Number(n.config.seconds)) ||
|
|
Number(n.config.seconds) < 1)
|
|
)
|
|
throw new Error("O intervalo mínimo é 1 segundo.");
|
|
if (
|
|
n.type === "delay" &&
|
|
(!Number.isFinite(Number(n.config.seconds)) ||
|
|
Number(n.config.seconds) < 0 ||
|
|
Number(n.config.seconds) > 300)
|
|
)
|
|
throw new Error("A espera máxima é 300 segundos.");
|
|
if (
|
|
n.type === "limit" &&
|
|
(!Number.isInteger(Number(n.config.count)) ||
|
|
Number(n.config.count) < 1 ||
|
|
Number(n.config.count) > 1000)
|
|
)
|
|
throw new Error("O limite precisa de ser entre 1 e 1000.");
|
|
if (
|
|
n.type === "foreach" &&
|
|
(!Number.isInteger(Number(n.config.limit)) ||
|
|
n.config.limit < 1 ||
|
|
n.config.limit > 1000)
|
|
)
|
|
throw new Error("Limite de repetição inválido.");
|
|
if (
|
|
n.type === "random" &&
|
|
(!Number.isInteger(Number(n.config.length)) ||
|
|
n.config.length < 1 ||
|
|
n.config.length > 128)
|
|
)
|
|
throw new Error("O código precisa de ter entre 1 e 128 caracteres.");
|
|
if (n.type === "webhook" && String(n.config.secret || "").length < 16)
|
|
throw new Error(
|
|
"Usa um segredo de webhook com pelo menos 16 caracteres.",
|
|
);
|
|
if (n.type === "command" && !/^\S{1,64}$/.test(n.config.command || ""))
|
|
throw new Error("Indica um comando de chat sem espaços.");
|
|
if (
|
|
n.config.retries != null &&
|
|
(!Number.isInteger(Number(n.config.retries)) ||
|
|
n.config.retries < 0 ||
|
|
n.config.retries > 3)
|
|
)
|
|
throw new Error("São permitidas até 3 tentativas adicionais.");
|
|
}
|
|
if (!graph.nodes.some((n) => nodeTypes[n.type].trigger))
|
|
throw new Error("Adiciona pelo menos um bloco de início.");
|
|
const seenEdges = new Set();
|
|
for (const e of graph.edges) {
|
|
const source = ids.get(e.from),
|
|
target = ids.get(e.to),
|
|
key = `${e.from}:${e.port}:${e.to}`;
|
|
if (
|
|
!source ||
|
|
!target ||
|
|
source === target ||
|
|
nodeTypes[target.type].trigger ||
|
|
!nodeTypes[source.type].outputs.includes(e.port) ||
|
|
seenEdges.has(key)
|
|
)
|
|
throw new Error("Ligação inválida no flow.");
|
|
seenEdges.add(key);
|
|
}
|
|
const visiting = new Set(),
|
|
visited = new Set();
|
|
function visit(id) {
|
|
if (visiting.has(id))
|
|
throw new Error(
|
|
"O flow contém um ciclo. Usa Para cada item ou um temporizador para repetir.",
|
|
);
|
|
if (visited.has(id)) return;
|
|
visiting.add(id);
|
|
for (const e of graph.edges.filter((e) => e.from === id)) visit(e.to);
|
|
visiting.delete(id);
|
|
visited.add(id);
|
|
}
|
|
for (const id of ids.keys()) visit(id);
|
|
return graph;
|
|
}
|