# Introduction
I used to use Twitter as a news reader. It offered a great user experience as it made it easy to discuss news with friends. Now, I want to introduce this user experience in Bluesky.
This note outlines the implementation of Bluesky bots (@bbcnews-uk-rss.bsky.social, @bbcnews-world-rss.bsky.social) designed to repost RSS feeds of BBC News. I wish you readers implement bots with your favourite news sources.
# Implementation
The following function is implemented and deployed using Google Cloud Functions:
1. Fetch the latest RSS feeds from BBC RSS using the `rss-parser` package.
2. Fetch the latest posts from the bot account using the `@atproto/api` package.
3. For each RSS feed (fetched in Step 1) that was not posted to bluesky (fetched in Step 2),
1. Fetch the HTML of the news.
2. Extract description and image using the `cheerio` package.
3. Reduce the size of image using the `sharp` package.
4. Upload the image as a blob, and create a post with the image and link by `@atproto/api` package.
Then, the function is periodically triggered using Google Cloud Scheduler.
## Cloud Function
Create a Cloud Function with the following codes. Set a Pub/Sub trigger.
```javascript=
const functions = require("@google-cloud/functions-framework");
const { BskyAgent, AppBskyFeedPost } = require("@atproto/api");
const cheerio = require("cheerio");
const sharp = require("sharp");
const Parser = require("rss-parser");
const parser = new Parser();
const settings = [
{
account: "bbcnews-uk-rss.bsky.social",
password: "xxxx-xxxx-xxxx-xxxx",
url: "https://feeds.bbci.co.uk/news/uk/rss.xml#",
},
{
account: "bbcnews-world-rss.bsky.social",
password: "xxxx-xxxx-xxxx-xxxx",
url: "https://feeds.bbci.co.uk/news/world/rss.xml#",
},
];
async function get_feeds(url) {
const feed = await parser.parseURL(url);
let output = [];
for (const item of feed.items) {
output.push({
title: item.title,
link: item.link,
});
}
return output;
}
async function post(agent, item) {
let post = {
$type: "app.bsky.feed.post",
text: item.title,
createdAt: new Date().toISOString(),
};
const dom = await fetch(item.link)
.then((response) => response.text())
.then((html) => cheerio.load(html));
let description = null;
const description_ = dom('head > meta[property="og:description"]');
if (description_) {
description = description_.attr("content");
}
let image_url = null;
const image_url_ = dom('head > meta[property="og:image"]');
if (image_url_) {
image_url = image_url_.attr("content");
}
const buffer = await fetch(image_url)
.then((response) => response.arrayBuffer())
.then((buffer) => sharp(buffer))
.then((s) =>
s.resize(
s
.resize(800, null, {
fit: "inside",
withoutEnlargement: true,
})
.jpeg({
quality: 80,
progressive: true,
})
.toBuffer()
)
);
const image = await agent.uploadBlob(buffer, { encoding: "image/jpeg" });
post["embed"] = {
external: {
uri: item.link,
title: item.title,
description: description,
thumb: image.data.blob,
},
$type: "app.bsky.embed.external",
};
const res = AppBskyFeedPost.validateRecord(post);
if (res.success) {
console.log(post);
// await agent.post(post);
} else {
console.log(res.error);
}
}
async function main(setting) {
const agent = new BskyAgent({ service: "https://bsky.social" });
await agent.login({
identifier: setting.account,
password: setting.password,
});
let processed = new Set();
let cursor = "";
for (let i = 0; i < 3; ++i) {
const response = await agent.getAuthorFeed({
actor: setting.account,
limit: 100,
cursor: cursor,
});
cursor = response.cursor;
for (const feed of response.data.feed) {
processed.add(feed.post.record.text);
}
}
for (const feed of await get_feeds(setting.url)) {
if (!processed.has(feed.title)) {
await post(agent, feed);
} else {
console.log("skipped " + feed.title);
}
}
}
functions.cloudEvent("entrypoint", async (_) => {
for (const setting of settings) {
console.log("process " + setting.url);
await main(setting);
}
console.log("--- finish ---");
});
```
```json=
{
"name": "bbc-to-bluesky",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@atproto/api": "^0.4.3",
"cheerio": "^1.0.0-rc.12",
"rss-parser": "^3.13.0",
"sharp": "^0.32.4"
}
}
```
## Cloud Scheduler
Trigger the Pub/Sub signal every 15 minutes, i.e., the cron setting is the following.
```
*/15 * * * * *
```
# References
- https://zenn.dev/ryo_kawamata/articles/8d1966f6bb0a82 for reducing image sizes.