Blueskyの自分の投稿をRSSに変換する(GAS使用)

2024/2現在、「ユーザーページのURL/rss」でその人の投稿をRSS形式で取得できるようになっています。
(例) https://bsky.app/profile/nigauri.me/rss

Bluesky RSS化

Blueskyの自分の投稿をRSSで取得したい!それでIFTTTで自動連携したい!という人向け記事。GAS(Google Apps Script)を使います。

ちなみにユーザ名を入れるだけでRSSを出力してくれるカワリミ人形さんBluestream(Bluesky→RSS Feed Generator)というのが既にあるのでそれを使うのが一番簡単だけど、自分の好きなようにカスタマイズしたりAtomにしたりJSONをそのまま返したりブログパーツを作ったりと自由度が高い&なるべく第三者のサービスを噛ませたくないみたいな人はこちらをどうぞ。

以前Mastodonで似たような記事を書いたんだけど、あれは元々Atomで配信されてたフィードの改良みたいな感じだった。

Mastodon → Twitter の連携(IFTTT + GAS)
🕒️2019/05/03
Mastodon → (文字列編集) → Twitter と連携する方法のメモ書き。

今回はRSS/Atomの配信機能自体がない、かつログインしていないとフィードが見られないので、真っ当にログインしてから対象の機能にアクセスして情報を得てそれをRSSに変換する必要がある。

極端な話AT ProtocolやBluesky関係のライブラリがなくてもしかるべきURLに正しいパラメータつけてPOSTとかGETすればほぼ全機能にアクセスできるのでそれさえわかればスクラッチでもなんとかなる。新しくできた技術だからってやさしい作りになっているのでなんもむずかしくない。

注意: 開発環境として VSCode と clasp を使って TypeScript で書いているため、コードをまるまる参考にする場合はこれらおよび Node.js のインストールが必要なので各自やってください。参考までに俺はWindowsでやっております。

おまけ: GAS用の開発環境構築メモ
🕒️2023/04/21
Google Apps Script(GAS)を使用してBlueskyの自分の投稿をRSSに変換する記事でちょっと書いた、VSCode と clasp を使って TypeScriptでGASのコードを書くための開発環境の構築メモ。

ログイン

未ログインで見られるものは限られているのでまずはログイン処理を行う。

現時点ではOAuthとかそういった機能がなくてユーザ名とパスワードを用いた昔ながらのログインしかできない。セキュリティ的にはアレだけど技術的にはシンプルで簡単なので練習としては助かるね。パスワードはアプリパスワードの使用を推奨。

https://bsky.social/xrpc/com.atproto.server.createSessionidentifier (ハンドル名)と password のパラメータをつけてPOSTするとJSONが帰ってくる。その中の accessJwt が認証情報として必要なので保存しておく。

identifier には 「xxxx.bsky.social」または独自ドメインを設定する。以下のサンプルでは俺のハンドル名nigauri.meを設定してる。

let url = "https://bsky.social/xrpc/com.atproto.server.createSession";

let data = {
  "identifier": "nigauri.me",
  "password": "パスワード(アプリパスワード可)"
};

const options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions & { method: GoogleAppsScript.URL_Fetch.HttpMethod } = {
  method: "post",
  headers: {
    "Content-Type": "application/json; charset=UTF-8",
  },
  payload: JSON.stringify(data),
};

let response = UrlFetchApp.fetch(url, options);
let accessJwt = JSON.parse(response.getContentText()).accessJwt;

自分のPost一覧を取得する

https://bsky.social/xrpc/app.bsky.feed.getAuthorFeedactor (ハンドル名)、 limit (取得件数。省略可) をつけてGETするとJSONが帰ってくる。アクセスの際ヘッダーに認証情報をいれとくこと。

let url = "https://bsky.social/xrpc/app.bsky.feed.getAuthorFeed?actor=nigauri.me&limit=10";

const options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions & { method: GoogleAppsScript.URL_Fetch.HttpMethod } = {
  "method": "get",
  "contentType": "application/json",
  "headers": {
    "Authorization": Bearer ${accessJwt}
  }
};

let response = UrlFetchApp.fetch(url, options);
let responseJSON = JSON.parse(response.getContentText());

JSONの内容をRSSに変換する

あとは取得した内容を見てRSSに変えて出すだけ。

投稿した画像は response.feed.post.embedの中に入っているのでそこから取得してdescriptionの末尾に足すなどする。

画像なし、引用なしのPostはresponse.feed.post.embedが無い。またQuote Post(引用RT)の場合の引用元がembedに入ってくるのでembedはあるけどembed.imagesがない場合もある。

let channel = XmlService.createElement('channel');
let root = XmlService.createElement('rss').setAttribute('version', "2.0").addContent(channel);
let document = XmlService.createDocument(root);

function createElement(element, text) {
  return XmlService.createElement(element).setText(text);
};

channel.addContent(createElement("title", "Bluesky Users Feed (nigauri.me)"));
channel.addContent(createElement("link", "https://staging.bsky.app/profile/nigauri.me"));
channel.addContent(createElement("description", "nigauri.me's Bluesky posts"));
channel.addContent(createElement("language", "ja"));

for (let feed of response.feed) {
  let post = feed.post;
  let uri = post.uri;
  let embed = post.embed;
  let linkUrl = uri.split("app.bsky.feed.post/")[1];

  let description = post.record.text;
  if (embed != null && embed.images != null && 0 < embed.images.length) {
    for (let image of embed.images) {
      description += <img src="${image.fullsize}" />;
    }
  }

  let item = XmlService.createElement('item');
  item.addContent(createElement("title", post.record.text));
  item.addContent(createElement("description", description));
  item.addContent(createElement("link", https://staging.bsky.app/profile/nigauri.me/post/${linkUrl}));
  item.addContent(createElement("pubDate", post.record.createdAt));
  channel.addContent(item);
}

let xml = XmlService.getPrettyFormat().format(document);

Webサービス化する

doGet() を作ってさっきのXMLをレスポンスとして返す。

function doGet() {
  let accessJwt = accessJwt取得処理();
  let response = AuthorFeed取得処理(accessJwt);
  let xml = RSS変換処理(response);

  let out = ContentService.createTextOutput();
  out.setMimeType(ContentService.MimeType.RSS);
  out.setContent(xml);

  return out;
}

できたらclasp pushでGASにアップロードし、clasp openで編集画面を開いたらテストする。

問題なさそうなら「デプロイ」→「新しいデプロイ」→「種類の選択」でウェブアプリを選択→「デプロイ」で公開する。

その際に出てくる「https://script.google.com/macros/s/xxx/exec」みたいなURLをブラウザで開いたりRSSリーダーに読み込ませたりしてちゃんと動いてれば成功。

おつかれさまでした。