0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GASでBluesky APIと通信し,SNS投稿の本文やいいね数など取得するサンプル (ぴったり200行のコードで,ブルースカイ上メッセージのAT uri,cidほか詳細情報のJSONをGoogleスプレッドシートから簡単に得られる!)

0
Last updated at Posted at 2026-09-05

「読み・書き」の「読み」のほうをやってみよう!

この記事は,Bluesky上の特定の投稿メッセージのURLを指定すると,その投稿の詳細情報を得られる…というサンプルコードです。

何事も,データの取り扱いには 「読み・書き」の両方 があります。
今回は「読み・書き」のうち 「読み」(参照系) のほうをやるわけですね。

※SNS上での「読み・書き」とは:

今回は「読み・書き」のうち「読み」のほうですが,
これには使いどころが多いと思います。

たとえば,SNS上での投稿本文を読み取るために
正式な手段(公式API)を使えるようになれば,スクレイピングしなくて済みます。

また,その投稿のいいね数・リポスト数(RT数)なども読み取れるので,
SNS上での 人気の投稿を見分ける ことができ,ホットなトピックを識別できるわけです。

「便利そうだな…!」と感じたら,ぜひ下記のサンプルを動かしてみてくださいね。

たった200行のサンプルコードで,投稿を分析してみよう

下記の hoge() という関数を実行するだけです。

※コードの冒頭にある,Blueskyアカウントの認証情報はご自分のアカウントの情報に書き換えてください。

// 「Bluesky上の特定の投稿のURLを指定して,
//   その投稿の詳細情報を取得するサンプルコード」
// 
// ・Googleスプレッドシート(GAS)から,Bluesky上の1メッセージを分析します。
//   必要となるコードの基本的な動作原理をわずか200行で理解する事が目的です。
// ・とくに,特定の投稿の「ATプロトコル上でのURI」やcidといった情報を取得することで
//   その投稿を引用したり,リプライを付けたりすることがのちのち可能になります。
// 2026.9.5. @rwanda_go_tan


// ここに自分のBlueskyアカウント名とアプリパスワードを記述する。
const MY_BLUESKY_USERNAME = "~~~.bsky.social"; // 例: hoge.bsky.social
const MY_BLUESKY_PASSWORD = "~~~"; // Blueskyで「設定」→「プライバシーとセキュリティ」より


// メイン処理
function hoge(){

  console.log("Bluesky上のメッセージを調べます。");
  
  // まずセッションを作成し,認証トークンを取得
  const access_jwt = bluesky_createSession();


  // 調べたい投稿のURL (ブラウザ上で目視で確認できる形式のURL)
  const post_url = "https://bsky.app/profile/jp.bsky.app/post/3mikfzwbw2k2j"
    // ※サンプル投稿として,Bluesky日本語の公式アカウントの固定メッセージのURLを題材としている。

  // 投稿の詳細情報を調べる
  const post_detail = bluesky_getPostDetailInfoByUrl( post_url, access_jwt );


  // 結果をコンソールに表示
  console.log( 
    "結果表示します。\n"
      + "uri = "               + post_detail.uri + "\n" // ATプロトコル上での投稿のURL
      + "cid = "               + post_detail.cid + "\n" // 投稿のハッシュ値
      + "author_name = "       + post_detail.author_name + "\n"  // アカウントの(日本語などで書かれた)表示設定名
      + "record_created_at = " + post_detail.record_created_at + "\n" // 投稿日時
      + "record_text = "       + post_detail.record_text + "\n"  // 投稿本文
      + "like_count = "        + post_detail.like_count + "\n"   // いいね数
      + "repost_count = "      + post_detail.repost_count + "\n" // リポスト数
      + "quote_count = "       + post_detail.quote_count + "\n"  // 引用数
      + "reply_count = "       + post_detail.reply_count + "\n"  // リプライ数
      + "bookmark_count = "    + post_detail.bookmark_count      // ブックマーク数
  );

}


