St3ph-fr / apps-script-for-bluesky-social

Some Apps Script code to publish message and connect to Bluesky Social (Bsky.app)
4 stars 2 forks source link

I have no idea how github works, so here's some code with bug fixes and additional features #2

Open 408wij opened 5 days ago

408wij commented 5 days ago

// run and debug settings const DBUG = false; const LIVE = true; const DATERESTRICTED = true; const RESTART = false; const DISABLE_LOG = false;

// Bluesky constants const HANDLE='handle.bsky.social'; const APP_PASSWORD = "0123-abcd-4567-efgh"; const DID_URL="https://bsky.social/xrpc/com.atproto.identity.resolveHandle"; 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";

// RSS feed info and defaults const FEEDS = [ { url: 'https://feed1.url/feed/', post_txt: 'blah blah blah', default_img: 'https://image1.url/image.png', default_txt: 'consectetuer adipiscing elit.' }, { url: 'https://feed2.url/rss', post_txt: 'blah2 blah2 blah2', default_img: 'https://image1.url/image.png', default_txt: 'lorem ipsum dolor sit amet.' } ]

function main() { const auth = LIVE ? BlueskyAuth() : "foo"; for (var i = 0; i < FEEDS.length; i++) { publishFromRSS(auth, FEEDS[i]); } }

function publishFromRSS(auth, feed) { let rep = UrlFetchApp.fetch(feed.url,{ muteHttpExceptions: true}); if(rep.getResponseCode() != 200){ console.log('Error : '+ rep.getContentText()); return ; } if (RESTART) {PropertiesService.getScriptProperties().deleteProperty('LINKS') ;} let linkDone = JSON.parse(PropertiesService.getScriptProperties().getProperty('LINKS')) || {"items":[],"lastRun": new Date().getTime(),"init":true}

const xml = XmlService.parse(rep.getContentText()); const root = xml.getRootElement(); const channel = root.getChildren('channel'); const site = channel[0].getChild("link").getText(); 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") ? entry.getChild("link").getValue() : site; let guid = entry.getChild("guid") ? entry.getChild("guid").getValue() : link; let title = entry.getChild("title").getValue(); let desc = entry.getChild("description").getValue(); let pubdate = entry.getChild("pubDate") ? entry.getChild("pubDate").getValue() : new Date(); let msec = Date.parse(pubdate); let pubdatetime = new Date(msec).toISOString(); if (DBUG) { console.log("guid: " + guid); console.log("link: " + link); console.log("title: " + title); }

// set up date nonsense
pubdate = pubdatetime.substring(0,10); let today = new Date(); let yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); today = today.toISOString().substring(0,10); yesterday = yesterday.toISOString().substring(0,10);

if((DISABLE_LOG) || (linkDone.items.indexOf(guid)<0 )){
   if(!DATERESTRICTED || (DATERESTRICTED && ((pubdate === today) || (pubdate === yesterday)))) {
     //publishNews(title,link,auth,desc,txt,pubdatetime,defimg, deftxt);
     publishNews(title,link,auth,desc,pubdatetime,feed);
     if (DBUG) { console.log("publishNews called"); }
   }
   newArrayLink.push(guid); // if guid not in linkedDone then add it
}

}

// log this run if (!DISABLE_LOG) { if(linkDone.init){ linkDone.init = false ;} linkDone.lastRun = new Date().getTime(); linkDone.items.push(...newArrayLink); // old code was erasing the linkDone list every time the code ran PropertiesService.getScriptProperties().setProperty('LINKS',JSON.stringify(linkDone)); } if (DBUG) { console.log("linkDone: " + linkDone); } }

//function publishNews(title,link,auth,desc,txt,createdAt,defimg, deftxt){ function publishNews(title,link,auth,desc,createdAt,feed){ let details = getPostDetails(link);

// get image let regex = /srcset="([^"]+)"/; let match = desc.match(regex); const urlsArray = match ? match[1].split(',').map(url => url.trim().split(' ')[0]) : []; let img =urlsArray[urlsArray.length - 1]; if ((img == null) || (img === "") || (img.slice(-4) == "avif")) { // assumes javascript bails after the first comparison; i.e., null string doesn't generate an error; also--avif test is a dirty hack that should be replaced img = feed.default_img; }

// get description regex = /<\/a>\s([^<]+)|<\/div>\s([^<]+)|\s([^<]+)\s(?=]]>|\s*$)/; match = desc.match(regex); let description = match ? (match[1] || match[2] || match[3]).trim() : feed.default_txt; if (DBUG) { console.log("desc: " + description); }

// title = decodeSpecialChars(title); // why? // description = decodeSpecialChars(description); // why? let message = { "collection": "app.bsky.feed.post", "repo": auth.did, "record": { "text":feed.post_txt, "createdAt": createdAt, "$type": "app.bsky.feed.post", "embed": { "$type": "app.bsky.embed.external", "external": { "uri": link, "title":title, "description": description

  }
 } 
}

} if (!((img === "") || (img == null))) { let blob = UrlFetchApp.fetch(img).getBlob() let blobOpt = { 'method' : 'POST', 'headers' : {"Authorization": "Bearer " + auth.token}, 'contentType': blob.getContentType(), 'muteHttpExceptions': true, 'payload' : blob.getBytes() }; if (LIVE) { 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 } } if (DBUG) { console.log("img: " + img); } } let postOpt = { 'method' : 'POST', 'headers' : {"Authorization": "Bearer " + auth.token}, 'contentType': 'application/json', 'muteHttpExceptions': true, 'payload' : JSON.stringify(message) }; if (LIVE) { const postRep = UrlFetchApp.fetch(POST_FEED_URL, postOpt); if (DBUG) { console.log("postRep: " + 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 if (DBUG) { console.log("auth DID: " + 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); const TOKEN = JSON.parse(tokenRep.getContentText()).accessJwt if (DBUG) { console.log("auth token: " + 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 }

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, "'"); }

408wij commented 3 days ago

The above chokes on https://www.reddit.com/.rss, which differs a lot from other rss formats. It doesn't die on meetup.com (e.g., https://www.meetup.com//events/rss/ but doesn't handle it right, either. It does OK on a lot of other sites I tested, though.