Posting to BlueSky using Node-RED

So after a few hours trying to glean the information from T'internet, I've managed to make a basic post to BlueSky

1st thing is to go into BluSky account settings,select App password and generate a password (save it)

Then you use can use this flow
image
(insert app password in the inject string)

[{"id":"fddce66d9654b9a3","type":"inject","z":"dd99f8450c6d098a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"identifier\":\"YOURHANDLE.bsky.social\",\"password\":\"xxxx\"}","payloadType":"str","x":200,"y":530,"wires":[["1ae222637db7a145"]]},{"id":"f02fd48eaa6ff102","type":"debug","z":"dd99f8450c6d098a","name":"debug 449","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":760,"y":530,"wires":[]},{"id":"3d1521b467752377","type":"http request","z":"dd99f8450c6d098a","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"https://bsky.social/xrpc/com.atproto.server.createSession","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"Content-Type","keyValue":"","valueType":"application/json","valueValue":""}],"x":520,"y":530,"wires":[["f02fd48eaa6ff102"]]},{"id":"1ae222637db7a145","type":"json","z":"dd99f8450c6d098a","name":"","property":"payload","action":"","pretty":false,"x":360,"y":530,"wires":[["3d1521b467752377"]]}]

This should return you a message with a did and an accessJwt

Then use this flow to post a message
image

(alter http node to add in the accessJwt string as Bearer auth)
(insert did string in inject node)

[{"id":"a8b23b1969605d3f","type":"inject","z":"dd99f8450c6d098a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{   \"repo\": \"did:plc:xxxx\",   \"collection\": \"app.bsky.feed.post\",   \"record\": {       \"$type\": \"app.bsky.feed.post\",       \"text\": \"This is your post text\",       \"createdAt\": \"2024-11-18T11:44:49.658Z\"   } }","payloadType":"str","x":200,"y":610,"wires":[["ed2b735365beb1d0"]]},{"id":"9f3ebbef803fc516","type":"debug","z":"dd99f8450c6d098a","name":"debug 450","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":760,"y":610,"wires":[]},{"id":"890dc231d4abbd2f","type":"http request","z":"dd99f8450c6d098a","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"https://bsky.social/xrpc/com.atproto.repo.createRecord","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"Content-Type","keyValue":"","valueType":"application/json","valueValue":""},{"keyType":"Authorization","keyValue":"","valueType":"other","valueValue":"Bearer eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NksifQ.eyJzY29wZSI6ImNvbS5hdHByb3RvLmFwcFBhc3NQcml2aWxlZ2VkIiwic3ViIjoiZGlkOnBsYzp4bHF3dXJ2dmZjZHg3ZDZ2YTRrd3Nvc3UiLCJpYXQiOjE3MzE5Mjc0NTUsImV4cCI6MTczMTkzNDY1NSwiYXVkIjoiZGlkOndlYjptYWl0YWtlLnVzLXdlc3QuaG9zdC5ic2t5Lm5ldHdvcmsifQ.K74mRkbkfESUt5PMftKe_s44Tetec2e5uuL_v4OmD5Tny4r2WNsB_D5uTMFQMIhZ5D38rVdmDpdS_3eshrF-nQ"}],"x":560,"y":610,"wires":[["9f3ebbef803fc516"]]},{"id":"ed2b735365beb1d0","type":"json","z":"dd99f8450c6d098a","name":"","property":"payload","action":"","pretty":false,"x":360,"y":610,"wires":[["890dc231d4abbd2f"]]}]

And it should make a post

Obviously this is all very manual and industrial POC :slight_smile:

5 Likes

Thanks for breaking ground on this. :slight_smile:

I think I'm going to see if I can auto-post new/updated blog posts to my bsky account. Won't be using Node-RED for that though probably since I'll want the build process - which runs on Cloudflare pages - to do it at build time. If I can't do that, I'll monitor my own site using Node-RED and then post from there.

More refined solution - all variables are in the inject node

[{"id":"f6871b4e1aa213a6","type":"inject","z":"dd99f8450c6d098a","name":"","props":[{"p":"text","v":"This is  test post using Node RED","vt":"str"},{"p":"name","v":"name.bsky.social","vt":"str"},{"p":"appPassword","v":"xxxx","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":170,"y":420,"wires":[["7610b4a9a9d2acc2"]]},{"id":"b372b4db22cc3b08","type":"debug","z":"dd99f8450c6d098a","name":"debug 453","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1020,"y":470,"wires":[]},{"id":"26da7c2cbf874038","type":"http request","z":"dd99f8450c6d098a","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"https://bsky.social/xrpc/com.atproto.server.createSession","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"Content-Type","keyValue":"","valueType":"application/json","valueValue":""}],"x":640,"y":420,"wires":[["ae8f8676f19ea089","aad6e2ec64ae6d05"]]},{"id":"0dd74070eaad543e","type":"json","z":"dd99f8450c6d098a","name":"","property":"payload","action":"","pretty":false,"x":490,"y":420,"wires":[["26da7c2cbf874038","63a3342de6697c6e"]]},{"id":"5d3dcdb6c2d70ac6","type":"debug","z":"dd99f8450c6d098a","name":"debug 454","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1030,"y":520,"wires":[]},{"id":"f6ab72588f1cd251","type":"http request","z":"dd99f8450c6d098a","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"https://bsky.social/xrpc/com.atproto.repo.createRecord","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"Content-Type","keyValue":"","valueType":"application/json","valueValue":""}],"x":660,"y":520,"wires":[["5d3dcdb6c2d70ac6"]]},{"id":"a4575fe3c3a7fda8","type":"json","z":"dd99f8450c6d098a","name":"","property":"payload","action":"obj","pretty":false,"x":190,"y":520,"wires":[["230dbb7e57bcfb7d"]]},{"id":"afbf8e17a961ff95","type":"debug","z":"dd99f8450c6d098a","name":"debug 455","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":660,"y":570,"wires":[]},{"id":"5aa74fb53755a2e1","type":"template","z":"dd99f8450c6d098a","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{ \"repo\": \"{{flow.did}}\",\"collection\": \"app.bsky.feed.post\",   \"record\": {       \"$type\": \"app.bsky.feed.post\",\"text\": \"{{text}}\",\"createdAt\": \"{{myymd}}T{{mytimes}}.{{mymillis}}Z\"   } }","output":"str","x":580,"y":470,"wires":[["a4575fe3c3a7fda8","b372b4db22cc3b08"]]},{"id":"32bb10207125f7e9","type":"simpletime","z":"dd99f8450c6d098a","name":"","mydate":false,"myymd":true,"myyear":false,"mymonth":false,"mymonthn":false,"mydom":false,"mydoy":false,"myday":false,"myhourpm":false,"myhour":false,"mytime":false,"mytimes":true,"myminute":false,"myminutes":false,"mysecond":false,"mymillis":true,"myepoch":false,"myrawdate":false,"mypm":false,"x":410,"y":470,"wires":[["5aa74fb53755a2e1"]]},{"id":"ae8f8676f19ea089","type":"change","z":"dd99f8450c6d098a","name":"","rules":[{"t":"set","p":"did","pt":"flow","to":"payload.did","tot":"msg","dc":true},{"t":"set","p":"accessJwt","pt":"flow","to":"payload.accessJwt","tot":"msg","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":840,"y":420,"wires":[["8a88406b2a5b1090"]]},{"id":"8a88406b2a5b1090","type":"change","z":"dd99f8450c6d098a","name":"","rules":[{"t":"delete","p":"headers","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":210,"y":470,"wires":[["32bb10207125f7e9"]]},{"id":"230dbb7e57bcfb7d","type":"template","z":"dd99f8450c6d098a","name":"","field":"headers","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{ \"Authorization\":\"Bearer {{flow.accessJwt}}\" }","output":"str","x":340,"y":520,"wires":[["b676a50613a5bc22"]]},{"id":"b676a50613a5bc22","type":"json","z":"dd99f8450c6d098a","name":"","property":"headers","action":"obj","pretty":false,"x":480,"y":520,"wires":[["afbf8e17a961ff95","f6ab72588f1cd251"]]},{"id":"7610b4a9a9d2acc2","type":"template","z":"dd99f8450c6d098a","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\"identifier\":\"{{name}}\",\"password\":\"{{appPassword}}\"}","output":"str","x":330,"y":420,"wires":[["0dd74070eaad543e"]]},{"id":"63a3342de6697c6e","type":"debug","z":"dd99f8450c6d098a","name":"debug 456","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":640,"y":370,"wires":[]},{"id":"aad6e2ec64ae6d05","type":"debug","z":"dd99f8450c6d098a","name":"debug 457","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":830,"y":370,"wires":[]}]

Doesn't handle putting any links #tags or handles in the text - just plain text only at moment

Commentary on whats going on
You edit inject node with text you want to post, the bksy.social handle - cymplecy.bsky.social in my case and an app password (as explained in 1st post)

It then sends a http request off that returns some values that are needed to do the real post.

I then add in the time and the other variable values needed and make a 2nd http request (to a different endpoint) that does the actual post

A lot of the information I used to find this out came from here

[edit]19Nov 2024
If you want to include mentions of other accounts and #tags - there's another level of complexity to goto -I've done it manually but now trying to do it within the flow!

1 Like

Latest version now handles mentions and hashtags (LIMITATION if you use one - then you need to use both)


Note I used Blockly as my function node but it wouldn't upload to the forum as it exceed 32000 char limit so I've copied the JS it generates into a function node for here

[{"id":"ba432f16a297f974","type":"inject","z":"dd99f8450c6d098a","name":"green","props":[{"p":"text","v":"@cheerlights.bsky.social #cheerlights This is test post using NodeRED - green grass","vt":"str"},{"p":"name","v":"xxxx.bsky.social","vt":"str"},{"p":"appPassword","v":"xxxxxxx","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":140,"y":600,"wires":[["8b34ec1fff3ecef6"]]},{"id":"931227442ccb14dd","type":"debug","z":"dd99f8450c6d098a","name":"debug 464","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1180,"y":720,"wires":[]},{"id":"1467e34f3a46fe31","type":"http request","z":"dd99f8450c6d098a","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"https://bsky.social/xrpc/com.atproto.server.createSession","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"Content-Type","keyValue":"","valueType":"application/json","valueValue":""}],"x":710,"y":670,"wires":[["3c32c66dcd937c19"]]},{"id":"f0ea247730c98478","type":"json","z":"dd99f8450c6d098a","name":"","property":"payload","action":"","pretty":false,"x":570,"y":630,"wires":[["1467e34f3a46fe31","c2cb04d260412407"]]},{"id":"6143d043c66e6d8c","type":"debug","z":"dd99f8450c6d098a","name":"debug 465","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"statusCode","statusType":"msg","x":1180,"y":810,"wires":[]},{"id":"bb3f01506e35f79e","type":"http request","z":"dd99f8450c6d098a","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"https://bsky.social/xrpc/com.atproto.repo.createRecord","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"Content-Type","keyValue":"","valueType":"application/json","valueValue":""}],"x":790,"y":810,"wires":[["6143d043c66e6d8c"]]},{"id":"e53f5f7745109a5a","type":"json","z":"dd99f8450c6d098a","name":"","property":"payload","action":"obj","pretty":false,"x":320,"y":770,"wires":[["3ebcd3d1c1454446"]]},{"id":"62a77df00a14da2a","type":"debug","z":"dd99f8450c6d098a","name":"debug 466","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1180,"y":770,"wires":[]},{"id":"9d0922b21146d9e5","type":"template","z":"dd99f8450c6d098a","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{ \"repo\": \"{{flow.did}}\",\"collection\": \"app.bsky.feed.post\",   \"record\": {       \"$type\": \"app.bsky.feed.post\",\"text\": \"{{text}}\",\"createdAt\": \"{{myymd}}T{{mytimes}}.{{mymillis}}Z\"\n\n{{{facets}}}\n\n   } }","output":"str","x":850,"y":720,"wires":[["931227442ccb14dd","e53f5f7745109a5a"]]},{"id":"027f19aca8f3549f","type":"simpletime","z":"dd99f8450c6d098a","name":"","mydate":false,"myymd":true,"myyear":false,"mymonth":false,"mymonthn":false,"mydom":false,"mydoy":false,"myday":false,"myhourpm":false,"myhour":false,"mytime":false,"mytimes":true,"myminute":false,"myminutes":false,"mysecond":false,"mymillis":true,"myepoch":false,"myrawdate":false,"mypm":false,"x":680,"y":720,"wires":[["9d0922b21146d9e5"]]},{"id":"3c32c66dcd937c19","type":"change","z":"dd99f8450c6d098a","name":"","rules":[{"t":"set","p":"did","pt":"flow","to":"payload.did","tot":"msg","dc":true},{"t":"set","p":"accessJwt","pt":"flow","to":"payload.accessJwt","tot":"msg","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":890,"y":670,"wires":[["9d215bf0bde7d990","70f22979df50dff3"]]},{"id":"70f22979df50dff3","type":"change","z":"dd99f8450c6d098a","name":"","rules":[{"t":"delete","p":"headers","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":720,"wires":[["027f19aca8f3549f"]]},{"id":"3ebcd3d1c1454446","type":"template","z":"dd99f8450c6d098a","name":"","field":"headers","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{ \"Authorization\":\"Bearer {{flow.accessJwt}}\" }","output":"str","x":470,"y":770,"wires":[["e3543264d4471753"]]},{"id":"e3543264d4471753","type":"json","z":"dd99f8450c6d098a","name":"","property":"headers","action":"obj","pretty":false,"x":620,"y":770,"wires":[["62a77df00a14da2a","bb3f01506e35f79e"]]},{"id":"1a9817da605c639e","type":"template","z":"dd99f8450c6d098a","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\"identifier\":\"{{name}}\",\"password\":\"{{appPassword}}\"}","output":"str","x":440,"y":630,"wires":[["f0ea247730c98478"]]},{"id":"c2cb04d260412407","type":"debug","z":"dd99f8450c6d098a","name":"debug 467","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1180,"y":630,"wires":[]},{"id":"9d215bf0bde7d990","type":"debug","z":"dd99f8450c6d098a","name":"debug 468","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1180,"y":670,"wires":[]},{"id":"22f90199bd33d7ad","type":"debug","z":"dd99f8450c6d098a","name":"debug 469","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1180,"y":580,"wires":[]},{"id":"8c3aec09fc23ed15","type":"switch","z":"dd99f8450c6d098a","name":"","property":"url","propertyType":"msg","rules":[{"t":"nempty"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":490,"y":550,"wires":[["8e0c6d1e7836e79a"],["45929b74029dac0a"]]},{"id":"8e0c6d1e7836e79a","type":"http request","z":"dd99f8450c6d098a","name":"","method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":630,"y":500,"wires":[["340d28fba5b3725a","be81b2a0b4047cd1"]]},{"id":"340d28fba5b3725a","type":"change","z":"dd99f8450c6d098a","name":"","rules":[{"t":"set","p":"mentionDid","pt":"flow","to":"payload.did","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":540,"wires":[["56b8cfc4cfcdeb84","66d96ecb386154c2"]]},{"id":"56b8cfc4cfcdeb84","type":"template","z":"dd99f8450c6d098a","name":"","field":"facets","fieldType":"msg","format":"handlebars","syntax":"mustache","template":",\n\"facets\": [\n{\n    \"index\": {\n        \"byteStart\": {{mentionStart}},\n        \"byteEnd\": {{mentionEnd}}\n    },\n    \"features\": [\n        {\n            \"$type\": \"app.bsky.richtext.facet#mention\",\n            \"did\": \"{{flow.mentionDid}}\"\n        }\n    ]\n},\n    {\n      \"index\": {\n        \"byteStart\": {{hashtagStart}},\n        \"byteEnd\": {{hashtagEnd}}\n         },\n      \"features\": [\n        {\n          \"$type\": \"app.bsky.richtext.facet#tag\",\n          \"tag\": \"{{hashtag}}\"\n        }\n      ]\n    }\n    ]","output":"str","x":660,"y":580,"wires":[["b2bd616a8f37767c"]]},{"id":"b2bd616a8f37767c","type":"change","z":"dd99f8450c6d098a","name":"","rules":[{"t":"delete","p":"url","pt":"msg"},{"t":"delete","p":"headers","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":580,"wires":[["22f90199bd33d7ad","1a9817da605c639e"]]},{"id":"5dcdb13abc220f42","type":"inject","z":"dd99f8450c6d098a","name":"plain","props":[{"p":"text","v":"This is test post using NodeRED","vt":"str"},{"p":"name","v":"xxxxx.bsky.social","vt":"str"},{"p":"appPassword","v":"xxxxxx","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":140,"y":560,"wires":[["8b34ec1fff3ecef6"]]},{"id":"66d96ecb386154c2","type":"debug","z":"dd99f8450c6d098a","name":"debug 470","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1180,"y":540,"wires":[]},{"id":"be81b2a0b4047cd1","type":"debug","z":"dd99f8450c6d098a","name":"debug 471","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":1180,"y":500,"wires":[]},{"id":"2c73ebab576739d5","type":"comment","z":"dd99f8450c6d098a","name":"Requires simpletime node","info":"","x":570,"y":460,"wires":[]},{"id":"a017dc0cd32db1dd","type":"function","z":"dd99f8450c6d098a","name":"Blocky generated JS","func":"var msgText, mentionStart, hashtagStart, mentionEnd, hashtagEnd, hashtag, facets;\n\n\nmsgText = String(msg['text']) + ' ';\nmentionStart = msgText.indexOf('@') + 1;\nhashtagStart = msgText.indexOf('#') + 1;\nif (mentionStart > 0) {\n  mentionEnd = msgText.slice((mentionStart - 1), msgText.length).indexOf(' ') + 1;\n  msg['url'] = ('https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=' + String(msgText.slice(((mentionStart + 1) - 1), mentionEnd - 1)));\n}\nif (hashtagStart > 0) {\n  hashtagEnd = msgText.slice((hashtagStart - 1), msgText.length).indexOf(' ') + 1;\n  hashtagEnd = hashtagStart + (hashtagEnd - 1);\n  hashtag = msgText.slice(((hashtagStart + 1) - 1), hashtagEnd - 1);\n  msg['hashtag'] = hashtag;\n  node.status({fill:\"blue\", shape:\"ring\", text:hashtag});\n}\nmsg['mentionStart'] = (mentionStart - 1);\nmsg['mentionEnd'] = (mentionStart + (mentionEnd - 2));\nmsg['hashtagStart'] = (hashtagStart - 1);\nmsg['hashtagEnd'] = (hashtagEnd - 1);\nfacets = '';\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":500,"wires":[["8c3aec09fc23ed15"]]},{"id":"8b34ec1fff3ecef6","type":"junction","z":"dd99f8450c6d098a","x":250,"y":540,"wires":[["a017dc0cd32db1dd"]]},{"id":"45929b74029dac0a","type":"junction","z":"dd99f8450c6d098a","x":310,"y":590,"wires":[["1a9817da605c639e"]]}]

Note: the position of mentions and hashtags within a skeet have to be added on to the posted message as something called facets otherwise they just end up as plain text.
Further more, you have to look up something called the did which is BlueSky's internal id of a handle but at least that is easy to do

e.g simple get request that doesn't need authorisation
https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=cymplecy.bsky.social

1 Like

Nicer version

NRtoBlueSky.json (98.1 KB)

Set your name and app password in the top inject, deploy and inject to set them up

This flow can handle a mention and/or a tag and/or a link (only 1 of each at moment)
[edit]Updated 21Nov2024 to handle a link in the text