// Bluesky上に投稿された1件の投稿メッセージについて
// ブラウザ上で確認できるURLから,その投稿の詳細情報を取得する。
// 詳細情報の中には,at-uri,cid などが含まれる。
function bluesky_getPostDetailInfoByUrl( post_url, access_jwt ){

  // 投稿のURLから,Blueskyハンドル名と投稿の識別子を抽出
  const match_result = post_url.match( /profile\/([^\/]+)\/post\/([^\/]+)/ );
  if( ! match_result ){
    throw "無効なBluesky投稿URLです。";
  }
  const target_bluesky_username = match_result[1];
  const target_post_id          = match_result[2];
  

  // 暫定的な at-uri を組み立てる。
  // (この時点では,uri中でBlueskyユーザー名の部分がDID形式になっていない。
  //  APIから返却されるat-uriでは,Blueskyユーザー名の部分が did:plc:~ 形式で置き換えられる。)
  const temporary_at_uri = "at://"
    + target_bluesky_username
    + "/app.bsky.feed.post/"
    + target_post_id
  ;
  
  // Bluesky API上の getPostThread エンドポイントのURLを構築する。(GET形式)
  const http_params = {
    uri          : temporary_at_uri,
    depth        : 0,
    parentHeight : 0,
  };
  const all_query_strings = Object.keys( http_params )
    .map(function( param_key ){
      
      // key=value 形式にする
      const param_value = http_params[ param_key ];
      const one_query_string = encodeURIComponent( param_key )
        + "="
        + encodeURIComponent( param_value )
      ;

      return one_query_string;

    })
    .join("&")
  ;
  const api_url_with_get_params = `https://bsky.social/xrpc/app.bsky.feed.getPostThread`
    + "?"
    + all_query_strings // GETパラメータを付与
  ;
    // Bluesky HTTP API Reference — Bluesky App
    // /xrpc/app.bsky.feed.getPostThread
    // https://endpoints.bsky.app/#bluesky-app/tag/appbskyfeed/GET/xrpc/app.bsky.feed.getPostThread
    // このエンドポイントへのリクエストとレスポンスの仕様が記載されている。
  
 
  // メッセージ情報を取得するための Bluesky API にアクセス
  const url_fetch_options = {
    method             : "GET",
    headers            : {
      "Authorization" : "Bearer " + access_jwt
    },
    muteHttpExceptions : true
  };
  const http_response_obj = UrlFetchApp.fetch(
    api_url_with_get_params, 
    url_fetch_options
  );

  // 正常にアクセスできたか?  
  if( http_response_obj.getResponseCode() !== 200 ){
    throw "APIアクセスでエラーが発生しています。 " + http_response_obj.getContentText();
  }
  // 正常にアクセスできた場合,レスポンス本文を見やすくインデントして表示
  console.log( 
    "content_text : " 
      + JSON.stringify(
        JSON.parse( http_response_obj.getContentText() ), 
        null,
        2 // 半角スペース2つずつ使用してインデント
      )
  );


  // 受信データから必要な情報を(重要な物のみ)抽出
  const json_http_response_content = JSON.parse( http_response_obj.getContentText() );
  const target_post_data = json_http_response_content["thread"]["post"];

  // 呼び出し元が利用しやすいような形式で返す  
  return {
    uri : target_post_data["uri"],
    cid : target_post_data["cid"],
    author_name       : target_post_data["author"]["displayName"],
    record_created_at : target_post_data["record"]["createdAt"],
    record_text       : target_post_data["record"]["text"],
    like_count        : target_post_data["likeCount"],
    repost_count      : target_post_data["repostCount"], // リポスト数と引用数はブラウザ上では合算表示されている
    quote_count       : target_post_data["quoteCount"],
    reply_count       : target_post_data["replyCount"],
    bookmark_count    : target_post_data["bookmarkCount"]
  };

}


// Blueskyのセッションを作成する。
// 認証に使うアクセストークン文字列(accessJwt)を返す。
function bluesky_createSession() {

  // セッション作成用のAPIにアクセスする
  const api_response = UrlFetchApp.fetch(
    "https://bsky.social/xrpc/com.atproto.server.createSession", 
    {
      method  : "POST",
      headers : {
        "Content-Type" : "application/json"
      },
      payload : JSON.stringify({
        identifier : MY_BLUESKY_USERNAME,
        password   : MY_BLUESKY_PASSWORD
      })
    }
  );

  // APIからのレスポンスは正常か    
  if( api_response.getResponseCode() === 200 ){
    
    // レスポンスをJSONとしてパース
    const session_obj = JSON.parse( api_response.getContentText() );
    console.log("Blueskyセッション作成に成功。");

    // 認証に使うアクセストークン文字列を返す
    return session_obj.accessJwt;
      // accessJwtとは,以降のAPIリクエスト中に認証に使う「短寿命のアクセストークン」を指す。
      // 
      // ※参考文献 ATプロトコルでの認証方法について: HTTP API (XRPC)
      // https://atproto.com/ja/specs/xrpc
      // Authenticationの欄から引用:
      // "Most requests should be authenticated using an access JWT,
      //  but the validity lifetime for these tokens is short.
      //  Every couple minutes, a new access JWT can be requested"

  }
  else
  {

    // 例外を投げ処理中断
    throw "Blueskyセッション作成に失敗。" + api_response.getContentText();

  }

}

上記のコードの説明

