{ "nodes": [ { "id": "6aa059e4-e78f-4bbd-a707-994a39840f97", "name": "Create Session", "type": "n8n-nodes-base.httpRequest", "position": [ -520, 240 ], "parameters": { "url": "https://bsky.social/xrpc/com.atproto.server.createSession", "method": "POST", "options": {}, "sendBody": true, "bodyParameters": { "parameters": [ { "name": "identifier", "value": "youruser.bsky.social" }, { "name": "password", "value": "your-app-passord-here" } ] } }, "notesInFlow": true, "typeVersion": 4.1 }, { "id": "143e37b0-de79-4329-99a2-51484c9609a8", "name": "List followers", "type": "n8n-nodes-base.httpRequest", "position": [ -280, 240 ], "parameters": { "url": "https://bsky.social/xrpc/app.bsky.graph.getFollowers", "options": { "response": { "response": { "responseFormat": "json" } }, "pagination": { "pagination": { "parameters": { "parameters": [ { "name": "cursor", "value": "={{ $response.body.cursor }}" } ] }, "maxRequests": 2, "requestInterval": 250, "limitPagesFetched": true } } }, "sendQuery": true, "sendHeaders": true, "queryParameters": { "parameters": [ { "name": "actor", "value": "={{ $json.did }}" }, { "name": "limit", "value": "100" } ] }, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "=Bearer {{ $item(\"0\").$node[\"Create Session\"].json[\"accessJwt\"] }}" } ] } }, "typeVersion": 4.2 }, { "id": "f1436a63-a23f-4082-9209-12c21a26ad91", "name": "Convert to File", "type": "n8n-nodes-base.convertToFile", "position": [ 100, 620 ], "parameters": { "options": { "fileName": "followers-basuracero.json" }, "operation": "toJson" }, "typeVersion": 1.1 }, { "id": "f8beea47-6f36-4dfb-b2e7-bf94adb63e66", "name": "Extract from File", "type": "n8n-nodes-base.extractFromFile", "position": [ 100, 240 ], "parameters": { "options": {}, "operation": "fromJson" }, "typeVersion": 1 }, { "id": "41658372-3054-4909-850b-3bffd1b1b79c", "name": "Split Out", "type": "n8n-nodes-base.splitOut", "position": [ 520, 240 ], "parameters": { "options": { "destinationFieldName": "did" }, "fieldToSplitOut": "newDids" }, "typeVersion": 1 }, { "id": "c94aa8e9-06db-4b24-a20a-5615b7129023", "name": "Loop Over Items", "type": "n8n-nodes-base.splitInBatches", "position": [ 740, 240 ], "parameters": { "options": {} }, "typeVersion": 3 }, { "id": "4d1c6a2f-3acd-4783-96d4-693ced06fd97", "name": "Wait", "type": "n8n-nodes-base.wait", "position": [ -100, 620 ], "webhookId": "b1608475-db84-4f23-acd6-d003f5094afd", "parameters": {}, "typeVersion": 1.1 }, { "id": "e4125cb8-8eb5-4cf0-b00d-e7ce4ec8236e", "name": "Find new followers", "type": "n8n-nodes-base.code", "position": [ 280, 240 ], "parameters": { "jsCode": "// Datos de entrada\nconst listFollowers = $('List followers').all()[0].json.followers;\nconst extractFromFile = $('Extract from File').all()[0].json.data[0].followers;\n\n// Verificar que tenemos acceso a los datos\nconsole.log('listFollowers length:', Array.isArray(listFollowers) ? listFollowers.length : 'no es array');\nconsole.log('extractFromFile length:', Array.isArray(extractFromFile) ? extractFromFile.length : 'no es array');\n\n// Mostrar algunos ejemplos de cada lista\nconsole.log('Ejemplo de listFollowers:', listFollowers?.slice(0, 2));\nconsole.log('Ejemplo de extractFromFile:', extractFromFile?.slice(0, 2));\n\n// Crear conjunto de DIDs del archivo extraído\nconst existingDids = new Set(extractFromFile?.map(item => item.did) || []);\nconsole.log('DIDs existentes:', Array.from(existingDids).slice(0, 5));\n\n// Filtrar listFollowers\nconst newFollowers = listFollowers?.filter(follower => !existingDids.has(follower.did)) || [];\n\nreturn {\n json: {\n debug: {\n listFollowersCount: listFollowers?.length || 0,\n extractFromFileCount: extractFromFile?.length || 0,\n existingDidsCount: existingDids.size,\n newFollowersCount: newFollowers.length\n },\n newFollowers,\n newDids: newFollowers.map(follower => follower.did),\n count: newFollowers.length\n }\n}" }, "typeVersion": 2, "alwaysOutputData": true }, { "id": "a28accb5-ee47-431f-83e4-376425e9899e", "name": "Read followers from file", "type": "n8n-nodes-base.readWriteFile", "position": [ -80, 240 ], "parameters": { "options": {}, "fileSelector": "=followers-{{ $('Create Session').item.json.handle }}.json" }, "typeVersion": 1 }, { "id": "aa2ab5e1-eb6f-4657-9a9a-66417ffa421e", "name": "Save followers to file", "type": "n8n-nodes-base.readWriteFile", "position": [ 280, 620 ], "parameters": { "options": { "append": false }, "fileName": "=followers-{{ $('Create Session').item.json.handle }}.json", "operation": "write" }, "typeVersion": 1 }, { "id": "9a4fb5e5-f2f6-4aa1-846e-8d6460ad7765", "name": "Define welcome message", "type": "n8n-nodes-base.set", "position": [ 980, 260 ], "parameters": { "options": {}, "assignments": { "assignments": [ { "id": "afe7fe9b-3bd4-4429-afe9-81e5fe934e07", "name": "text", "type": "string", "value": "Hello, thanks for your follow. You can read more about my over my site:" }, { "id": "97590cd1-9d85-442b-baa3-bad849ff9be0", "name": "link", "type": "string", "value": "https://yoursite.com" } ] } }, "typeVersion": 3.4 }, { "id": "594ce66f-acbd-4c31-806c-382aa9a98ed0", "name": "Sticky Note2", "type": "n8n-nodes-base.stickyNote", "position": [ 920, 160 ], "parameters": { "width": 230, "height": 266, "content": "### 2. Define your welcome message and link here" }, "typeVersion": 1 }, { "id": "c24a7971-11a7-4164-9f2d-78335264f250", "name": "Sticky Note3", "type": "n8n-nodes-base.stickyNote", "position": [ 220, 460 ], "parameters": { "width": 231, "height": 338, "content": "### 3. **Important** \n\nYou need to manually run \"Save followers to file\" once before the first time so you populate your list of existing followers" }, "typeVersion": 1 }, { "id": "c6e766cf-a118-4db0-8e3c-32662c40737b", "name": "Send message", "type": "n8n-nodes-base.httpRequest", "position": [ 1360, 260 ], "parameters": { "url": "={{ $item(\"0\").$node[\"Create Session\"].json.didDoc.service[0].serviceEndpoint }}/xrpc/chat.bsky.convo.sendMessage", "method": "POST", "options": {}, "jsonBody": "={\n \"convoId\" : \"{{ $json.convo.id }}\",\n \"message\" : {\n \"text\" : \"{{ $('Define welcome message').item.json.text }}\\n\\n{{ $('Define welcome message').item.json.link }}\",\n \"facets\" : [\n {\n \"index\" : {\n \"byteStart\": {{ $('Define welcome message').item.json.text.length }},\n \"byteEnd\": {{ $('Define welcome message').item.json.text.length + 3 + $('Define welcome message').item.json.link.length}}\n },\n \"features\": [\n {\n \"$type\": \"app.bsky.richtext.facet#link\",\n \"uri\": \"{{ $('Define welcome message').item.json.link }}\"\n }\n ]\n }\n ]\n }\n}", "sendBody": true, "sendHeaders": true, "specifyBody": "json", "headerParameters": { "parameters": [ { "name": "Authorization", "value": "=Bearer {{ $item(\"0\").$node[\"Create Session\"].json[\"accessJwt\"] }}" }, { "name": "Atproto-Proxy", "value": "did:web:api.bsky.chat#bsky_chat" } ] } }, "typeVersion": 4.2 }, { "id": "33aa7e0c-58fe-4527-a94e-49bec0e06325", "name": "Get conversation ID", "type": "n8n-nodes-base.httpRequest", "position": [ 1200, 260 ], "parameters": { "url": "={{ $item(\"0\").$node[\"Create Session\"].json.didDoc.service[0].serviceEndpoint }}/xrpc/chat.bsky.convo.getConvoForMembers", "options": {}, "sendQuery": true, "sendHeaders": true, "queryParameters": { "parameters": [ { "name": "members", "value": "={{ $('Split Out').item.json.did }}" } ] }, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "=Bearer {{ $item(\"0\").$node[\"Create Session\"].json[\"accessJwt\"] }}" }, { "name": "Atproto-Proxy", "value": "did:web:api.bsky.chat#bsky_chat" } ] } }, "typeVersion": 4.2 }, { "id": "066f1d0d-319a-4676-9c0a-5b00d206ffd2", "name": "Sticky Note", "type": "n8n-nodes-base.stickyNote", "position": [ -40, -100 ], "parameters": { "color": 5, "width": 479, "height": 307, "content": "## Send a welcome private message to your new BlueSky followers\n\nThis flow will save your current followers in a file and check for new ones on the next execution, sending them the Defined message an link as a private message.\n\nOnce messages are sent, the new list of followers will be saved into the file.\n\n**Important: Follow the yellow notes in order before enabling the full flow for the first time**" }, "typeVersion": 1 }, { "id": "a61e301e-a0ca-48e9-9a46-05975662aa90", "name": "Sticky Note1", "type": "n8n-nodes-base.stickyNote", "position": [ -560, 40 ], "parameters": { "width": 181, "height": 364, "content": "### 1. Define your Bluesky user and app password first\n\nThe App password should have access to private messages" }, "typeVersion": 1 }, { "id": "1b1dcd74-7a71-49e4-99e8-c079b692aca5", "name": "Each 60 minutes", "type": "n8n-nodes-base.scheduleTrigger", "position": [ -720, 240 ], "parameters": { "rule": { "interval": [ { "field": "minutes", "minutesInterval": 60 } ] } }, "typeVersion": 1.2 }, { "id": "6acf153c-cdc6-42c1-85f9-2692c8777eef", "name": "No Operation, do nothing", "type": "n8n-nodes-base.noOp", "position": [ 520, 620 ], "parameters": {}, "typeVersion": 1 } ], "pinData": {}, "connections": { "Wait": { "main": [ [ { "node": "Convert to File", "type": "main", "index": 0 } ] ] }, "Split Out": { "main": [ [ { "node": "Loop Over Items", "type": "main", "index": 0 } ] ] }, "Send message": { "main": [ [ { "node": "Loop Over Items", "type": "main", "index": 0 } ] ] }, "Create Session": { "main": [ [ { "node": "List followers", "type": "main", "index": 0 } ] ] }, "List followers": { "main": [ [ { "node": "Read followers from file", "type": "main", "index": 0 }, { "node": "Wait", "type": "main", "index": 0 } ] ] }, "Convert to File": { "main": [ [ { "node": "Save followers to file", "type": "main", "index": 0 } ] ] }, "Each 60 minutes": { "main": [ [ { "node": "Create Session", "type": "main", "index": 0 } ] ] }, "Loop Over Items": { "main": [ [], [ { "node": "Define welcome message", "type": "main", "index": 0 } ] ] }, "Extract from File": { "main": [ [ { "node": "Find new followers", "type": "main", "index": 0 } ] ] }, "Find new followers": { "main": [ [ { "node": "Split Out", "type": "main", "index": 0 } ] ] }, "Get conversation ID": { "main": [ [ { "node": "Send message", "type": "main", "index": 0 } ] ] }, "Define welcome message": { "main": [ [ { "node": "Get conversation ID", "type": "main", "index": 0 } ] ] }, "Save followers to file": { "main": [ [ { "node": "No Operation, do nothing", "type": "main", "index": 0 } ] ] }, "Read followers from file": { "main": [ [ { "node": "Extract from File", "type": "main", "index": 0 } ] ] } } }