{
  "name": "RSS Security News Deduplicator with State Tracking",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "9f3d47dc-9b78-486a-b3f2-f15fd866c61d",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        976,
        -112
      ]
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        976,
        -288
      ],
      "id": "d94a3471-41b4-43a8-b210-7f74a7b310f6",
      "name": "When clicking 'Execute workflow'"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1rN0gYpZnH1GXKAjyCsQvBcu7l5pDv_pxVo28DCWGpjE"
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1",
          "cachedResultUrl": ""
        },
        "options": {}
      },
      "id": "6532f3d3-a50a-4365-833b-0b611fee24f9",
      "name": "Read State from Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1200,
        -112
      ],
      "alwaysOutputData": true,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "hHAfsVGhuZEemmJV",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Guard against empty sheet (first run)\nconst items = $input.all();\nconst stateRow = items.length > 0 ? items[0].json : {};\nconst lastPubDate = stateRow['LastPubDate'] || null;\n\nreturn [{\n  json: {\n    lastPubDate: lastPubDate,\n    hasState: !!lastPubDate\n  }\n}];"
      },
      "id": "c0470904-4998-420a-8fb2-59e6d4955a07",
      "name": "Parse State1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1424,
        -112
      ]
    },
    {
      "parameters": {
        "url": "https://feeds.feedburner.com/TheHackersNews",
        "options": {}
      },
      "id": "308c3e24-d8aa-416b-97ed-aaeba1844c1c",
      "name": "Fetch RSS Feed",
      "type": "n8n-nodes-base.rssFeedRead",
      "typeVersion": 1,
      "position": [
        1648,
        -112
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "const state = $('Parse State1').first().json;\nconst rssItems = $input.all();\n\nif (!state.hasState) {\n  // First run — treat all items as new\n  return rssItems;\n}\n\nconst lastPubDate = new Date(state.lastPubDate);\nconst filtered = rssItems.filter(item => {\n  const pubDate = new Date(item.json.pubDate || item.json.isoDate);\n  return pubDate > lastPubDate;\n});\n\nreturn filtered;"
      },
      "id": "17b0c337-0d30-4d7c-b5f4-2ee119ac0472",
      "name": "Filter New Items",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1856,
        -112
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "id": "1",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.title }}",
              "rightValue": "Exploit"
            },
            {
              "id": "2",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.title }}",
              "rightValue": "0-day"
            },
            {
              "id": "3",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.title }}",
              "rightValue": "Bitcoin"
            },
            {
              "id": "4",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.title }}",
              "rightValue": "CVE"
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "id": "238c8e17-6a7e-42fc-bf6c-86cfc2449a4a",
      "name": "Filter",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2,
      "position": [
        2080,
        -112
      ]
    },
    {
      "parameters": {
        "jsCode": "for (const item of $input.all()) {\n  const title = item.json.title || 'No Title';\n  const link = item.json.link || '';\n  \n  // 1. Simple escape for the title only\n  const escapeHTML = (str) => str.toString()\n      .replace(/&/g, '&amp;')\n      .replace(/</g, '&lt;')\n      .replace(/>/g, '&gt;');\n\n  const cleanTitle = escapeHTML(title);\n  const date = item.json.pubDate ? new Date(item.json.pubDate).toLocaleString() : 'Recently';\n\n  // 2. Prepare Tags\n  let tags = [];\n  const lowerTitle = title.toLowerCase();\n  if (lowerTitle.includes('exploit')) tags.push('#Exploit');\n  if (lowerTitle.includes('0-day')) tags.push('#ZeroDay');\n  if (lowerTitle.includes('cve')) tags.push('#CVE');\n  if (lowerTitle.includes('bitcoin')) tags.push('#Bitcoin');\n  const tagString = tags.length > 0 ? tags.join(' ') : '#Security';\n\n  // 3. THE FIX: Move the link to its own line without <a> tags\n  // This prevents Telegram from parsing the URL characters.\n  item.json.tg_message = `<b>🚨 NEW SECURITY ALERT 🚨</b>\\n\\n` +\n                         `<b>📌 Title:</b> ${cleanTitle}\\n` +\n                         `<b>📅 Published:</b> ${date}\\n\\n` +\n                         `<b>🔗 Link:</b>\\n${link}\\n\\n` + // No <a> tag here\n                         `_________________________\\n` +\n                         `<b>🏷 Keywords:</b> <code>${tagString}</code>`;\n}\n\nreturn $input.all();"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2304,
        -112
      ],
      "id": "ad326cde-6a34-445e-9558-60c901681914",
      "name": "Code in JavaScript"
    },
    {
      "parameters": {
        "chatId": "-1003735898745",
        "text": "={{ $json.tg_message }}",
        "additionalFields": {
          "parse_mode": "HTML"
        }
      },
      "id": "70a760ef-f95a-455e-b1cc-c8c83ded853b",
      "name": "Send to Telegram",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1,
      "position": [
        2528,
        -112
      ],
      "webhookId": "fd1e6280-09cb-4b80-8a06-2a3dfe77a90a",
      "alwaysOutputData": false,
      "credentials": {
        "telegramApi": {
          "id": "0vnrBUm3coYjYkdp",
          "name": "Telegram account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Find the max pubDate across ALL newly-fetched items (not just keyword-filtered)\n// so we don't re-process non-matching items on the next run\nconst allNewItems = $('Filter New Items').all();\n\nconst maxDate = allNewItems.reduce((max, item) => {\n  const d = new Date(item.json.pubDate || item.json.isoDate || 0);\n  return d > max ? d : max;\n}, new Date(0));\n\nreturn [{\n  json: {\n    StateName: 'rss_state',\n    LastPubDate: maxDate.toISOString()\n  }\n}];"
      },
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "Compute New State",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2752,
        -112
      ]
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1rN0gYpZnH1GXKAjyCsQvBcu7l5pDv_pxVo28DCWGpjE"
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1",
          "cachedResultUrl": ""
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "StateName": "={{ $json.StateName }}",
            "LastPubDate": "={{ $json.LastPubDate }}"
          },
          "matchingColumns": [
            "StateName"
          ],
          "schema": [
            {
              "id": "StateName",
              "displayName": "StateName",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "LastPubDate",
              "displayName": "LastPubDate",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "id": "0825ef9a-1af8-437b-a053-dcc930b5f12d",
      "name": "Update State",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        2976,
        -112
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "hHAfsVGhuZEemmJV",
          "name": "Google Sheets account"
        }
      }
    }
  ],
  "pinData": {},
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Read State from Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "Read State from Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read State from Sheet": {
      "main": [
        [
          {
            "node": "Parse State1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse State1": {
      "main": [
        [
          {
            "node": "Fetch RSS Feed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch RSS Feed": {
      "main": [
        [
          {
            "node": "Filter New Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter New Items": {
      "main": [
        [
          {
            "node": "Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Send to Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send to Telegram": {
      "main": [
        [
          {
            "node": "Compute New State",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute New State": {
      "main": [
        [
          {
            "node": "Update State",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "saveManualExecutions": true,
    "saveExecutionProgress": true,
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false
  },
  "versionId": "3938a1a5-83c8-4bc7-b6e6-839105d5cee1",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "e09d1f0f8b9f78e80ac70b6dc8726dd263b0b6ffc4300b0eee3b58f6da316f29"
  },
  "id": "gVLkFzM49qE0YqKy",
  "tags": []
}