まずメイン処理である hoge() を呼ぶと,
bluesky_createSession() という関数で Bluesky API と通信し
自分のBlueskyアカウントで API にログインして,セッションが作成されます。

セッションの実態は,accessJwtと呼ばれる認証トークン文字列です。
このトークン文字列は,発行されてから短時間の間,APIを使用する際の認証情報として使えます。
(ここまでは投稿系の処理と同じ流れです。)

次いで,Bluesky上で調べたい投稿メッセージのURLとして下記のものを指定しています。
これはあくまでも一例で,日本語圏の私たちにとってこのメッセージが
最も手軽にサンプルとして言及しやすそうだったので,今回はこれを使っています。

Bluesky日本語(公式)の投稿
https://bsky.app/profile/jp.bsky.app/post/3mikfzwbw2k2j
引用: 『日本のBlueskyユーザーの皆さん、こんにちは!👋
英語以外の言語として初の、日本語公式アカウントを開設しました。(以下略』

そして,その投稿のURLを
bluesky_getPostDetailInfoByUrl() という関数に渡しており,
この関数の中身が今回のキモです。

まず,ブラウザ上で見えている「 http:// ~ 」という投稿メッセージのURLを,
Blueskyが内部的に使用している「ATプロトコルのURL」(暫定的な値)に変換し
「at://ユーザー名/app.bsky.feed.post/投稿の識別子」
の形にしています。

次いで,その「ATプロトコルのURL」を
Blueskyの下記のURLにGETリクエストで投げます。

Bluesky上の特定の投稿をスレッド形式で解析してくれるAPI
https://bsky.social/xrpc/app.bsky.feed.getPostThread

このAPIの仕様については,下記にマニュアルがありますのでどうぞ。

Bluesky HTTP API Reference — Bluesky App
"/xrpc/app.bsky.feed.getPostThread"
https://endpoints.bsky.app/#bluesky-app/tag/appbskyfeed/GET/xrpc/app.bsky.feed.getPostThread

  • このエンドポイントへのリクエストとレスポンスの仕様が記載されている。
  • 利用時に認証が必要かどうかについて: 「Does not require auth, but additional metadata and filtering will be applied for authed requests.」つまり,認証無しでも最低限の利用はできる そうなんですが,パラメータを指定して絞り込みしたいような場合は認証が必要。だから,このAPIをフル機能で活用したい場合,Blueskyのセッションを前もって作っておき,headerにaccess_jwtを渡して 認証するのがよい ですね。

これでAPIへの通信が成功すると,まず
対象となるメッセージが「そもそも存在するかどうか」チェックされます。

URL文字列としてはBluesky上の正当な投稿URLに見えても,
その投稿は削除済みだったり,URLをtypoしてたりするかもしれませんよね。
そういう場合,コンソールログに下記のエラー情報が表示されます。

APIアクセスでエラーが発生しています。 {"error":"NotFound","message":"Post not found: at://~~"}


いっぽう,もし対象メッセージがBluesky上に存在すれば
調べたい投稿メッセージについて,
いろいろ詳しい情報がJSONでズラ~ッと返ってきます。

返却される情報の量があまりに多くなり過ぎないように,

  • 「スレッド内の 親側 のリプライは何階層までたどるか?」
  • 「スレッド内の 子側 のリプライは何階層までたどるか?」

といった探索範囲を絞れるようになっています。

(※この探索範囲の階層を変化させるには,
APIにGETリクエストを投げる際に
depth や parentHeight といったパラメータを調節しましょう。)

先に掲載したサンプルコードでは,受信したJSONを
全文まるごとコンソールログで読めるようになってますので,
興味があったらそのJSONの中をじっくり読んでみてくださいね。

実用上は,その受信したJSONの中で
おもだったよく使う情報だけを抽出して取得できれば良いので,
この関数の返り値も「よく使う値」を下記のようにチョイスして返しています。

  • その投稿メッセージの「ATプロトコル上での正式なURL」(JSON上では,単に uri というプロパティ名になっています。)
  • その投稿メッセージの「cid」(その投稿メッセージの内容が改ざんされていないことを証明するハッシュ値のようなもの。)
  • 投稿者のアカウント名
  • 投稿日時
  • 投稿本文
  • その投稿についた「いいね」の数
  • リポスト数
  • 引用数
  • リプライ数
  • ブックマーク数

これらの情報を取得した結果,最終的にコンソールログで下記のように出力されます。


結果表示します。
uri = at://did:plc:ry3hbexak5ytsum7aazhpkbv/app.bsky.feed.post/3mikfzwbw2k2j
cid = bafyreiaupiqpvecwabwcyuql3ixp5q5f5us4zy5fdjmjsb7p7w2zgekip4
author_name = Bluesky日本語(公式)
record_created_at = 2026-04-03T00:00:04.008Z
record_text = 日本のBlueskyユーザーの皆さん、こんにちは!👋
英語以外の言語として初の、日本語公式アカウントを開設しました。

今後、 @bsky.app などの投稿の翻訳、日本向けの告知などを行っていく予定です。

ぜひこの投稿をリポストして、多くの方にこのアカウントを知っていただけるようご協力ください。
like_count = 9180
repost_count = 8422
quote_count = 144
reply_count = 95
bookmark_count = 436

とくに,冒頭に「uri」「cid」という2つの項目が表示されていますね。
これらの情報は…

  • 「uri」としてここでAPIから返却される値は暫定的な値ではなく,正式な値 です。このuri の中に含まれる「投稿者のアカウント名」の部分は,「did:plc:~~」という内部的な識別子で表現されています。
  • cid は,なんかよくわからない英数字が並んでいます よね。どうしてこんな情報が必要なのか?と疑問に思うかもしれませんが・・・。bot処理する上では必要になってくる値です。

この上記の「uri と cid という2つの情報のセット」は,Bluesky上で1つの投稿メッセージのありかを特定するために必ず必要 になる情報なんです。

たとえば,その投稿をAPI経由で「リポストしたい,リプライしたい」,みたいな時にその2つの情報が必要になります。

ブラウザ上で見えている「https:// ~」という形式のURLではなく,
「Blueskyが内部的に使用している,ATプロトコルでの uri」
(および,そこに記載されている情報の真正性を証明する ための cid)
が必要になる。というのがポイントですね~。

ですから,今回のサンプルコードは「読み・書き」でいうと「読み」のほうなんですけど,
「書き」のためにも間接的に役立つ。という事になります。

今回のサンプルコードを使って url + cid を取得しない限り,
リポストやリプライといった更新系の処理ができないわけですから・・・。

その意味で,Bluesky APIを使う際に汎用的に必要になる処理だ,と言えますね。

むすびに

参照系と更新系,両方の操作を覚えていって
SNSのAPIを自在に操れるようになってまいりましょう。

ちなみに今回のサンプルコードの内容,一見すると短くて簡単そうに見えますが
作るのは結構大変でした。

どうして大変だったかというと,
一般的に出回っているサンプルコードでは今回の処理は
「たいていSDK内で隠ぺいされている」部分 に該当するため,
このAPI のURLでこういうJSONが返ってきて…という細かい情報が
おもてだって取り扱われている事例が少なく,情報が手に入りづらかったのです。

そのようにWeb上の情報が少ないと,この件は
AIに聞いてもぜんっぜん正しい答えが返ってきませんでした。
何度問いただしても,何度修正の命令をかけて繰り返しても,
AIくんは,この件で動作する正しいサンプルコードを生み出せなかったのです。
ネット上にその情報が無いためです。
(AIってそういう点はほんとに弱いですよね~。)

で,今回の私が書いたこの記事が,Web上に公開されることによって
AIたちも今後は正しい返事ができるようになってくれる事でしょう。
つまり この記事を書く時の私の気持ちは,「AIへの養分をみずから提供している」 わけです。それでいいんです。

それで,今回のこの記事をきっかけにBlueskyや,Bluesky上でのbot作成に関心を持ってくれる人が増えてくれれば幸いです。
・・・ってか,そういう事を人間たちがするために必要になるコードをAIたちが正しく出力・提供できるように,AIの皆さんが正しい技術情報や動作するサンプルコードをよく学習してくれれば幸いです。

・・・ん?私は一体,誰に向けて今この文章を書いているんだ? 人間の読者?それともAIのクローラー?
よくわからんな・・・。まあいいか・・・

※関連リンク

なぜBluesky上でのbot作成が必要になったか,という背景については下記をご覧ください。

『【作業ログ・パート4】Twitter(X)上で無料でbotが作れなくなったので,かわりにBlueskyでオープンソースのbotツール作りを進めている。現時点で,GASを使って無料で「今年の残り時間bot」や「ラジオ体操bot」などをBluesky上で動かし,自動投稿できている。なかなか大変だけど開発を継続中』
https://posfie.com/@ouen_suru_tan/p/74JZDyD


『(月ごとの進ちょくまとめ)Twitter(X)からBlueskyへbotを移行する作業の履歴・おもな出来事とリンク集』
https://posfie.com/@ouen_suru_tan/p/UjEZA7X


『GASのトリガー残り時間を制御・配分・管理する汎用フレームワーク「GTRM (=GAS トリガー・リソース・マネージャー)」。わずか2ステップで利用開始する方法』
https://qiita.com/rwanda_go_tan/items/26071fdcbe7f7a915765

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?