
Desde siempre me ha gustado crear bots en diferentes plataformas, con distintas funciones, pero siempre bots.
Empecemos por el principio: ¿qué es un bot? Si buscamos la definición en la RAE:
“Programa que imita el comportamiento humano.”
¿Por qué Bluesky?
Hace casi un año dejé Twitter y me mudé a Bluesky. Como en todos los inicios en redes sociales, faltaban cosas.
Para mí, Twitter —además de la gente con la que he conectado— lo usaba principalmente para informarme (al menos en los primeros años) sobre la actualidad en tiempo real.
Al empezar en Bluesky, apenas había cuentas de periódicos o medios de noticias con las que informarme, así que decidí montar algunos bots que publicaran automáticamente las noticias que aparecían en sus portales web.
¿Cómo lo hice?
Para hacer esto hay varias formas, pero la más legal es usar su propio feed RSS para ir publicando lo que los medios dejan accesible para el uso de terceros (como yo).
He visto otros ejemplos en la red que hacen mirrors de cuentas de Twitter y republican lo que se publica en esos perfiles.
Funciona, pero el tema de derechos de autor y otras cuestiones no me convencían. De hecho, han tenido un recorrido bastante corto en Bluesky.
Pasos para crear un bot de noticias
- Crear una cuenta de Bluesky (lógico).
- Generar una App Password para poder usarla desde fuera.
- Elegir un feed RSS con la información que quieres publicar.
- Configurar un script que:
- Lea el feed RSS.
- Prepare los posts.
- Los publique automáticamente cada cierto tiempo.
Mi caso: tres bots, tres periódicos
Opté por hacer tres bots y después de un año, estos son sus números:
- 📰
@deia_bot.bsky.social
438 followers - 20.1k posts. - 🗞️
@elcorreo_bot.bsky.social
2.9k followers - 19.1k posts. - ⚽
@marca_bot.bsky.social
285 followers - 11.8k posts.
Todos son periódicos con feed RSS para publicar sus noticias y, en el momento en que los creé, no tenían cuenta en Bluesky. Ahora algunos de ellos ya tienen cuentas oficiales, pero con poca actividad.
Realmente, el script que da vida al bot es el mismo en todos los casos; lo único que cambia es el feed y las claves para usar la cuenta desde fuera.
Copy & paste de toda la vida.
Por aquí os dejo el código para escribir un simple mensaje usando la API de Bluesky:
📤 Código para publicar en Bluesky un simple mensaje
function postToBluesky() {
const handle = "TU_HANDLE";
const appPassword = "TU_APP_PASSWORD";
const postText = "¡Hola desde Google Apps Script! 🚀 #Bluesky";
const loginResponse = UrlFetchApp.fetch("https://bsky.social/xrpc/com.atproto.server.createSession", {
method: "post",
contentType: "application/json",
payload: JSON.stringify({
identifier: handle,
password: appPassword
})
});
const session = JSON.parse(loginResponse.getContentText());
const accessJwt = session.accessJwt;
const did = session.did;
const postPayload = {
repo: did,
collection: "app.bsky.feed.post",
record: {
$type: "app.bsky.feed.post",
text: postText,
createdAt: new Date().toISOString()
}
};
const postResponse = UrlFetchApp.fetch("https://bsky.social/xrpc/com.atproto.repo.createRecord", {
method: "post",
contentType: "application/json",
headers: {
Authorization: `Bearer ${accessJwt}`
},
payload: JSON.stringify(postPayload)
});
Logger.log(postResponse.getContentText());
}
💡 Consejo: Sustituye
"TU_APP_PASSWORD"
por la contraseña de aplicación que generes en https://bsky.app/settings/app-passwords y"TU_HANDLE"
con el valor de la cuenta desde la que publiques.
Para poder escribir los mensajes de un feed RSS, es necesario hacer cambios en el script. En mi caso, empecé a partir del siguiente script:
📤 Código para publicar en Bluesky un feed RSS
const RSS_FEED = 'YOUR FEED URL'
const HANDLE='YOUR HANDLE';
const DID_URL="https://bsky.social/xrpc/com.atproto.identity.resolveHandle";
const APP_PASSWORD = "YOUR APP PASSWORD";
const API_KEY_URL= "https://bsky.social/xrpc/com.atproto.server.createSession";
const FEED_URL="https://bsky.social/xrpc/app.bsky.feed.getAuthorFeed";
const POST_FEED_URL = "https://bsky.social/xrpc/com.atproto.repo.createRecord";
const UPLOAD_IMG_URL = "https://bsky.social/xrpc/com.atproto.repo.uploadBlob";
function setupTrigger(){ // To be run to create trigger
ScriptApp.newTrigger('publishFromRSS').timeBased().everyMinutes(15).create();
}
function publishFromRSS() {
const url = RSS_FEED
let rep = UrlFetchApp.fetch(url,{ muteHttpExceptions: true})
if(rep.getResponseCode() != 200){
console.log('Error : '+ rep.getContentText())
return ;
}
// PropertiesService.getScriptProperties().deleteProperty('LINKS') ; // Uncomment to erase property and restart from 0
let linkDone = JSON.parse(PropertiesService.getScriptProperties().getProperty('LINKS')) || {"items":[],"lastRun": new Date().getTime(),"init":true}
const auth = BlueskyAuth();
const xml = XmlService.parse(rep.getContentText());
const root = xml.getRootElement();
const channel = root.getChildren('channel')
const entries = channel[0].getChildren("item");
var newArrayLink = []
for(var i = 0 ; i < entries.length ; i++){
let entry = entries[i]
let link = entry.getChild("link").getValue();
let title = entry.getChild("title").getValue();
if(linkDone.items.indexOf(link)<0 ){
publishNews(title,link,auth)
// linkDone.items.unshift(link)
// if(!linkDone.init){ linkDone.items.pop()}
}
newArrayLink.push(link)
// return false; // Uncomment to do just one publication
}
if(linkDone.init){ linkDone.init = false ;}
linkDone.lastRun = new Date().getTime();
linkDone.items = newArrayLink;
PropertiesService.getScriptProperties().setProperty('LINKS',JSON.stringify(linkDone))
console.log(linkDone)
}
function publishNews(title,link,auth){
let details = getPostDetails(link);
let description = details.description ? details.description : title;
title = decodeSpecialChars(title);
description = decodeSpecialChars(description)
let message = { "collection": "app.bsky.feed.post", "repo": auth.did, "record":
{ "text":description, "createdAt": new Date().toISOString(), "$type": "app.bsky.feed.post",
"embed": {
"$type": "app.bsky.embed.external",
"external": {
"uri": link,
"title":title,
"description": description
}
}
}
}
if(details.img){
let blob = UrlFetchApp.fetch(details.img).getBlob()
let blobOpt = {
'method' : 'POST',
'headers' : {"Authorization": "Bearer " + auth.token},
'contentType': blob.getContentType(),
'muteHttpExceptions': true,
'payload' : blob.getBytes()
};
let res = UrlFetchApp.fetch(UPLOAD_IMG_URL,blobOpt)
if(res.getResponseCode() == 200){
let pic = JSON.parse(res.getContentText());
message.record.embed.external.thumb = pic.blob
}
}
let postOpt = {
'method' : 'POST',
'headers' : {"Authorization": "Bearer " + auth.token},
'contentType': 'application/json',
'muteHttpExceptions': true,
'payload' : JSON.stringify(message)
};
const postRep = UrlFetchApp.fetch(POST_FEED_URL, postOpt);
console.log(postRep.getContentText())
}
function BlueskyAuth(){
// 1. we resolve handle
let handleOpt = {
'method' : 'GET',
};
let handleUrl = encodeURI(DID_URL+"?handle="+HANDLE)
const handleRep = UrlFetchApp.fetch(handleUrl, handleOpt);
const DID = JSON.parse(handleRep.getContentText()).did
console.log(DID)
// 2. We get Token
let tokenOpt = {
'method' : 'POST',
'contentType': 'application/json',
'payload' : JSON.stringify({"identifier":DID,"password":APP_PASSWORD})
};
const tokenRep = UrlFetchApp.fetch(API_KEY_URL, tokenOpt);
// console.log(tokenRep.getContentText())
const TOKEN = JSON.parse(tokenRep.getContentText()).accessJwt
console.log(TOKEN)
return {"did":DID,"token":TOKEN};
}
function getPostDetails(url){
let details = {}
let rep = UrlFetchApp.fetch(url,{ muteHttpExceptions: true})
let html= rep.getContentText();
if(html.indexOf('property="og:image"') < 0){
details.img = false;
}else{
let start = html.indexOf('content="',html.indexOf('property="og:image"')) + 'content="'.length ;
let end = html.indexOf('"',start)
details.img = html.substring(start,end)
}
let start = html.indexOf('content="',html.indexOf('meta name="description"')) + 'content="'.length ;
if(start >0){
let end = html.indexOf('"',start)
details.description = decodeHTML(html.substring(start,end))
}else{
details.description = false
}
console.log(details)
return details;
}
function decodeHTML(txt) {
// From answer : https://stackoverflow.com/a/4339083/3556215
var map = {"gt":">" /* , … */};
return txt.replace(/&(#(?:x[0-9a-f]+|\d+)|[a-z]+);?/gi, function($0, $1) {
if ($1[0] === "#") {
return String.fromCharCode($1[1].toLowerCase() === "x" ? parseInt($1.substr(2), 16) : parseInt($1.substr(1), 10));
} else {
return map.hasOwnProperty($1) ? map[$1] : $0;
}
});
}
function decodeSpecialChars(text) {
return text
.replace(/"/g, '"')
.replace(/&/g, '&')
.replace(/ /g, ' ')
.replace(/'/g, "'");
}
💡 Consejo: Sustituye
"YOUR APP PASSWORD"
por la contraseña de aplicación que generes en https://bsky.app/settings/app-passwords,"YOUR HANDLE"
con el valor de la cuenta desde la que publiques y"YOUR FEED URL"
No todos los Feed RSS son iguales, es lógico que tengáis que toquetear un poco según su estructura, pero si realmente has llegado hasta aquí no creo que tengas problemas.
Pasos finales
Vale, si ya conseguimos publicar los posts de forma manual (ejecutando el script desde nuestro portátil), lo único que nos falta es automatizar ese lanzamiento. En mi caso, he optado por usar el servicio App Script que ofrece Google. Este servicio nos permite, de manera gratuita, lanzar nuestros scripts n veces al día.
Despedida
Espero que este post te haya resultado útil y te anime a experimentar con la API de Bluesky y la automatización de publicaciones. Si tienes dudas, sugerencias o quieres compartir tus propios bots, ¡déjame un comentario o contáctame por Bluesky!
¡Gracias por leer y mucha suerte con tus proyectos!