Files
hn-oss-digest/n8n/workflow-digest-oss.json
T

1025 lines
40 KiB
JSON

{
"id": "tZwGIY9rcdybQLPr",
"name": "Digest OSS Hacker News",
"description": null,
"active": true,
"activeVersionId": "0a920067-acb9-49ef-9fa7-fe8a7dfdd006",
"createdAt": "2026-10-15T09:32:21.737Z",
"updatedAt": "2026-10-15T12:04:40.776Z",
"isArchived": false,
"versionId": "0a920067-acb9-49ef-9fa7-fe8a7dfdd006",
"versionCounter": 2,
"sourceWorkflowId": null,
"triggerCount": 1,
"nodes": [
{
"id": "006047cd-339c-4921-9f30-060c0666089d",
"name": "Credenziali",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-420,
0
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "c1",
"name": "gemini_key",
"value": "INSERISCI_GEMINI_KEY",
"type": "string"
},
{
"id": "c2",
"name": "telegram_token",
"value": "INSERISCI_TELEGRAM_TOKEN",
"type": "string"
},
{
"id": "c3",
"name": "telegram_chat_id",
"value": "INSERISCI_CHAT_ID",
"type": "string"
},
{
"id": "c4",
"name": "n8n_key",
"value": "INSERISCI_N8N_API_KEY",
"type": "string"
}
]
},
"options": {}
}
},
{
"id": "4b323904-fa5b-4c1c-ae0d-8445a5ed387a",
"name": "Ogni 12 ore",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-620,
0
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "=20 14 15 10 *"
}
]
}
}
},
{
"id": "212a624e-34e7-43a7-bce2-f751712fd56b",
"name": "Chat ID impostato?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-220,
0
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "haschat",
"leftValue": "={{ $json.telegram_chat_id }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "fceddced-e50d-4f7f-b3c2-1b5ffc94c6c9",
"name": "Leggi checkpoint",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"position": [
0,
-100
],
"parameters": {
"url": "https://n8n.as89.it/api/v1/data-tables/qkbTUn1IKaltUfVo/rows",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-N8N-API-KEY",
"value": "={{ $('Credenziali').first().json.n8n_key }}"
}
]
},
"options": {
"timeout": 30000
}
}
},
{
"id": "d8602c73-6a43-429d-94db-36646bd10110",
"name": "Prendi Show HN",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"position": [
220,
-100
],
"parameters": {
"url": "=https://hn.algolia.com/api/v1/search_by_date?tags=show_hn&numericFilters=created_at_i%3E{{ $json.sinceEpoch }}&hitsPerPage=1000",
"options": {}
}
},
{
"id": "c0053636-8b6a-4a34-b872-91fb4f66c034",
"name": "Prendi Tutte le Storie",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"position": [
440,
-100
],
"parameters": {
"url": "=https://hn.algolia.com/api/v1/search_by_date?tags=story&numericFilters=created_at_i%3E{{ $('Normalizza checkpoint').first().json.sinceEpoch }}&hitsPerPage=1000",
"options": {}
}
},
{
"id": "f8d03a8e-454e-424c-99d8-b825df23d560",
"name": "Seleziona OSS",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
-100
],
"parameters": {
"jsCode": "const REPO_RE = /github\\.com|gitlab\\.com|codeberg\\.org|sr\\.ht|sourcehut/i;\nconst OSS_TITLE_RE = /\\b(open ?sourc|foss|self.?-?host|local.?first|free software|libre)\\b/i;\nconst score = h => (h.points || 0) * 3 + (h.num_comments || 0);\nconst norm = t => (t || '').replace(/^show hn:\\s*/i, '').toLowerCase().trim().replace(/\\s+/g, ' ');\nconst link = h => h.url || ('https://news.ycombinator.com/item?id=' + h.objectID);\n\nconst showAll = $('Prendi Show HN').first().json.hits || [];\nconst show = showAll.filter(h => REPO_RE.test(h.url || '') || OSS_TITLE_RE.test(h.title || ''));\nconst stories = $('Prendi Tutte le Storie').first().json.hits || [];\nconst showTitles = new Set(show.map(h => norm(h.title)));\nconst repos = stories.filter(h => REPO_RE.test(h.url || '') && !showTitles.has(norm(h.title)));\nconst news = stories.filter(h => !REPO_RE.test(h.url || '') && OSS_TITLE_RE.test(h.title || '') && (h.points || 0) >= 3);\n\nconst all = [\n ...show.map(h => ({ ...h, _cat: 'show' })),\n ...repos.map(h => ({ ...h, _cat: 'repo' })),\n ...news.map(h => ({ ...h, _cat: 'news' })),\n];\nconst seen = new Set();\nconst dedup = [];\nfor (const h of all) { const n = norm(h.title); if (!seen.has(n)) { seen.add(n); dedup.push(h); } }\ndedup.sort((a, b) => score(b) - score(a));\n\nconst readmeUrlFor = (u) => {\n try {\n const m = String(u || '').match(/^https?:\\/\\/([^\\/]+)\\/([^\\/?#]+)\\/([^\\/?#]+)/i);\n if (!m) return null;\n const host = m[1].toLowerCase();\n const repo = m[2] + '/' + m[3];\n if (host === 'github.com' || host.endsWith('.github.com')) return 'https://raw.githubusercontent.com/' + repo + '/HEAD/README.md';\n if (host === 'gitlab.com' || host.endsWith('.gitlab.com')) return 'https://gitlab.com/' + repo + '/-/raw/HEAD/README.md';\n if (host === 'codeberg.org') return 'https://codeberg.org/' + repo + '/raw/branch/HEAD/README.md';\n } catch (e) {}\n return null;\n};\n\nconst items = await Promise.all(dedup.map(async (h) => {\n const it = { id: h.objectID, title: h.title, url: link(h), points: h.points || 0, comments: h.num_comments || 0, cat: h._cat };\n const ru = readmeUrlFor(it.url);\n if (ru) {\n try {\n const r = await this.helpers.httpRequest({ url: ru, timeout: 8000 });\n it.readme = String(r).slice(0, 1200);\n } catch (e) {\n it.readmeErr = String((e && e.message) || e).slice(0, 80);\n }\n }\n return it;\n}));\nconst conReadme = items.filter(i => i.readme).length;\n\nconst sinceOld = $('Normalizza checkpoint').first().json.sinceEpoch;\nconst sinceNew = Math.max(sinceOld, ...all.map(h => h.created_at_i || 0));\nconst newItemCount = all.filter(h => (h.created_at_i || 0) > sinceOld).length;\n\nconst prompt = 'Sei un redattore tecnico italiano specializzato in software open source. ' +\n 'Ricevi un elenco di item da Hacker News. ALCUNI item hanno un campo \"readme\": il testo reale del README del progetto, scaricato dal repository. ' +\n 'Per OGNI item produci: name (nome breve; se il README rivela il vero nome del progetto, usa quello), ' +\n 'desc, points, comments, url IDENTICO a quello ricevuto. ' +\n 'REGOLE per desc: se l\\'item ha il campo readme, scrivi da 4 a 6 frasi in italiano (almeno 400 caratteri): ' +\n 'il problema che risolve, come funziona in pratica, a chi serve, un dettaglio tecnico notevole se presente. ' +\n 'Basa le frasi SOLO sul README: non inventare nulla. ' +\n 'Se il README mostra che il progetto non e\\' open source o e\\' il client di un servizio a pagamento, dillo esplicitamente. ' +\n 'Se l\\'item NON ha il campo readme, scrivi 2 frasi dal titolo e ANTICIPALE con il marcatore [non verificato]. ' +\n 'Niente marketing, niente emoji (eccetto il marcatore). ' +\n 'NON OMETTERE NESSUN ITEM: la risposta deve contenere esattamente ' + items.length + ' item in totale. ' +\n 'Raggruppa per categoria: show, repo, news, come nel campo cat. ' +\n 'Ordina ogni categoria per importanza. ' +\n 'Rispondi SOLO con JSON valido, forma esatta: ' +\n '{\"sections\":{\"show\":[{\"name\":\"\",\"desc\":\"\",\"points\":0,\"comments\":0,\"url\":\"\"}],\"repo\":[],\"news\":[]}} ' +\n '\\n\\nELENCO ITEM (JSON):\\n' + JSON.stringify(items);\n\nreturn [{ json: { items, count: items.length, conReadme, sinceOld, sinceNew, newItemCount, prompt } }];"
}
},
{
"id": "f082ed2c-c61a-49c7-b101-44be06b278e4",
"name": "Gemini riassume",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 10000,
"onError": "continueErrorOutput",
"position": [
880,
-100
],
"parameters": {
"method": "POST",
"url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={{ $('Credenziali').first().json.gemini_key }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ contents: [{ parts: [{ text: $json.prompt }] }], generationConfig: { responseMimeType: 'application/json', temperature: 0.4, maxOutputTokens: 30000, thinkingConfig: { thinkingBudget: 0 } } }) }}",
"options": {
"timeout": 300000
}
}
},
{
"id": "fbdc92fb-e069-4889-8789-83987ebad447",
"name": "Prepara messaggi",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1100,
-100
],
"parameters": {
"jsCode": "const pick = (name) => { try { const j = $(name).first().json; if (j && j.candidates) return j; } catch (e) {} return null; };\nconst resp = pick('Gemini riassume') || pick('Gemini lite (fallback)');\n\nconst raw = ((resp.candidates || [])[0] || {}).content;\nconst text = (raw && Array.isArray(raw.parts)) ? raw.parts.map(p => p.text || '').join('') : '';\nlet digest;\ntry { digest = JSON.parse(text); }\ncatch (e) {\n const m = text.match(/\\{[\\s\\S]*\\}/);\n if (!m) throw new Error('Gemini non ha restituito JSON valido: ' + text.slice(0, 300));\n digest = JSON.parse(m[0]);\n}\nconst meta = $('Seleziona OSS').first().json;\nconst secs = (digest && digest.sections) || {};\nconst total = ['show', 'repo', 'news'].reduce((n, k) => n + ((secs[k] || []).length), 0);\n\nconst date = new Date();\nconst quando = date.toLocaleString('it-IT', { weekday: 'long', day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Rome' });\nconst header = '\\ud83e\\udd8a DIGEST OPEN SOURCE da Hacker News\\n' + quando +\n '\\n' + meta.count + ' progetti e novita OSS delle ultime 12 ore (Show HN + repo + release). Zero tagli.' +\n (total !== meta.count ? '\\n(attenzione: Gemini ne ha restituiti ' + total + ' — eventuali mancanti non si riproporranno nella prossima finestra)' : '') + '\\n\\n';\n\nconst TITLES = { show: '\\ud83d\\ude80 NUOVI PROGETTI OPEN SOURCE (Show HN)', repo: '\\ud83d\\udd17 PROGETTI EMERSI NELLE DISCUSSIONI', news: '\\ud83d\\udcf0 RELEASE E NOVITA SU PROGETTI ESISTENTI' };\nconst blocks = [];\nfor (const k of ['show', 'repo', 'news']) {\n const arr = secs[k] || [];\n if (!arr.length) continue;\n blocks.push(TITLES[k]);\n for (const it of arr) {\n let line = '\\u2022 ' + (it.name || '?');\n if (it.desc) line += ' \\u2014 ' + it.desc;\n line += ' [' + (it.points || 0) + '\\u2191/' + (it.comments || 0) + '\\ud83d\\udcac]';\n blocks.push(line);\n blocks.push(it.url || '');\n }\n blocks.push('');\n}\nconst MAX = 3800;\nconst msgs = [];\nlet cur = header;\nfor (const b of blocks) {\n if (cur.length + b.length + 1 > MAX) { msgs.push(cur.trimEnd()); cur = '(segue)\\n' + b + '\\n'; }\n else cur += b + '\\n';\n}\nif (cur.trim()) msgs.push(cur.trimEnd());\n\nconst cred = $('Credenziali').first().json;\nreturn msgs.map((t, i) => ({ json: {\n chat_id: cred.telegram_chat_id, text: t,\n seq: i + 1, totale: msgs.length, sinceNew: meta.sinceNew,\n} }));"
}
},
{
"id": "e232c24f-b68c-4b75-985f-1da15ffe4f7f",
"name": "Invia su Telegram",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"position": [
1320,
-100
],
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $('Credenziali').first().json.telegram_token }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: Number($json.chat_id), text: $json.text }) }}",
"options": {
"timeout": 60000
}
}
},
{
"id": "ce380b59-8b69-4ab1-a2a2-4a8c6311f70d",
"name": "Aggiorna checkpoint",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"executeOnce": true,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"position": [
1540,
-100
],
"parameters": {
"method": "POST",
"url": "https://n8n.as89.it/api/v1/data-tables/qkbTUn1IKaltUfVo/rows",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-N8N-API-KEY",
"value": "={{ $('Credenziali').first().json.n8n_key }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ data: [{ sinceEpoch: $('Prepara messaggi').first().json.sinceNew }] }) }}",
"options": {
"timeout": 30000
}
}
},
{
"id": "c54b86c0-058b-48b3-9988-f80e0d5f6b6d",
"name": "Avvisa in chat Hermes",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
0,
140
],
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $('Credenziali').first().json.telegram_token }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: Number($('Credenziali').first().json.telegram_chat_id), text: 'Attiva il digest: apri t.me/Hn12h_bot e premi AVVIA, poi scrivi qui a Vale la parola via. (Messaggio automatico del workflow Digest OSS: chat ID non ancora configurato.)' }) }}",
"options": {
"timeout": 60000
}
}
},
{
"id": "aa3e286c-8675-4f05-bb31-3246d4013ca8",
"name": "Normalizza checkpoint",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
110,
-100
],
"parameters": {
"jsCode": "const rows = ($('Leggi checkpoint').first().json.data || []);\nconst sinceEpoch = rows.length ? Math.max(...rows.map(r => r.sinceEpoch || 0)) : 0;\nreturn [{ json: { sinceEpoch } }];"
}
},
{
"id": "c5e7e303-ef49-4bd1-a519-ada163a0ab77",
"name": "Gemini lite (fallback)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 15000,
"position": [
990,
60
],
"parameters": {
"method": "POST",
"url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent?key={{ $('Credenziali').first().json.gemini_key }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ contents: [{ parts: [{ text: $('Seleziona OSS').first().json.prompt }] }], generationConfig: { responseMimeType: 'application/json', temperature: 0.4, maxOutputTokens: 30000, thinkingConfig: { thinkingBudget: 0 } } }) }}",
"options": {
"timeout": 300000
}
}
}
],
"connections": {
"Ogni 12 ore": {
"main": [
[
{
"node": "Credenziali",
"type": "main",
"index": 0
}
]
]
},
"Credenziali": {
"main": [
[
{
"node": "Chat ID impostato?",
"type": "main",
"index": 0
}
]
]
},
"Chat ID impostato?": {
"main": [
[
{
"node": "Leggi checkpoint",
"type": "main",
"index": 0
}
],
[
{
"node": "Avvisa in chat Hermes",
"type": "main",
"index": 0
}
]
]
},
"Leggi checkpoint": {
"main": [
[
{
"node": "Normalizza checkpoint",
"type": "main",
"index": 0
}
]
]
},
"Prendi Show HN": {
"main": [
[
{
"node": "Prendi Tutte le Storie",
"type": "main",
"index": 0
}
]
]
},
"Prendi Tutte le Storie": {
"main": [
[
{
"node": "Seleziona OSS",
"type": "main",
"index": 0
}
]
]
},
"Seleziona OSS": {
"main": [
[
{
"node": "Gemini riassume",
"type": "main",
"index": 0
}
]
]
},
"Gemini riassume": {
"main": [
[
{
"node": "Prepara messaggi",
"type": "main",
"index": 0
}
],
[
{
"node": "Gemini lite (fallback)",
"type": "main",
"index": 0
}
]
]
},
"Prepara messaggi": {
"main": [
[
{
"node": "Invia su Telegram",
"type": "main",
"index": 0
}
]
]
},
"Invia su Telegram": {
"main": [
[
{
"node": "Aggiorna checkpoint",
"type": "main",
"index": 0
}
]
]
},
"Normalizza checkpoint": {
"main": [
[
{
"node": "Prendi Show HN",
"type": "main",
"index": 0
}
]
]
},
"Gemini lite (fallback)": {
"main": [
[
{
"node": "Prepara messaggi",
"type": "main",
"index": 0
}
]
]
}
},
"nodeGroups": [],
"settings": {
"timezone": "Europe/Rome",
"executionOrder": "v1"
},
"staticData": {
"node:Ogni 12 ore": {
"recurrenceRules": [
null
],
"recurrenceRuleSignatures": [
null
]
}
},
"pinData": null,
"meta": null,
"tags": [],
"shared": [
{
"role": "workflow:owner",
"workflowId": "tZwGIY9rcdybQLPr",
"projectId": "haxNGpGWGLCeacr6",
"project": {
"id": "haxNGpGWGLCeacr6",
"name": "Andrea Sarego <andrea.sarego@linux.it>",
"type": "personal",
"icon": null,
"description": null,
"customTelemetryTags": [],
"creatorId": "e3520907-ef65-42c2-a65a-27100f80e8c5",
"createdAt": "2026-09-06T05:31:25.651Z",
"updatedAt": "2026-09-06T05:39:36.353Z"
},
"createdAt": "2026-10-15T09:32:21.737Z",
"updatedAt": "2026-10-15T09:32:21.737Z"
}
],
"activeVersion": {
"versionId": "0a920067-acb9-49ef-9fa7-fe8a7dfdd006",
"workflowId": "tZwGIY9rcdybQLPr",
"nodes": [
{
"id": "006047cd-339c-4921-9f30-060c0666089d",
"name": "Credenziali",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-420,
0
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "c1",
"name": "gemini_key",
"value": "INSERISCI_GEMINI_KEY",
"type": "string"
},
{
"id": "c2",
"name": "telegram_token",
"value": "INSERISCI_TELEGRAM_TOKEN",
"type": "string"
},
{
"id": "c3",
"name": "telegram_chat_id",
"value": "INSERISCI_CHAT_ID",
"type": "string"
},
{
"id": "c4",
"name": "n8n_key",
"value": "INSERISCI_N8N_API_KEY",
"type": "string"
}
]
},
"options": {}
}
},
{
"id": "4b323904-fa5b-4c1c-ae0d-8445a5ed387a",
"name": "Ogni 12 ore",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-620,
0
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "=20 14 15 10 *"
}
]
}
}
},
{
"id": "212a624e-34e7-43a7-bce2-f751712fd56b",
"name": "Chat ID impostato?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-220,
0
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "haschat",
"leftValue": "={{ $json.telegram_chat_id }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "fceddced-e50d-4f7f-b3c2-1b5ffc94c6c9",
"name": "Leggi checkpoint",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"position": [
0,
-100
],
"parameters": {
"url": "https://n8n.as89.it/api/v1/data-tables/qkbTUn1IKaltUfVo/rows",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-N8N-API-KEY",
"value": "={{ $('Credenziali').first().json.n8n_key }}"
}
]
},
"options": {
"timeout": 30000
}
}
},
{
"id": "d8602c73-6a43-429d-94db-36646bd10110",
"name": "Prendi Show HN",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"position": [
220,
-100
],
"parameters": {
"url": "=https://hn.algolia.com/api/v1/search_by_date?tags=show_hn&numericFilters=created_at_i%3E{{ $json.sinceEpoch }}&hitsPerPage=1000",
"options": {}
}
},
{
"id": "c0053636-8b6a-4a34-b872-91fb4f66c034",
"name": "Prendi Tutte le Storie",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"position": [
440,
-100
],
"parameters": {
"url": "=https://hn.algolia.com/api/v1/search_by_date?tags=story&numericFilters=created_at_i%3E{{ $('Normalizza checkpoint').first().json.sinceEpoch }}&hitsPerPage=1000",
"options": {}
}
},
{
"id": "f8d03a8e-454e-424c-99d8-b825df23d560",
"name": "Seleziona OSS",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
-100
],
"parameters": {
"jsCode": "const REPO_RE = /github\\.com|gitlab\\.com|codeberg\\.org|sr\\.ht|sourcehut/i;\nconst OSS_TITLE_RE = /\\b(open ?sourc|foss|self.?-?host|local.?first|free software|libre)\\b/i;\nconst score = h => (h.points || 0) * 3 + (h.num_comments || 0);\nconst norm = t => (t || '').replace(/^show hn:\\s*/i, '').toLowerCase().trim().replace(/\\s+/g, ' ');\nconst link = h => h.url || ('https://news.ycombinator.com/item?id=' + h.objectID);\n\nconst showAll = $('Prendi Show HN').first().json.hits || [];\nconst show = showAll.filter(h => REPO_RE.test(h.url || '') || OSS_TITLE_RE.test(h.title || ''));\nconst stories = $('Prendi Tutte le Storie').first().json.hits || [];\nconst showTitles = new Set(show.map(h => norm(h.title)));\nconst repos = stories.filter(h => REPO_RE.test(h.url || '') && !showTitles.has(norm(h.title)));\nconst news = stories.filter(h => !REPO_RE.test(h.url || '') && OSS_TITLE_RE.test(h.title || '') && (h.points || 0) >= 3);\n\nconst all = [\n ...show.map(h => ({ ...h, _cat: 'show' })),\n ...repos.map(h => ({ ...h, _cat: 'repo' })),\n ...news.map(h => ({ ...h, _cat: 'news' })),\n];\nconst seen = new Set();\nconst dedup = [];\nfor (const h of all) { const n = norm(h.title); if (!seen.has(n)) { seen.add(n); dedup.push(h); } }\ndedup.sort((a, b) => score(b) - score(a));\n\nconst readmeUrlFor = (u) => {\n try {\n const m = String(u || '').match(/^https?:\\/\\/([^\\/]+)\\/([^\\/?#]+)\\/([^\\/?#]+)/i);\n if (!m) return null;\n const host = m[1].toLowerCase();\n const repo = m[2] + '/' + m[3];\n if (host === 'github.com' || host.endsWith('.github.com')) return 'https://raw.githubusercontent.com/' + repo + '/HEAD/README.md';\n if (host === 'gitlab.com' || host.endsWith('.gitlab.com')) return 'https://gitlab.com/' + repo + '/-/raw/HEAD/README.md';\n if (host === 'codeberg.org') return 'https://codeberg.org/' + repo + '/raw/branch/HEAD/README.md';\n } catch (e) {}\n return null;\n};\n\nconst items = await Promise.all(dedup.map(async (h) => {\n const it = { id: h.objectID, title: h.title, url: link(h), points: h.points || 0, comments: h.num_comments || 0, cat: h._cat };\n const ru = readmeUrlFor(it.url);\n if (ru) {\n try {\n const r = await this.helpers.httpRequest({ url: ru, timeout: 8000 });\n it.readme = String(r).slice(0, 1200);\n } catch (e) {\n it.readmeErr = String((e && e.message) || e).slice(0, 80);\n }\n }\n return it;\n}));\nconst conReadme = items.filter(i => i.readme).length;\n\nconst sinceOld = $('Normalizza checkpoint').first().json.sinceEpoch;\nconst sinceNew = Math.max(sinceOld, ...all.map(h => h.created_at_i || 0));\nconst newItemCount = all.filter(h => (h.created_at_i || 0) > sinceOld).length;\n\nconst prompt = 'Sei un redattore tecnico italiano specializzato in software open source. ' +\n 'Ricevi un elenco di item da Hacker News. ALCUNI item hanno un campo \"readme\": il testo reale del README del progetto, scaricato dal repository. ' +\n 'Per OGNI item produci: name (nome breve; se il README rivela il vero nome del progetto, usa quello), ' +\n 'desc, points, comments, url IDENTICO a quello ricevuto. ' +\n 'REGOLE per desc: se l\\'item ha il campo readme, scrivi da 4 a 6 frasi in italiano (almeno 400 caratteri): ' +\n 'il problema che risolve, come funziona in pratica, a chi serve, un dettaglio tecnico notevole se presente. ' +\n 'Basa le frasi SOLO sul README: non inventare nulla. ' +\n 'Se il README mostra che il progetto non e\\' open source o e\\' il client di un servizio a pagamento, dillo esplicitamente. ' +\n 'Se l\\'item NON ha il campo readme, scrivi 2 frasi dal titolo e ANTICIPALE con il marcatore [non verificato]. ' +\n 'Niente marketing, niente emoji (eccetto il marcatore). ' +\n 'NON OMETTERE NESSUN ITEM: la risposta deve contenere esattamente ' + items.length + ' item in totale. ' +\n 'Raggruppa per categoria: show, repo, news, come nel campo cat. ' +\n 'Ordina ogni categoria per importanza. ' +\n 'Rispondi SOLO con JSON valido, forma esatta: ' +\n '{\"sections\":{\"show\":[{\"name\":\"\",\"desc\":\"\",\"points\":0,\"comments\":0,\"url\":\"\"}],\"repo\":[],\"news\":[]}} ' +\n '\\n\\nELENCO ITEM (JSON):\\n' + JSON.stringify(items);\n\nreturn [{ json: { items, count: items.length, conReadme, sinceOld, sinceNew, newItemCount, prompt } }];"
}
},
{
"id": "f082ed2c-c61a-49c7-b101-44be06b278e4",
"name": "Gemini riassume",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 10000,
"onError": "continueErrorOutput",
"position": [
880,
-100
],
"parameters": {
"method": "POST",
"url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={{ $('Credenziali').first().json.gemini_key }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ contents: [{ parts: [{ text: $json.prompt }] }], generationConfig: { responseMimeType: 'application/json', temperature: 0.4, maxOutputTokens: 30000, thinkingConfig: { thinkingBudget: 0 } } }) }}",
"options": {
"timeout": 300000
}
}
},
{
"id": "fbdc92fb-e069-4889-8789-83987ebad447",
"name": "Prepara messaggi",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1100,
-100
],
"parameters": {
"jsCode": "const pick = (name) => { try { const j = $(name).first().json; if (j && j.candidates) return j; } catch (e) {} return null; };\nconst resp = pick('Gemini riassume') || pick('Gemini lite (fallback)');\n\nconst raw = ((resp.candidates || [])[0] || {}).content;\nconst text = (raw && Array.isArray(raw.parts)) ? raw.parts.map(p => p.text || '').join('') : '';\nlet digest;\ntry { digest = JSON.parse(text); }\ncatch (e) {\n const m = text.match(/\\{[\\s\\S]*\\}/);\n if (!m) throw new Error('Gemini non ha restituito JSON valido: ' + text.slice(0, 300));\n digest = JSON.parse(m[0]);\n}\nconst meta = $('Seleziona OSS').first().json;\nconst secs = (digest && digest.sections) || {};\nconst total = ['show', 'repo', 'news'].reduce((n, k) => n + ((secs[k] || []).length), 0);\n\nconst date = new Date();\nconst quando = date.toLocaleString('it-IT', { weekday: 'long', day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Rome' });\nconst header = '\\ud83e\\udd8a DIGEST OPEN SOURCE da Hacker News\\n' + quando +\n '\\n' + meta.count + ' progetti e novita OSS delle ultime 12 ore (Show HN + repo + release). Zero tagli.' +\n (total !== meta.count ? '\\n(attenzione: Gemini ne ha restituiti ' + total + ' — eventuali mancanti non si riproporranno nella prossima finestra)' : '') + '\\n\\n';\n\nconst TITLES = { show: '\\ud83d\\ude80 NUOVI PROGETTI OPEN SOURCE (Show HN)', repo: '\\ud83d\\udd17 PROGETTI EMERSI NELLE DISCUSSIONI', news: '\\ud83d\\udcf0 RELEASE E NOVITA SU PROGETTI ESISTENTI' };\nconst blocks = [];\nfor (const k of ['show', 'repo', 'news']) {\n const arr = secs[k] || [];\n if (!arr.length) continue;\n blocks.push(TITLES[k]);\n for (const it of arr) {\n let line = '\\u2022 ' + (it.name || '?');\n if (it.desc) line += ' \\u2014 ' + it.desc;\n line += ' [' + (it.points || 0) + '\\u2191/' + (it.comments || 0) + '\\ud83d\\udcac]';\n blocks.push(line);\n blocks.push(it.url || '');\n }\n blocks.push('');\n}\nconst MAX = 3800;\nconst msgs = [];\nlet cur = header;\nfor (const b of blocks) {\n if (cur.length + b.length + 1 > MAX) { msgs.push(cur.trimEnd()); cur = '(segue)\\n' + b + '\\n'; }\n else cur += b + '\\n';\n}\nif (cur.trim()) msgs.push(cur.trimEnd());\n\nconst cred = $('Credenziali').first().json;\nreturn msgs.map((t, i) => ({ json: {\n chat_id: cred.telegram_chat_id, text: t,\n seq: i + 1, totale: msgs.length, sinceNew: meta.sinceNew,\n} }));"
}
},
{
"id": "e232c24f-b68c-4b75-985f-1da15ffe4f7f",
"name": "Invia su Telegram",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"position": [
1320,
-100
],
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $('Credenziali').first().json.telegram_token }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: Number($json.chat_id), text: $json.text }) }}",
"options": {
"timeout": 60000
}
}
},
{
"id": "ce380b59-8b69-4ab1-a2a2-4a8c6311f70d",
"name": "Aggiorna checkpoint",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"executeOnce": true,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"position": [
1540,
-100
],
"parameters": {
"method": "POST",
"url": "https://n8n.as89.it/api/v1/data-tables/qkbTUn1IKaltUfVo/rows",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-N8N-API-KEY",
"value": "={{ $('Credenziali').first().json.n8n_key }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ data: [{ sinceEpoch: $('Prepara messaggi').first().json.sinceNew }] }) }}",
"options": {
"timeout": 30000
}
}
},
{
"id": "c54b86c0-058b-48b3-9988-f80e0d5f6b6d",
"name": "Avvisa in chat Hermes",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
0,
140
],
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $('Credenziali').first().json.telegram_token }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: Number($('Credenziali').first().json.telegram_chat_id), text: 'Attiva il digest: apri t.me/Hn12h_bot e premi AVVIA, poi scrivi qui a Vale la parola via. (Messaggio automatico del workflow Digest OSS: chat ID non ancora configurato.)' }) }}",
"options": {
"timeout": 60000
}
}
},
{
"id": "aa3e286c-8675-4f05-bb31-3246d4013ca8",
"name": "Normalizza checkpoint",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
110,
-100
],
"parameters": {
"jsCode": "const rows = ($('Leggi checkpoint').first().json.data || []);\nconst sinceEpoch = rows.length ? Math.max(...rows.map(r => r.sinceEpoch || 0)) : 0;\nreturn [{ json: { sinceEpoch } }];"
}
},
{
"id": "c5e7e303-ef49-4bd1-a519-ada163a0ab77",
"name": "Gemini lite (fallback)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 15000,
"position": [
990,
60
],
"parameters": {
"method": "POST",
"url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent?key={{ $('Credenziali').first().json.gemini_key }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ contents: [{ parts: [{ text: $('Seleziona OSS').first().json.prompt }] }], generationConfig: { responseMimeType: 'application/json', temperature: 0.4, maxOutputTokens: 30000, thinkingConfig: { thinkingBudget: 0 } } }) }}",
"options": {
"timeout": 300000
}
}
}
],
"connections": {
"Ogni 12 ore": {
"main": [
[
{
"node": "Credenziali",
"type": "main",
"index": 0
}
]
]
},
"Credenziali": {
"main": [
[
{
"node": "Chat ID impostato?",
"type": "main",
"index": 0
}
]
]
},
"Chat ID impostato?": {
"main": [
[
{
"node": "Leggi checkpoint",
"type": "main",
"index": 0
}
],
[
{
"node": "Avvisa in chat Hermes",
"type": "main",
"index": 0
}
]
]
},
"Leggi checkpoint": {
"main": [
[
{
"node": "Normalizza checkpoint",
"type": "main",
"index": 0
}
]
]
},
"Prendi Show HN": {
"main": [
[
{
"node": "Prendi Tutte le Storie",
"type": "main",
"index": 0
}
]
]
},
"Prendi Tutte le Storie": {
"main": [
[
{
"node": "Seleziona OSS",
"type": "main",
"index": 0
}
]
]
},
"Seleziona OSS": {
"main": [
[
{
"node": "Gemini riassume",
"type": "main",
"index": 0
}
]
]
},
"Gemini riassume": {
"main": [
[
{
"node": "Prepara messaggi",
"type": "main",
"index": 0
}
],
[
{
"node": "Gemini lite (fallback)",
"type": "main",
"index": 0
}
]
]
},
"Prepara messaggi": {
"main": [
[
{
"node": "Invia su Telegram",
"type": "main",
"index": 0
}
]
]
},
"Invia su Telegram": {
"main": [
[
{
"node": "Aggiorna checkpoint",
"type": "main",
"index": 0
}
]
]
},
"Normalizza checkpoint": {
"main": [
[
{
"node": "Prendi Show HN",
"type": "main",
"index": 0
}
]
]
},
"Gemini lite (fallback)": {
"main": [
[
{
"node": "Prepara messaggi",
"type": "main",
"index": 0
}
]
]
}
},
"nodeGroups": [],
"authors": "Andrea Sarego",
"name": null,
"description": null,
"autosaved": false,
"createdAt": "2026-10-15T12:04:40.781Z",
"updatedAt": "2026-10-15T12:04:40.781Z",
"workflowPublishHistory": [
{
"id": 100,
"workflowId": "tZwGIY9rcdybQLPr",
"versionId": "0a920067-acb9-49ef-9fa7-fe8a7dfdd006",
"event": "activated",
"userId": "e3520907-ef65-42c2-a65a-27100f80e8c5",
"createdAt": "2026-10-15T12:04:41.034Z"
},
{
"id": 101,
"workflowId": "tZwGIY9rcdybQLPr",
"versionId": "0a920067-acb9-49ef-9fa7-fe8a7dfdd006",
"event": "deactivated",
"userId": "e3520907-ef65-42c2-a65a-27100f80e8c5",
"createdAt": "2026-10-15T12:04:43.159Z"
},
{
"id": 102,
"workflowId": "tZwGIY9rcdybQLPr",
"versionId": "0a920067-acb9-49ef-9fa7-fe8a7dfdd006",
"event": "activated",
"userId": "e3520907-ef65-42c2-a65a-27100f80e8c5",
"createdAt": "2026-10-15T12:04:46.358Z"
}
]
}
}