{ "nodes": [ { "parameters": { "httpMethod": "POST", "path": "device-relay", "options": {} }, "id": "f6be6a6e-93e1-4150-a911-486160aebc8e", "name": "Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ 1056, 1312 ], "webhookId": "fe4a40f7-acfa-4380-99d1-72c4b7c7ca34" }, { "parameters": {}, "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ 1600, 1472 ], "id": "0b345f57-e545-4ab5-8d3c-81f63c7764df", "name": "Merge" }, { "parameters": { "mode": "raw", "jsonOutput": "{}", "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 1424, 1472 ], "id": "109dfc7e-e89e-4098-9a31-2597fb3a0761", "name": "Config" }, { "parameters": { "jsCode": "// n8n Code node (Run Once for All Items)\n\n// Toggle this to silence logs quickly\nconst DEBUG = true;\n\nconst log = (...args) => {\n if (DEBUG) console.log(\"[debug]\", ...args);\n};\n\nconst redact = (value) => {\n const SECRET_KEY_RE = /(token|secret|password|apiKey|key)/i;\n\n const walk = (v) => {\n if (v == null) return v;\n if (Array.isArray(v)) return v.map(walk);\n\n if (typeof v === \"object\") {\n const out = {};\n for (const [k, child] of Object.entries(v)) {\n if (SECRET_KEY_RE.test(k)) out[k] = \"***REDACTED***\";\n else out[k] = walk(child);\n }\n return out;\n }\n\n return v;\n };\n\n return walk(value);\n};\n\nlog(\"Input item count:\", $input.all().length);\n\nconst wrapper = $input.first().json;\nlog(\"Wrapper keys:\", Object.keys(wrapper ?? {}));\n\nconst config = wrapper.config;\nconst event = wrapper.event;\nconst recipientsResolved = wrapper.recipientsResolved;\n\nlog(\"Has config?\", !!config, \"Has event?\", !!event);\nlog(\"Config (redacted):\", redact(config));\nlog(\"Event keys:\", Object.keys(event ?? {}));\nlog(\"Event.body keys:\", Object.keys(event?.body ?? {}));\n\nif (!config) throw new Error(\"Missing config\");\nif (!event) throw new Error(\"Missing event\");\nif (!recipientsResolved) throw new Error(\"Missing RecipientsResolved\");\nif (!config?.recipients) throw new Error(\"Missing config.recipients (ConfigModel)\");\nif (!config?.endpoints) throw new Error(\"Missing config.endpoints (ConfigModel)\");\nif (!config?.templates) throw new Error(\"Missing config.templates (ConfigModel)\");\nif (!event?.body?.data?.type)\n throw new Error(\"Missing event.body.data.type (InboundDataModel discriminator)\");\nif (!event?.body?.device.id) throw new Error(\"Missing event.body.device.id\");\nif (!event?.body?.id) throw new Error(\"Missing event.body.id (eventId)\");\n\nconst deviceId = event.body.device.id;\nconst dataType = event.body.data.type;\nconst eventId = event.body.id;\n\nlog(\"deviceId:\", deviceId);\nlog(\"dataType:\", dataType);\nlog(\"eventId:\", eventId);\n\n// meta is what templates typically refer to as {meta.*}\nconst meta = {\n id: event.body.id,\n createdAt: event.body.createdAt,\n deviceId: event.body.deviceId,\n source: event.body.source,\n ...(event.body.meta ?? {}),\n};\nlog(\"meta:\", meta);\n\nconst ensureAllExist = (kind, ids, registry) => {\n for (const id of ids) {\n if (!registry[id]) {\n throw new Error(`Unknown ${kind} id \"${id}\" referenced by routing logic`);\n }\n }\n};\n\n// Build OutboundDispatchPlan from config + event\nconst dispatches = [];\n/**\n * @type {Array}\n */\nconst recipients = recipientsResolved;\n\nlog(\"recipient ids:\", recipients.map((r) => r.id).flat().join(\", \"));\n\nfor (const recipient of recipientsResolved) {\n const recipientId = recipient.id;\n\n if (!Array.isArray(recipient.rules)) continue;\n\n for (const rule of recipient.rules) {\n const endpointIds = rule.then?.endpointIds ?? [];\n const templateIds = rule.then?.templateIds ?? [];\n\n if (endpointIds.length === 0 || templateIds.length === 0) continue;\n\n ensureAllExist(\"endpoint\", endpointIds, config.endpoints);\n ensureAllExist(\"template\", templateIds, config.templates);\n\n for (const endpointId of endpointIds) {\n for (const templateId of templateIds) {\n dispatches.push({\n id: `${eventId}:${recipientId}:${rule.id}:${endpointId}:${templateId}`,\n recipientId,\n ruleId: rule.id,\n endpointId,\n templateId,\n matchedRuleIds: [rule.id],\n });\n }\n }\n }\n}\n\nconst outbound = {\n eventId,\n deviceId,\n dataType,\n configVersion: config.version,\n dispatches,\n};\n\nlog(\"dispatches count:\", dispatches.length);\nlog(\"outbound:\", redact(outbound));\n\n// Return RuntimeModel\nreturn [\n {\n json: {\n event,\n config,\n outbound,\n },\n },\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1920, 2240 ], "id": "08579bae-485b-4ddf-a089-e04f00a57668", "name": "Form outbound", "alwaysOutputData": false }, { "parameters": { "jsCode": "// n8n Code node (Run Once for All Items)\n\n// Toggle this to silence logs quickly\nconst DEBUG = true;\n\nconst log = (...args) => {\n if (DEBUG) console.log(\"[debug]\", ...args);\n};\n\nconst redact = (value) => {\n const SECRET_KEY_RE = /(token|secret|password|apiKey|key)/i;\n\n const walk = (v) => {\n if (v == null) return v;\n if (Array.isArray(v)) return v.map(walk);\n\n if (typeof v === \"object\") {\n const out = {};\n for (const [k, child] of Object.entries(v)) {\n if (SECRET_KEY_RE.test(k)) out[k] = \"***REDACTED***\";\n else out[k] = walk(child);\n }\n return out;\n }\n\n return v;\n };\n\n return walk(value);\n};\n\nconst wrapper = $input.first().json;\n\nconst config = wrapper.config;\nconst event = wrapper.event;\nconst outbound = wrapper.outbound;\n\nlog(\"Has config?\", !!config, \"Has event?\", !!event, \"Has outbound?\", !!outbound);\nlog(\"Outbound (redacted):\", redact(outbound));\n\nif (!config) throw new Error(\"Missing config\");\nif (!event) throw new Error(\"Missing event\");\nif (!outbound) throw new Error(\"Missing outbound\");\nif (!config?.recipients) throw new Error(\"Missing config.recipients (ConfigModel)\");\nif (!config?.endpoints) throw new Error(\"Missing config.endpoints (ConfigModel)\");\nif (!config?.templates) throw new Error(\"Missing config.templates (ConfigModel)\");\nif (!Array.isArray(outbound?.dispatches)) throw new Error(\"Missing outbound.dispatches\");\nif (!event?.body?.data?.type) throw new Error(\"Missing event.body.data.type\");\nif (!event?.body?.device.id) throw new Error(\"Missing event.body.device.id\");\nif (!event?.body?.id) throw new Error(\"Missing event.body.id\");\n\nconst meta = {\n id: event.body.id,\n createdAt: event.body.createdAt,\n deviceId: event.body.device.id,\n source: event.body.source,\n ...(event.body.meta ?? {}),\n};\n\nconst mergeHeaders = (base, override) => ({\n ...(base || {}),\n ...(override || {}),\n});\n\nconst ensure = (kind, id, value) => {\n if (!value) throw new Error(`Unknown ${kind} id \"${id}\"`);\n return value;\n};\n\nconst defaultTimeoutMs = config?.defaults?.httpTimeoutMs;\nconst defaultHeaders = config?.defaults?.headers;\n\nlog(\"dispatch count:\", outbound.dispatches.length);\n\nconst items = outbound.dispatches.map((dispatch) => {\n const recipientId = dispatch.recipientId;\n const endpointId = dispatch.endpointId;\n const templateId = dispatch.templateId;\n\n const recipient = ensure(\"recipient\", recipientId, config.recipients[recipientId]);\n const endpoint = ensure(\"endpoint\", endpointId, config.endpoints[endpointId]);\n const template = ensure(\"template\", templateId, config.templates[templateId]);\n\n // IMPORTANT: no templating here. Leave placeholders as-is.\n // This node only adds metadata for later templating nodes.\n if (endpoint?.body?.type && endpoint.body.type !== \"json\") {\n throw new Error(\n `Endpoint \"${endpointId}\" body.type=\"${endpoint.body.type}\" is not supported by this node (expects \"json\")`\n );\n }\n\n return {\n json: {\n event,\n config,\n // metadata for later nodes that will do templating/rendering\n templateData: {\n meta, // typical {meta.*} namespace\n data: event.body.data, // typical {data.*} namespace\n device: event.body.device, // typical {data.*} namespace\n recipient: {\n id: recipientId,\n vars: recipient.vars, // typical {recipient.vars.*} namespace\n }\n },\n toTemplate: {\n template: {\n id: templateId,\n text: template.text, // where `{message}` (or similar) should come from later\n format: template.format,\n fields: template.fields,\n },\n endpoint: endpoint\n },\n },\n };\n});\n\nlog(\"Prepared items:\", items.length);\nlog(\"First item preview (redacted):\", redact(items[0]?.json));\n\nreturn items;" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2128, 2240 ], "id": "de659302-cee7-4b1e-849d-7b1c602a5e7f", "name": "Compose messages", "alwaysOutputData": false }, { "parameters": { "jsCode": "// n8n Code node (Run Once for All Items)\n\n// Toggle this to silence logs quickly\nconst DEBUG = true;\n\nconst log = (...args) => {\n if (DEBUG) console.log('[debug]', ...args);\n};\n\nconst redact = (value) => {\n const SECRET_KEY_RE = /(token|secret|password|apiKey|key)/i;\n\n const walk = (v, path = '') => {\n if (v == null) return v;\n\n if (Array.isArray(v)) return v.map((x, i) => walk(x, `${path}[${i}]`));\n\n if (typeof v === 'object') {\n const out = {};\n for (const [k, child] of Object.entries(v)) {\n const full = path ? `${path}.${k}` : k;\n if (SECRET_KEY_RE.test(k)) out[k] = '***REDACTED***';\n else out[k] = walk(child, full);\n }\n return out;\n }\n\n return v;\n };\n\n return walk(value);\n};\n\nlog('Input item count:', $input.all().length);\n\nconst wrapper = $input.first().json;\nlog('Wrapper keys:', Object.keys(wrapper ?? {}));\n\n// Allow common shapes: {config, event}, {config, body}, or direct webhook body\nconst config = wrapper.config;\nconst event = wrapper.event;\nconst recipientsFilteredGlobally = wrapper.recipientsFilteredGlobally;\n\nlog('Has config?', !!config, 'Has event?', !!event);\nlog('Config (redacted):', redact(config));\nlog('Event keys:', Object.keys(event ?? {}));\nlog('Event.body keys:', Object.keys(event?.body ?? {}));\n\nif (!recipientsFilteredGlobally) throw new Error('Missing recipientsFilteredGlobally');\nif (!config?.recipients) throw new Error('Missing config.recipients (ConfigModel)');\nif (!event?.body?.data?.type) throw new Error('Missing event.body.data.type (IncomingDataModel discriminator)');\nif (!event?.body?.device.id) throw new Error('Missing event.body.device.id');\n\nconst deviceId = event.body.device.id;\nconst dataType = event.body.data.type;\n\nlog('deviceId:', deviceId);\nlog('dataType:', dataType);\n\n// meta is what templates typically refer to as {meta.*}\nconst meta = {\n id: event.body.id,\n createdAt: event.body.createdAt,\n deviceId: event.body.device.id,\n source: event.body.source,\n ...(event.body.meta ?? {}),\n};\nlog('meta:', meta);\n\nconst isPresent = (v) => v !== undefined && v !== null;\n\n/**\n * If the value is a string, convert it to lowercase.\n * Otherwise, return the value as is.\n * @param {unknown} v - The value to be checked and potentially converted.\n * @returns {unknown | string} - The lowercase string or the original value.\n */\nconst normalizeIfString = (v) => {\n if (typeof v === 'string') {\n return v.trim().toLowerCase();\n }\n return v;\n}\n\n/**\n * Convert a value to a comparable number if possible.\n * @param {unknown} v - The value to be converted.\n * @returns {number | undefined} - The comparable number or undefined if conversion is not possible.\n */\nconst toComparableNumber = (v) => {\n if (typeof v === 'number' && Number.isFinite(v)) return v;\n if (typeof v === 'string' && normalizeIfString(v) !== '') {\n const n = Number(v);\n return Number.isFinite(n) ? n : undefined;\n }\n return undefined;\n};\n\nconst normalizeEquals = (actual, expected) => {\n log('normalizeEquals:', { actual, expected, actualType: typeof actual, expectedType: typeof expected });\n\n if (!isPresent(actual) && !isPresent(expected)) return true;\n\n if (typeof actual === 'boolean') {\n if (typeof expected === 'boolean') return actual === expected;\n if (typeof expected === 'string') {\n const s = normalizeIfString(expected);\n if (s === 'true') return actual === true;\n if (s === 'false') return actual === false;\n }\n return false;\n }\n\n if (typeof actual === 'number') {\n const n = toComparableNumber(expected);\n return n !== undefined ? actual === n : false;\n }\n\n return normalizeIfString(String(actual)) === normalizeIfString(String(expected));\n};\n\nconst getByPath = (root, path) => {\n const parts = (path ? String(path) : '')\n .split('.')\n .filter(Boolean);\n\n let cur = root;\n for (const part of parts) {\n if (cur == null) return undefined;\n cur = cur[part];\n }\n return cur;\n};\n\n// Uses \"operator\" (your current my.json)\nconst predicateMatches = (predicate) => {\n const op = predicate?.operator;\n const path = predicate?.path;\n const expected = predicate?.value;\n\n // Root lets your rules use paths like \"deviceId\" or \"data.body\" or \"meta.device.id\"\n const predicateRoot = { ...event.body, data: event.body.data, meta };\n const actual = getByPath(predicateRoot, path);\n\n log('predicate:', { path, op, expected, actual });\n\n switch (op) {\n case 'equals':\n return normalizeEquals(actual, expected);\n\n case 'not_equals':\n return !normalizeEquals(actual, expected);\n\n case 'includes': {\n if (!isPresent(actual)) return false;\n if (Array.isArray(actual)) return actual.includes(expected);\n return normalizeIfString(String(actual)).includes(normalizeIfString(String(expected ?? '')));\n }\n\n case 'not_includes': {\n if (!isPresent(actual)) return true;\n if (Array.isArray(actual)) return !actual.includes(expected);\n return !normalizeIfString(String(actual)).includes(normalizeIfString(String(expected ?? '')));\n }\n\n case 'starts_with':\n return isPresent(actual) && normalizeIfString(String(actual)).startsWith(normalizeIfString(String(expected ?? '')));\n\n case 'ends_with':\n return isPresent(actual) && normalizeIfString(String(actual)).endsWith(normalizeIfString(String(expected ?? '')));\n\n case 'matches_regex': {\n if (!isPresent(actual)) return false;\n try {\n const re = new RegExp(String(expected ?? ''));\n return re.test(String(actual));\n } catch {\n throw new Error(`Invalid regex in predicate for path \"${path}\": ${String(expected)}`);\n }\n }\n\n case 'gt':\n case 'gte':\n case 'lt':\n case 'lte': {\n const a = toComparableNumber(actual);\n const b = toComparableNumber(expected);\n log('numeric compare:', { op, a, b });\n if (a === undefined || b === undefined) return false;\n if (op === 'gt') return a > b;\n if (op === 'gte') return a >= b;\n if (op === 'lt') return a < b;\n return a <= b;\n }\n\n case 'exists':\n return isPresent(actual);\n\n default:\n throw new Error(`Unsupported predicate operator: ${op}`);\n }\n};\n\nconst resolveRecipient = (recipientId, recipient) => {\n log('--- resolveRecipient ---', recipientId);\n log('recipient (redacted):', redact(recipient));\n\n const allowedDeviceIds = Array.isArray(recipient.allowedDeviceIds) ? recipient.allowedDeviceIds : [];\n log('allowedDeviceIds:', allowedDeviceIds);\n if (allowedDeviceIds.length > 0 && !allowedDeviceIds.includes(deviceId)) {\n log('SKIP: deviceId not allowed', { deviceId, allowedDeviceIds });\n return null;\n }\n\n const allowedDataTypes = Array.isArray(recipient.allowedDataTypes) ? recipient.allowedDataTypes : [];\n log('allowedDataTypes:', allowedDataTypes);\n if (allowedDataTypes.length > 0 && !allowedDataTypes.includes(dataType)) {\n log('SKIP: dataType not allowed', { dataType, allowedDataTypes });\n return null;\n }\n\n const baseEndpointIds = Array.isArray(recipient.endpointIds) ? recipient.endpointIds : [];\n const baseTemplateIds = Array.isArray(recipient.templateIds) ? recipient.templateIds : [];\n log('baseEndpointIds:', baseEndpointIds);\n log('baseTemplateIds:', baseTemplateIds);\n\n const rules = Array.isArray(recipient.rules) ? recipient.rules : [];\n log('rule count:', rules.length);\n\n if (rules.length === 0) {\n log('NO RULES: selecting recipient with base routing');\n return { id: recipientId, ...recipient, endpointIds: baseEndpointIds, templateIds: baseTemplateIds };\n }\n\n const matchedRules = [];\n\n for (const rule of rules) {\n log('checking rule:', { ruleId: rule?.id, type: rule?.type });\n\n if (rule?.type !== dataType) {\n log('rule type mismatch, skip:', { ruleType: rule?.type, dataType });\n continue;\n }\n\n const whens = Array.isArray(rule.when) ? rule.when : [];\n log('when predicates:', whens);\n\n const matched = whens.every((p, idx) => {\n const ok = predicateMatches(p);\n log(`predicate[${idx}] result:`, ok);\n return ok;\n });\n\n log('rule matched?', matched, 'ruleId:', rule?.id);\n\n if (!matched) continue;\n\n // IMPORTANT: keep track of matches (you were missing this)\n matchedRules.push(rule);\n }\n\n log('matchedRules ids:', matchedRules.map((r) => r.id));\n\n if (matchedRules.length === 0) {\n log('NO MATCHED RULES: dropping recipient');\n return null;\n }\n\n return { \n id: recipientId,\n vars: recipient.vars,\n rules: matchedRules.map((r) => {\n return { id: r.id, then: r.then };\n }),\n };\n};\n\nlog('recipient ids:', recipientsFilteredGlobally.join(\", \"));\n\nconst resolvedRecipients = recipientsFilteredGlobally\n .map((id) => resolveRecipient(id, config.recipients?.[id]))\n .filter(Boolean);\n\nlog('resolvedRecipients count:', resolvedRecipients.length);\nlog('resolvedRecipients (redacted):', redact(resolvedRecipients));\n\nreturn [\n {\n json: {\n event,\n config,\n recipientsResolved: resolvedRecipients\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1504, 2240 ], "id": "50c95128-d942-45d6-8678-ac9c37ba2f84", "name": "Filter recipients by recipient rules", "alwaysOutputData": false }, { "parameters": { "jsCode": "// n8n Code node (Run Once for All Items)\n\n// Toggle this to silence logs quickly\nconst DEBUG = true;\n\nconst log = (...args) => {\n if (DEBUG) console.log(\"[debug]\", ...args);\n};\n\nconst redact = (value) => {\n const SECRET_KEY_RE = /(token|secret|password|apiKey|key)/i;\n\n const walk = (v) => {\n if (v == null) return v;\n if (Array.isArray(v)) return v.map(walk);\n\n if (typeof v === \"object\") {\n const out = {};\n for (const [k, child] of Object.entries(v)) {\n if (SECRET_KEY_RE.test(k)) out[k] = \"***REDACTED***\";\n else out[k] = walk(child);\n }\n return out;\n }\n return v;\n };\n\n return walk(value);\n};\n\nconst getByPath = (root, path) => {\n if (!path || typeof path !== \"string\") return undefined;\n\n // supports simple dot paths like: meta.createdAt, data.body, recipient.vars.botToken\n const parts = path.split(\".\").filter(Boolean);\n let cur = root;\n\n for (const part of parts) {\n if (cur == null) return undefined;\n cur = cur[part];\n }\n\n return cur;\n};\n\nconst templateString = (input, templateData, renderedText) => {\n if (typeof input !== \"string\") return input;\n\n return input.replace(/\\{([^{}]+)\\}/g, (match, rawToken) => {\n const token = String(rawToken).trim();\n\n // Special placeholder: {%text%} => already-rendered template.text\n if (token === \"%text%\") return String(renderedText ?? \"\");\n\n const value = getByPath(templateData, token);\n\n // If missing, keep placeholder as-is (non-strict)\n if (value === undefined || value === null) return match;\n\n // For non-primitives, stringify to avoid \"[object Object]\"\n if (typeof value === \"object\") return JSON.stringify(value);\n\n return String(value);\n });\n};\n\nconst templateDeepOnlyStrings = (value, templateData, renderedText) => {\n if (typeof value === \"string\") return templateString(value, templateData, renderedText);\n\n if (Array.isArray(value)) {\n return value.map((v) => templateDeepOnlyStrings(v, templateData, renderedText));\n }\n\n if (value && typeof value === \"object\") {\n const out = {};\n for (const [k, v] of Object.entries(value)) {\n out[k] = templateDeepOnlyStrings(v, templateData, renderedText);\n }\n return out;\n }\n\n return value;\n};\n\nconst items = $input.all().map((item) => {\n const wrapper = item.json;\n \n const config = wrapper.config;\n const event = wrapper.event;\n\n const templateData = wrapper.templateData;\n const toTemplate = wrapper.toTemplate;\n\n if (!templateData) throw new Error(\"Missing templateData\");\n if (!toTemplate) throw new Error(\"Missing toTemplate\");\n if (!toTemplate?.template?.text) throw new Error(\"Missing toTemplate.template.text\");\n if (!toTemplate?.endpoint) throw new Error(\"Missing toTemplate.endpoint\");\n\n // 1) Render template.text first (only uses templateData)\n const renderedText = templateString(toTemplate.template.text, templateData, \"\");\n\n // 2) Template ONLY within toTemplate (strings only), using templateData,\n // with {%text%} resolved from the rendered template.text above.\n const templatedEndpoint = templateDeepOnlyStrings(\n toTemplate.endpoint,\n templateData,\n renderedText\n );\n\n // 3) Build \"toSend\" as a flattened, templated endpoint (no nesting)\n // Keep everything else as-is; just add toSend.\n const toSend = {\n method: templatedEndpoint.method,\n url: templatedEndpoint.url,\n headers: templatedEndpoint.headers ?? {},\n searchParams: templatedEndpoint.searchParams ?? {},\n timeoutMs: templatedEndpoint.timeoutMs,\n body:\n templatedEndpoint?.body?.type === \"json\"\n ? (templatedEndpoint.body?.content ?? {})\n : templatedEndpoint?.body?.content ?? templatedEndpoint?.body,\n };\n\n log(\"Rendered text preview:\", renderedText);\n log(\"toSend (redacted):\", redact(toSend));\n\n return {\n json: {\n event,\n config,\n toSend,\n },\n };\n});\n\nreturn items;" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2336, 2240 ], "id": "019d5da3-e44a-4192-8748-6edca4d23b71", "name": "Template messages", "alwaysOutputData": false }, { "parameters": { "jsCode": "// n8n Code node (Run Once for All Items)\n// Filters recipients by allowedDeviceIds / allowedDataTypes ONLY when those props exist.\n\nconst wrapper = $input.first().json;\nconst config = wrapper.config;\nconst event = wrapper.event;\n\nif (!config?.recipients) throw new Error('Missing config.recipients');\nif (!event?.body?.device?.id) throw new Error('Missing event.body.device.id');\nif (!event?.body?.data?.type) throw new Error('Missing event.body.data.type');\n\nconst deviceId = event.body.device.id;\nconst dataType = event.body.data.type;\n\n\nconst isNotDisabled = (recipient) => {\n if (recipient.disabled === true) return false;\n return true;\n};\n\nconst isAllowed = (recipient) => {\n // If prop is missing/null => do NOT filter by it (recipient remains)\n if (Array.isArray(recipient.allowedDeviceIds) && recipient.allowedDeviceIds.length > 0) {\n if (!recipient.allowedDeviceIds.includes(deviceId)) return false;\n }\n\n if (Array.isArray(recipient.allowedDataTypes) && recipient.allowedDataTypes.length > 0) {\n if (!recipient.allowedDataTypes.includes(dataType)) return false;\n }\n\n return true;\n};\n\nconst out = Object.entries(config.recipients)\n .filter(([, recipient]) => isNotDisabled(recipient))\n .filter(([, recipient]) => isAllowed(recipient))\n .map(([id, recipient]) => (id));\n\nreturn [\n {\n json: {\n event,\n config,\n recipientsFilteredGlobally: out\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1296, 2240 ], "id": "8aa9e847-e4d7-4709-bbe6-47f44e367266", "name": "Filter recipients by DeviceId and DataType filters" }, { "parameters": { "jsCode": "// n8n Code node (Run Once for All Items)\n\n// Toggle this to silence logs quickly\nconst DEBUG = true;\n\nconst log = (...args) => {\n if (DEBUG) console.log(\"[debug]\", ...args);\n};\n\nconst redact = (value) => {\n const SECRET_KEY_RE = /(token|secret|password|apiKey|key)/i;\n\n const walk = (v) => {\n if (v == null) return v;\n if (Array.isArray(v)) return v.map(walk);\n\n if (typeof v === \"object\") {\n const out = {};\n for (const [k, child] of Object.entries(v)) {\n if (SECRET_KEY_RE.test(k)) out[k] = \"***REDACTED***\";\n else out[k] = walk(child);\n }\n return out;\n }\n\n return v;\n };\n\n return walk(value);\n};\n\nlog(\"Input item count:\", $input.all().length);\n\nconst wrapper = $input.first().json;\nlog(\"Wrapper keys:\", Object.keys(wrapper ?? {}));\n\nconst config = wrapper.config;\nconst event = wrapper.event;\nconst recipientsResolved = wrapper.recipientsResolved;\n\nlog(\"Has config?\", !!config, \"Has event?\", !!event);\nlog(\"Config (redacted):\", redact(config));\nlog(\"Event keys:\", Object.keys(event ?? {}));\nlog(\"Event.body keys:\", Object.keys(event?.body ?? {}));\n\nif (!config) throw new Error(\"Missing config\");\nif (!event) throw new Error(\"Missing event\");\nif (!recipientsResolved) throw new Error(\"Missing RecipientsResolved\");\n\n// meta is what templates typically refer to as {meta.*}\nconst meta = {\n id: event.body.id,\n createdAt: event.body.createdAt,\n deviceId: event.body.device.id,\n source: event.body.source,\n ...(event.body.meta ?? {}),\n};\nlog(\"meta:\", meta);\n\n/**\n * @type {Array}\n */\nconst recipients = recipientsResolved;\n\nlog(\"recipient ids:\", recipients.map((r) => r.id).flat().join(\", \"));\n\nfor (const recipient of recipientsResolved) {\n\n const rulesFiltered = [...recipient.rules];\n\n /**\n * @type {Array}\n */\n const recipientEndpointIds = config.recipients[recipient.id]?.endpointIds ?? [];\n /**\n * @type {Array}\n */\n const recipientTemplateIds = config.recipients[recipient.id]?.templateIds ?? [];\n for (const rule of recipient.rules) {\n // filter rules by recipient.endpointIds (if any)\n /**\n * @type {Array}\n */\n const ruleEndpointIds = [...rule.then.endpointIds];\n\n if (recipientEndpointIds.length > 0 && ruleEndpointIds.length > 0) {\n if (recipientEndpointIds.some(item => ruleEndpointIds.includes(item))) {\n rule.then.endpointIds = ruleEndpointIds.filter(id => recipientEndpointIds.includes(id));\n log(`Found matching endpoints for rule ${rule.id}:`, rule.then.endpointIds);\n } else {\n const index = rulesFiltered.findIndex(r => r.id === rule.id);\n if (index > -1) {\n log(`No matching endpoints for rule ${rule.id}, removing it`);\n rulesFiltered.splice(index, 1);\n }\n }\n }\n\n // leave only unique\n rule.then.endpointIds.splice(0, rule.then.endpointIds.length, ...new Set(rule.then.endpointIds));\n \n // filter rules by recipient.templateIds (if any)\n /**\n * @type {Array}\n */\n const ruleTemplateIds = [...rule.then.templateIds];\n\n if (recipientTemplateIds.length > 0 && ruleTemplateIds.length > 0) {\n if (recipientTemplateIds.some(item => ruleTemplateIds.includes(item))) {\n rule.then.templateIds = ruleTemplateIds.filter(id => recipientTemplateIds.includes(id));\n log(`Found matching templates for rule ${rule.id}:`, rule.then.templateIds);\n } else {\n const index = rulesFiltered.findIndex(r => r.id === rule.id);\n if (index > -1) {\n log(`No matching templates for rule ${rule.id}, removing it`);\n rulesFiltered.splice(index, 1);\n }\n }\n }\n\n // leave only unique\n rule.then.templateIds.splice(0, rule.then.templateIds.length, ...new Set(rule.then.templateIds));\n\n log(`Recipient ${recipient.id} filtered rules:`, recipient.rules.length, \"->\", rulesFiltered.length);\n\n recipient.rules = rulesFiltered;\n }\n}\n\n// Return RuntimeModel\nreturn [\n {\n json: {\n event,\n config,\n recipientsResolved\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1712, 2240 ], "id": "dc545cfe-c7de-4f30-9b5a-419a5bcbd90a", "name": "Filter rules by recipients endpointIds and templateIds", "alwaysOutputData": false }, { "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "leftValue": "={{ $json.toSend.method }}", "rightValue": "POST", "operator": { "type": "string", "operation": "equals" }, "id": "eda2323a-bf78-4eb5-be11-f7e3abe9b155" } ], "combinator": "and" } }, { "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "cd76dc0d-3f7a-4a11-8e58-ff83dcff3c6e", "leftValue": "={{ $json.toSend.method }}", "rightValue": "GET", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" } } ] }, "options": { "ignoreCase": true } }, "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ 1088, 2544 ], "id": "e5727b7a-8490-4053-b916-00eb9de823d2", "name": "Switch" }, { "parameters": { "method": "={{ $json.toSend.method }}", "url": "={{ $json.toSend.url }}", "sendQuery": true, "specifyQuery": "json", "jsonQuery": "={{ JSON.stringify($json.toSend.searchParams ?? {}) }}", "sendHeaders": true, "specifyHeaders": "json", "jsonHeaders": "={{ JSON.stringify($json.toSend.headers ?? {}) }}", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ $json.toSend.body }}", "options": {} }, "id": "af2ca88a-3f20-46f2-9bb7-283ee1bd7d5c", "name": "Send to Telegram (POST)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ 1296, 2464 ] }, { "parameters": { "method": "={{ $json.toSend.method }}", "url": "={{ $json.toSend.url }}", "sendQuery": true, "specifyQuery": "json", "jsonQuery": "={{ JSON.stringify($json.toSend.searchParams) }}", "sendHeaders": true, "specifyHeaders": "json", "jsonHeaders": "={{ JSON.stringify($json.toSend.headers) }}", "options": {} }, "id": "a27e5294-590e-4c6b-817e-b76ccbc176ae", "name": "Send to Telegram (GET)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ 1296, 2624 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "4d08dd15-fd42-4c1d-9d7a-32f7a00f5a9f", "leftValue": "={{ $json.headers[\"x-n8n-device-relay\"] }}", "rightValue": "", "operator": { "type": "string", "operation": "exists", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 1232, 1312 ], "id": "409e36eb-7d51-43a8-9be8-eabc48770c90", "name": "If" }, { "parameters": { "errorMessage": "No token specified!" }, "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ 1600, 1328 ], "id": "ab8801d0-91dc-42c5-bdb8-c7848d2eb37b", "name": "Stop and Error" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "4d08dd15-fd42-4c1d-9d7a-32f7a00f5a9f", "leftValue": "={{ $input.all().length }}", "rightValue": 2, "operator": { "type": "number", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 1088, 1888 ], "id": "80107617-58c2-4126-91bf-63294a480bee", "name": "Validate config presence" }, { "parameters": { "errorMessage": "No config or webhook data are present" }, "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ 1296, 2064 ], "id": "ee44ba5c-2bd8-4d0c-ada9-afff66ca2103", "name": "Stop and Error - config presence" }, { "parameters": { "errorMessage": "No tokens in config" }, "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ 1504, 2064 ], "id": "fa6dd447-a322-4fda-96fc-c999ff6934d4", "name": "Stop and Error - config tokens length" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "4d08dd15-fd42-4c1d-9d7a-32f7a00f5a9f", "leftValue": "={{ $input.all()[1].json.tokens.length\n}}", "rightValue": 0, "operator": { "type": "number", "operation": "gt" } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 1296, 1888 ], "id": "bc4d06c4-7f23-495e-a933-04753091b46e", "name": "Validate config tokens length" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "4d08dd15-fd42-4c1d-9d7a-32f7a00f5a9f", "leftValue": "={{ $input.all()[1].json.tokens.some(tokenObj =>\n tokenObj.key === $input.all()[0].json.headers[\"x-n8n-device-relay\"]\n) }}", "rightValue": 1, "operator": { "type": "boolean", "operation": "exists", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 1504, 1888 ], "id": "b9939b24-8922-4013-9c31-d31d1330d1fc", "name": "Validate config token" }, { "parameters": { "errorMessage": "Provided token does not exist" }, "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ 1712, 2064 ], "id": "7106bb64-9ffa-4fac-bce3-b2bb9b160098", "name": "Stop and Error - validate config token" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "4d08dd15-fd42-4c1d-9d7a-32f7a00f5a9f", "leftValue": "={{ $input.all()[0].json.body.data.type }}", "rightValue": 1, "operator": { "type": "string", "operation": "exists", "singleValue": true } }, { "id": "c40abd2c-cc8f-4a9b-b111-161e8c200f76", "leftValue": "={{ $input.all()[0].json.body.data.type }}", "rightValue": "", "operator": { "type": "string", "operation": "notEmpty", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 1920, 1888 ], "id": "76577436-0b6b-4f6f-8eeb-39116e046b64", "name": "Validate config data type" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "4d08dd15-fd42-4c1d-9d7a-32f7a00f5a9f", "leftValue": "={{ $input.all()[0].json.body.device.id }}", "rightValue": 1, "operator": { "type": "string", "operation": "exists", "singleValue": true } }, { "id": "c40abd2c-cc8f-4a9b-b111-161e8c200f76", "leftValue": "={{ $input.all()[0].json.body.device.id }}", "rightValue": "", "operator": { "type": "string", "operation": "notEmpty", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 2128, 1888 ], "id": "b852f393-3ca2-4453-9ac4-6ecea3748f90", "name": "Validate config device id" }, { "parameters": { "errorMessage": "data type does not exist or empty" }, "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ 2128, 2064 ], "id": "f92463e3-5edd-4880-9abd-05aa3705561f", "name": "Stop and Error - validate data type in event" }, { "parameters": { "errorMessage": "No Device ID provided" }, "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ 2336, 2064 ], "id": "f5ab8548-d437-433a-8243-3a2be4a15c1d", "name": "Stop and Error - validate device id in event" }, { "parameters": { "content": "## Edit the config variable there ->\n" }, "type": "n8n-nodes-base.stickyNote", "position": [ 1088, 1456 ], "typeVersion": 1, "id": "699b998b-b524-49aa-a711-1cc10751d100", "name": "Sticky Note" }, { "parameters": { "jsCode": "// 0) Validate webhook payload type\nconst all = $input.all();\n\nconst eventItem = all[0].json; // from Webhook\nconst configItem = all[1].json; // from config node\n\nreturn [\n {\n json: {\n event: eventItem,\n config: configItem\n }\n }\n];\n" }, "id": "0a9a0842-a62f-423d-a472-7743f4eabbee", "name": "Remap objects", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1088, 2240 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "4d08dd15-fd42-4c1d-9d7a-32f7a00f5a9f", "leftValue": "={{ $input.all()[1].json.tokens.find(tokenObj =>\n tokenObj.name === $input.all()[0].json.body.device.id\n).key }}", "rightValue": "={{ $input.all()[0].json.headers[\"x-n8n-device-relay\"] }}", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 1712, 1888 ], "id": "cfc38bbc-6880-4b79-a58d-9ed18ca0a42e", "name": "Validate config token per device" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "4d08dd15-fd42-4c1d-9d7a-32f7a00f5a9f", "leftValue": "={{ $input.all()[1].json.tokens.find(tokenObj =>\n tokenObj.name.toLowerCase() === \"debug\").key }}", "rightValue": "={{ $input.all()[0].json.headers[\"x-n8n-device-relay\"] }}", "operator": { "type": "string", "operation": "notEquals" } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 1712, 1696 ], "id": "a6b7f945-6936-46b6-a8b7-df40f030b11f", "name": "Is not debug device token" }, { "parameters": { "errorMessage": "Token does not match with defined device" }, "type": "n8n-nodes-base.stopAndError", "typeVersion": 1, "position": [ 1920, 2064 ], "id": "a198749f-04dc-4f5c-893a-fea478463df4", "name": "Stop and Error - validate config per device" }, { "parameters": { "assignments": { "assignments": [ { "id": "2f835c8c-13fa-49f9-b5d9-922d92154be7", "name": "step", "value": "initial", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 2080, 1072 ], "id": "5c7c98fd-44e4-4a50-96a4-32bdd8b85d54", "name": "Step value: initial", "executeOnce": true }, { "parameters": {}, "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ 2912, 816 ], "id": "ef0e3973-073e-4d70-881d-84ef89a76c48", "name": "Merge after step and config check" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "9095153c-1228-4fe9-934f-1c59c362b3a8", "leftValue": "={{ $input.all()[1].json.logIngestions }}", "rightValue": "", "operator": { "type": "object", "operation": "exists", "singleValue": true } }, { "id": "b4c8d7f7-6b71-4076-8b4d-d489ef7a766b", "leftValue": "={{ $json.config.logIngestions }}", "rightValue": "", "operator": { "type": "object", "operation": "exists", "singleValue": true } } ], "combinator": "or" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 2080, 928 ], "id": "f8fc04c3-a284-4451-8e4b-af131b97d9b7", "name": "If logIngestions exists" }, { "parameters": { "assignments": { "assignments": [ { "id": "2f835c8c-13fa-49f9-b5d9-922d92154be7", "name": "step", "value": "config_validated", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 2208, 1232 ], "id": "29ebf172-aed1-4e8a-8c4f-c05618d7e60f", "name": "Step value: config_validated", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-000000000012", "name": "step", "value": "validate_tokens_length", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 2352, 1392 ], "id": "a31f163b-6af6-4726-88ef-5f03956b8348", "name": "Step value: validate_tokens_length", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-000000000013", "name": "step", "value": "validate_config_token", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 2496, 1536 ], "id": "692f337e-e45c-4aca-8a40-24a6f3a06f8a", "name": "Step value: validate_config_token", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-000000000014", "name": "step", "value": "validate_device_token", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 2624, 1664 ], "id": "344caf6d-51cc-452c-b397-9efe2bb646f2", "name": "Step value: validate_device_token", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-000000000015", "name": "step", "value": "validate_debug_token", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 2768, 1776 ], "id": "c34c9baf-8218-4d6c-aaf1-4f7eb602f38b", "name": "Step value: validate_debug_token", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-000000000016", "name": "step", "value": "validate_data_type", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 2896, 1888 ], "id": "57b18acc-35b9-48b0-98c3-0ce20e6362c4", "name": "Step value: validate_data_type", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-000000000017", "name": "step", "value": "validate_device_id", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3008, 2000 ], "id": "936d150c-6890-4bee-a53c-70d568866775", "name": "Step value: validate_device_id", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-000000000018", "name": "step", "value": "step_remap_objects", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3136, 2112 ], "id": "7a110201-602d-46a0-ad17-a1bcfc0e8e79", "name": "Step value: step_remap_objects", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-000000000019", "name": "step", "value": "step_filter_recipients_by_deviceid_datatype", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3280, 2224 ], "id": "565ddcda-f941-441d-b136-11ecfb29a912", "name": "Step value: step_filter_recipients_by_deviceid_datatype", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-00000000001a", "name": "step", "value": "step_filter_recipients_by_rules", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3408, 2336 ], "id": "8e21e8bc-24dd-42cc-978b-45f642b1c5f4", "name": "Step value: step_filter_recipients_by_rules", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-00000000001b", "name": "step", "value": "step_filter_recipients_by_endpointids_templateids", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3520, 2448 ], "id": "cc5309a4-777c-4cd9-9293-31fe194ba761", "name": "Step value: step_filter_recipients_by_endpointids_templateids", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-00000000001c", "name": "step", "value": "step_form_outbound", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3648, 2544 ], "id": "78bcc070-a52c-498f-99a1-f8e938bd2870", "name": "Step value: step_form_outbound", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-00000000001d", "name": "step", "value": "step_compose_messages", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3776, 2656 ], "id": "d798977a-a64b-47f9-b66e-7f9fe3e46653", "name": "Step value: step_compose_messages", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-00000000001e", "name": "step", "value": "step_template_messages", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 3920, 2768 ], "id": "9c55a8e5-88aa-4c1c-a28b-da2870efcce9", "name": "Step value: step_template_messages", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-00000000001f", "name": "step", "value": "send_message", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 4064, 2864 ], "id": "38183f47-61d7-47b8-93aa-de1bf5251acd", "name": "Step value: send_message", "executeOnce": true }, { "parameters": { "assignments": { "assignments": [ { "id": "00000000-0000-4000-8000-000000000020", "name": "step", "value": "send_message_after", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 4176, 2960 ], "id": "2176e546-44cf-4d5e-a4c4-46baa2ba7df3", "name": "Step value: send_message_after", "executeOnce": true }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "4d08dd15-fd42-4c1d-9d7a-32f7a00f5a9f", "leftValue": "={{ $input.all().length }}", "rightValue": 2, "operator": { "type": "number", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 3184, 816 ], "id": "8ec743a5-16d6-4c6d-9208-37e475c5e24d", "name": "Validate body and step presence" }, { "parameters": { "content": "This is an optional log ingestion workflow to send telemetry and logs to an HTTP endpoint. It will be skipped if there is no `.logIngestion` property in JSON config.", "color": 7 }, "type": "n8n-nodes-base.stickyNote", "position": [ 2016, 736 ], "typeVersion": 1, "id": "7ba91fdc-b25d-4266-8936-e74712c780bb", "name": "Sticky Note1" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "4d08dd15-fd42-4c1d-9d7a-32f7a00f5a9f", "leftValue": "={{ Object.keys($input.all()[0].json.config.logIngestions || {}).length > 0 }}", "rightValue": 2, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 3408, 816 ], "id": "580c3060-78f3-4d86-b5c5-14fd06800c54", "name": "Validate logIngestions length" }, { "parameters": { "workflowId": { "__rl": true, "value": "yQrHfWrwGyBImk0h", "mode": "list", "cachedResultUrl": "/workflow/yQrHfWrwGyBImk0h", "cachedResultName": "LogIngestion in Device Relay by Kenya-West" }, "workflowInputs": { "mappingMode": "defineBelow", "value": { "logIngestions": "={{ $json.logIngestions }}", "step": "={{ $json.step }}", "event": "={{ $json.event }}" }, "matchingColumns": [], "schema": [ { "id": "event", "displayName": "event", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "removed": false }, { "id": "logIngestions", "displayName": "logIngestions", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "removed": false }, { "id": "step", "displayName": "step", "required": false, "defaultMatch": false, "display": true, "canBeUsedToMatch": true, "removed": false } ], "attemptToConvertTypes": false, "convertFieldsToString": true }, "options": {} }, "type": "n8n-nodes-base.executeWorkflow", "typeVersion": 1.2, "position": [ 3824, 816 ], "name": "Call LogIngestion in Device Relay by Kenya-West", "id": "02ae1880-e959-4c5d-bed6-2303efc21be0", "onError": "continueRegularOutput" }, { "parameters": { "jsCode": "const all = $input.all();\n\nconst eventItem = all[0].json.event;\nconst configItem = all[0].json.config;\nconst step = all[1].json.step;\n\nreturn [\n {\n json: {\n event: eventItem,\n config: configItem,\n logIngestions: configItem.logIngestions,\n step\n }\n }\n];\n" }, "id": "34a23523-acdc-471a-9017-44b39e61a7c6", "name": "Remap logIngestion", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 3632, 816 ] } ], "connections": { "Webhook": { "main": [ [ { "node": "If", "type": "main", "index": 0 } ] ] }, "Merge": { "main": [ [ { "node": "Validate config presence", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 }, { "node": "Step value: initial", "type": "main", "index": 0 } ] ] }, "Config": { "main": [ [ { "node": "Merge", "type": "main", "index": 1 } ] ] }, "Form outbound": { "main": [ [ { "node": "Compose messages", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 }, { "node": "Step value: step_form_outbound", "type": "main", "index": 0 } ] ] }, "Compose messages": { "main": [ [ { "node": "Template messages", "type": "main", "index": 0 }, { "node": "Step value: step_compose_messages", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 } ] ] }, "Filter recipients by recipient rules": { "main": [ [ { "node": "Filter rules by recipients endpointIds and templateIds", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 }, { "node": "Step value: step_filter_recipients_by_rules", "type": "main", "index": 0 } ] ] }, "Template messages": { "main": [ [ { "node": "Switch", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 }, { "node": "Step value: step_template_messages", "type": "main", "index": 0 } ] ] }, "Filter recipients by DeviceId and DataType filters": { "main": [ [ { "node": "Filter recipients by recipient rules", "type": "main", "index": 0 }, { "node": "Step value: step_filter_recipients_by_deviceid_datatype", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 } ] ] }, "Filter rules by recipients endpointIds and templateIds": { "main": [ [ { "node": "Form outbound", "type": "main", "index": 0 }, { "node": "Step value: step_filter_recipients_by_endpointids_templateids", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 } ] ] }, "Switch": { "main": [ [ { "node": "Send to Telegram (POST)", "type": "main", "index": 0 } ], [ { "node": "Send to Telegram (GET)", "type": "main", "index": 0 } ] ] }, "Send to Telegram (POST)": { "main": [ [ { "node": "Step value: send_message_after", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 } ] ] }, "Send to Telegram (GET)": { "main": [ [ { "node": "Step value: send_message_after", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 } ] ] }, "If": { "main": [ [ { "node": "Config", "type": "main", "index": 0 }, { "node": "Merge", "type": "main", "index": 0 } ], [ { "node": "Stop and Error", "type": "main", "index": 0 } ] ] }, "Validate config presence": { "main": [ [ { "node": "Validate config tokens length", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 } ], [ { "node": "Stop and Error - config presence", "type": "main", "index": 0 } ] ] }, "Validate config tokens length": { "main": [ [ { "node": "Validate config token", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 }, { "node": "Step value: validate_tokens_length", "type": "main", "index": 0 } ], [ { "node": "Stop and Error - config tokens length", "type": "main", "index": 0 } ] ] }, "Validate config token": { "main": [ [ { "node": "Is not debug device token", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 }, { "node": "Step value: validate_config_token", "type": "main", "index": 0 } ], [ { "node": "Stop and Error - validate config token", "type": "main", "index": 0 } ] ] }, "Validate config data type": { "main": [ [ { "node": "Validate config device id", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 }, { "node": "Step value: validate_data_type", "type": "main", "index": 0 } ], [ { "node": "Stop and Error - validate data type in event", "type": "main", "index": 0 } ] ] }, "Validate config device id": { "main": [ [ { "node": "Remap objects", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 }, { "node": "Step value: validate_device_id", "type": "main", "index": 0 } ], [ { "node": "Stop and Error - validate device id in event", "type": "main", "index": 0 } ] ] }, "Remap objects": { "main": [ [ { "node": "Filter recipients by DeviceId and DataType filters", "type": "main", "index": 0 }, { "node": "Step value: step_remap_objects", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 } ] ] }, "Validate config token per device": { "main": [ [ { "node": "Validate config data type", "type": "main", "index": 0 }, { "node": "If logIngestions exists", "type": "main", "index": 0 }, { "node": "Step value: validate_device_token", "type": "main", "index": 0 } ], [ { "node": "Stop and Error - validate config per device", "type": "main", "index": 0 } ] ] }, "Is not debug device token": { "main": [ [ { "node": "Validate config token per device", "type": "main", "index": 0 }, { "node": "Step value: validate_debug_token", "type": "main", "index": 0 } ], [ { "node": "Validate config data type", "type": "main", "index": 0 } ] ] }, "Step value: initial": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Merge after step and config check": { "main": [ [ { "node": "Validate body and step presence", "type": "main", "index": 0 } ] ] }, "If logIngestions exists": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 0 } ] ] }, "Step value: config_validated": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: validate_tokens_length": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: validate_config_token": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: validate_device_token": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: validate_debug_token": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: validate_data_type": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: validate_device_id": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: step_remap_objects": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: step_filter_recipients_by_deviceid_datatype": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: step_filter_recipients_by_rules": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: step_filter_recipients_by_endpointids_templateids": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: step_form_outbound": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: step_compose_messages": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: step_template_messages": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: send_message": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Step value: send_message_after": { "main": [ [ { "node": "Merge after step and config check", "type": "main", "index": 1 } ] ] }, "Validate body and step presence": { "main": [ [ { "node": "Validate logIngestions length", "type": "main", "index": 0 } ] ] }, "Validate logIngestions length": { "main": [ [ { "node": "Remap logIngestion", "type": "main", "index": 0 } ] ] }, "Remap logIngestion": { "main": [ [ { "node": "Call LogIngestion in Device Relay by Kenya-West", "type": "main", "index": 0 } ] ] } }, "pinData": {}, "meta": { "templateCredsSetupCompleted": true, "instanceId": "f4300700b5a89163b88ad6a908ff9e3076c705e8b03a3f40f03ff2f29f7e4289" } }