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?

Blueskyで①投稿成功したメッセージのURLを取得したり ②引用投稿(引用RT)したりするGASサンプル (引用したいメッセージのURLを指定し,bot投稿内に埋め込むためのAPI使用法。ちょうど400行でGoogleスプレッドシートから動かせるコード)

0
Last updated at Posted at 2026-09-07

①投稿成功したURLを取得し,②引用投稿してみよう!

GAS からBluesky APIを使ってbot投稿する方法を,より詳しく知っていきましょう!

単純に「メッセージ本文をbot投稿するだけで終わり」なら,こちらのたった100行の初歩的なサンプルで済みます。

でも,もう少し複雑な処理をしたい場合もありますよね。

  • ① bot投稿が成功したら,その投稿結果をブラウザ上で見れる URLを知りたい 。(bot投稿が成功したという証拠を得たい)

  • ② bot投稿する際に,メッセージ内に別の投稿を引用したい。(Twitter・Xでいう所の「引用リツイート」をしたい。)

上記の2つの方法を覚えるために,今回もサンプルコードをご用意しました。
たった400行です!
ぜひ,実際に動かしてみてくださいね。

わずか400行のサンプルコード

コード冒頭のBlueskyユーザ名とパスワードは,ご自分のアカウントの情報に書き換えておいてください。

// 「Bluesky上で自分が投稿したメッセージのURLを取得し,
//   その投稿を引用投稿するサンプルコード」
// 
// ・Googleスプレッドシート(GAS)から,Bluesky上で下記の流れを実行します。
//   (1) まず,1つメッセージを投稿。
//   (2) (1)で投稿できたメッセージのURLを取得。
//   (3) (2)を引用して,別のメッセージを投稿。
// ・引用投稿のために必要となるコードの基本的な動作原理を理解する事が目的です。
// ・サンプルコード全体でぴったり(無理やり)400行となっています。
//
// 2026.9.7. @rwanda_go_tan


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



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

  console.log(
    "Bluesky上で,"
      + "(1) 普通の投稿 → "
      + "(2) 1の投稿結果URL取得 → "
      + "(3) 引用投稿 の流れを実行します。"
  );


  // まずセッションを作成し,認証トークンを取得
  const access_jwt = bluesky_createSession();

  // 普通の投稿を実行
  const http_response_json = bluesky_postOneMessage({ 
    bs_post_message : "(1) 普通の投稿のテストです。" + ( new Date() ).toISOString(), 
    access_jwt      : access_jwt 
  });


  // 投稿成功時のHTTPレスポンスを使い,投稿結果URLを取得する。
  const my_posted_message_url = bluesky_getMessageUrlFromHttpResponseOnPostSuccess({
    my_bluesky_username : MY_BLUESKY_USERNAME,
    http_response_json  : http_response_json
  });
  console.log(
    "(2) 投稿結果の(ブラウザ上でアクセス可能な)URLを取得するテストです。\n"
      + "自分が投稿したメッセージのURLは " + my_posted_message_url
  );


  // 引用したい投稿の詳細情報を調べる (AT-uri, cid など)
  const quote_target_post_detail = bluesky_getPostDetailInfoByUrl( 
    my_posted_message_url, 
    access_jwt 
  );
  console.log(
    "引用したい投稿の内部的な詳細情報 :\n"
      + "・uri = " + quote_target_post_detail["uri"] + "\n"
      + "・cid = " + quote_target_post_detail["cid"] + "\n"
  );


  // 上記を引用して,もう1つのメッセージを投稿する
  bluesky_postOneMessage({ 
    bs_post_message : "(3) 引用投稿のテストです。" + ( new Date() ).toISOString(), 
    access_jwt      : access_jwt,

    // 引用したい投稿の情報をオプションとして指定できる
    qupte_target_post_info : {
      quote_uri : quote_target_post_detail["uri"],
      quote_cid : quote_target_post_detail["cid"]
    }  
  });


  return;

}


// 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();

  }

}


// 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にbot API経由でメッセージを1つ投稿する。
function bluesky_postOneMessage( argument_obj ){

  // この関数の引数を解釈
  const bs_post_message        = argument_obj.bs_post_message;
  const access_jwt             = argument_obj.access_jwt;
  const qupte_target_post_info = argument_obj.qupte_target_post_info;

  console.log( "Blueskyにメッセージを投稿します。投稿内容: " + bs_post_message );


  // UrlFetch用のパラメータを構築する
  let url_fetch_params = {

    method  : "POST",

    headers : {
      "Authorization" : "Bearer " + access_jwt, // 認証トークンを渡す
      "Content-Type"  : "application/json"
    },

    // APIに送る各種パラメータ類はこの中に記載する (JSONではなく文字列形式)
    payload : null // とりあえずnullにしておいて,以降で値を組み立ててから代入する

  };
  // payloadを組み立てる
  let payload_obj = {

    // 自分のBlueskyアカウント名を指定する。
    repo       : MY_BLUESKY_USERNAME,

    // フィードへの投稿である旨を明示する。
    collection : "app.bsky.feed.post",

    // 具体的な投稿の内容を記載する。
    record     : {

      // 投稿本文
      text      : bs_post_message,

      // 作成日時
      createdAt : ( new Date() ).toISOString()

    }

  };
  // 引用したい投稿が指定されている場合
  if( qupte_target_post_info ){

    // fetch用のパラメータに引用関連の情報を指定する
    payload_obj["record"]["embed"] = {

      // 投稿内に別の投稿(record)を埋め込みたい(embed),という旨を指示する
      "$type": "app.bsky.embed.record",

      // 引用したい投稿
      record: {
        uri : qupte_target_post_info.quote_uri,
        cid : qupte_target_post_info.quote_cid
      }

    };
    console.log("投稿内に引用したい投稿のパラメータをセットしました。");

  }
  // payloadを組み立て終わったら文字列として保管する
  url_fetch_params["payload"] = JSON.stringify( payload_obj );


  // メッセージ投稿用のAPIにアクセスし,HTTPレスポンスを受信する
  const http_response_obj = UrlFetchApp.fetch(
    "https://bsky.social/xrpc/com.atproto.repo.createRecord", 
    url_fetch_params
  );

  // 受信したHTTP本文
  const http_response_texts = http_response_obj.getContentText();
    // NOTE:
    // 通信エラーが発生している場合は,この時点ではcontentはJSONではなくHTMLであったりする。
    // そのため,このタイミングでJSON.parseしようとするとHTMLをJSONとして解釈できないエラーになり得る。
    // JSON.parseするよりも前に,レスポンスが正常かどうかのチェックが要る。


  // APIからのレスポンスは正常か    
  if( http_response_obj.getResponseCode() === 200 ){

    console.log("Blueskyメッセージ投稿に成功。");

    // 通信に成功していたら,HTTPレスポンスをJSONとしてパース可能だとわかる
    const http_response_json = JSON.parse( http_response_texts );


/*
    // 受信したJSONを整形してデバッグ出力
    const response_json_as_indented_text = JSON.stringify(
      http_response_json,
      null,
      2 // 半角スペース2個でインデント整形して見やすくする
    );
    console.log( "HTTPレスポンスをデバッグ出力します。\n" + response_json_as_indented_text );
      // ※出力内容はそれなりに長いので注意
*/


    // 連続して通信するとエラーになりやすいので,少し待機
    Utilities.sleep(3000);

    // JSONを返す
    return http_response_json;

  }
  else
  {
 
    // 例外を投げ処理中断
    throw "Blueskyメッセージ投稿に失敗。" + http_response_texts;
 
  }

}


// メッセージの投稿成功時のHTTPレスポンスを使い,投稿結果のURLを取得する。
function bluesky_getMessageUrlFromHttpResponseOnPostSuccess( argument_obj ){

  // 引数を解釈
  const my_bluesky_username = argument_obj.my_bluesky_username;
  const http_response_json  = argument_obj.http_response_json


  // APIから返ってきたレスポンスを見て,投稿の識別子を取得する
  const posted_message_id = http_response_json["uri"].split("/")[4];
    // NOTE:
    // 下記の末尾部分を取得している。
    // "uri": "at://did:plc:~(Blueskyのアカウント名を表す文字列)/app.bsky.feed.post/~(1つの投稿を指す文字列)",

  // ブラウザでアクセス可能なURLを構築
  const posted_message_url = "https://bsky.app/profile/"
    + my_bluesky_username // アカウント名
    + "/post/"
    + posted_message_id // 投稿の識別子
  ;
    // NOTE:
    // ここでは,投稿者のBlueskyアカウント名として
    // とりあえず自分自身のアカウント名だけを指定できるようにしてある。
    // どうしてそうしてあるかというと,
    // 引用やリプライの動作テストをする際には
    // まず自分自身のアカウント内だけで動作テストしてほしいから。
    // そうすれば,他人の投稿に対してテスト目的で引用やリプライが付いてしまって
    // まるで動作テストがスパム投稿のようになってしまう…という事態を回避しやすくなるだろう。
    // サンプルコードを公開する際には,そういう配慮も必要だと思う。


  return posted_message_url;

}

hoge() というメイン関数を実行すると,

  • (1) ご自分のアカウント内でテスト投稿が実行され,

  • (2) その投稿のURL (https:// ~~)がコンソールログに表示され,

  • (3) 次いで,いま投稿したばかりのそのテスト投稿を引用して,新たに別の投稿をします。

これで,引用RTの形式でbotつぶやき できるようになりますね!

ちなみにコード中の冒頭コメントに「全体でぴったり(無理やり)400行となっています。」との記載がありますが,あまり気にしないでください。キリのいい行数にそろえるために必要だったんで…。

コードの説明

hoge() 内では,まず bluesky_createSession() でAPIにログインしています。

ログインによって得た認証トークンを使い,
bluesky_postOneMessage() でテストメッセージを投稿します。

その際,「投稿したらしっぱなし」にはせず
投稿の結果として Bluesky APIからのHTTPレスポンスを取得し,
「投稿が成功した」という詳細情報を受け取って中身を調べます。
これが http_response_json というJSONデータです。

その受信したJSONを使って,
bluesky_getMessageUrlFromHttpResponseOnPostSuccess()
という関数で,投稿成功したメッセージのURLを組み立てています。

これが my_posted_message_url という変数で
コンソールログに出力されています。

なお,ここでいうURLというのは
ブラウザ上で目視で確認できるようなURLです。
(AT-uriではありません。)

Blueskyのサーバ内では,1つのメッセージのありかを特定するために
内部的には ATプロトコルのURL (AT-uri) がしばしば使われます。

でも,AT-uriを知ったところで
それをChromeとかのブラウザに貼り付けても,投稿したメッセージは見れません。
Blueskyのサーバ内部でしか通用しないデータ形式なのです。
そのため,API利用者の立場としては,AT-uri をhttp上のURLに変換 しなければいけません。

(AT-uriについては,こちらの記事で解説してありますのでご覧ください。)

最後に,メイン関数 hoge() の中では
先ほど投稿成功したメッセージの情報を使って,引用投稿(引用RT)しています。

ここで引用投稿のために使っている関数は,
引用ではない 「普通の投稿」のために使ったのと同じ関数 です。

関数の引数に「引用したい対象となるメッセージのありか」をオプションとして指定すれば
引用投稿できるようにしておきました。

普通の投稿と引用投稿とでは
APIと通信する際の送信用のJSON内で,下記のような違いがあります。


// メッセージ投稿用のAPIにアクセス
UrlFetchApp.fetch(

  // 該当APIのURL
  "https://bsky.social/xrpc/com.atproto.repo.createRecord", 

  // UrlFetchのオプション
  {
  
    // 自分のBlueskyアカウント名を指定する。
    repo       : MY_BLUESKY_USERNAME,

    // フィードへの投稿である旨を明示する。
    collection : "app.bsky.feed.post",

    // 具体的な投稿の内容を記載する。
    record     : {

      // 投稿本文
      text      : bs_post_message,

      // 作成日時
      createdAt : ( new Date() ).toISOString(),

      // ★引用投稿の場合は,ここに引用したい投稿の情報が入る
      embed : ~~

    }
  }
);
  

普通の投稿の場合,embed属性は不要です。
引用投稿の場合,embed属性内に下記のような値をセットします。


embed : {

  // 投稿内に別の投稿(record)を埋め込みたい(embed),という旨を指示する
  "$type": "app.bsky.embed.record",

  // 引用したい投稿
  record: {
    uri : "引用したい投稿のAT-uriの文字列",
    cid : "引用したい投稿のcid文字列"
  }

}

ここで上記JSON中の uri とは,前述した「ATプロトコル上でのURL」(AT-uri)です。
またcidは,該当メッセージの本文が改ざんされていないことを証明するハッシュ値のようなものです。

AT-uri と cid は2つセットで扱う事が多いですね。
この2つの情報をそろえれば,Blueskyのサーバ上での1つのメッセージのありかが定まるんです。

以上のように embed プロパティを指定しておけば,
APIはその投稿命令を 「引用投稿したいんだな」と認識 してくれます。

Bluesky上での「引用投稿」の使いどころ

「botから引用投稿したい」というのはどういう場合か?

意外かもしれませんが,「引用投稿をしたいから」ではありません。

個人的には,最大の動機づけは
「画像をタイムラインに流したいから。」というものでした。

  • まず,画像や動画を(botではなく)手動で投稿しておく。
  • 次に,それらの投稿をbotから引用投稿する。

そうすれば,「すでに投稿済みのメディアをbotから引用する」という形式で
それら画像とか動画とかのメディアが,タイムラインに自動で・定期的に流れてくれますよね。

そんな事しなくても「画像や動画をbotから投稿すればいいじゃないか」ですって?
そうはいかないんだな,これが。

少しめんどいんです~。画像や動画の投稿が。

Blueskyでは,Twitter(X)のように手軽にはメディアをbot投稿できません。
手動で投稿した画像などには,http形式のURLが割り当てられないんです。
(AT-uriは割り当てられますが。)

それに,Googleスプレッドシート上に存在するのはシートとセル値のテキストデータであって,画像データとかは無い んですよ。

もし,どうしても画像のバイナリを Bluesky API に送信したい場合は
Google Drive上に画像ファイルを置いて,
GASからGoogle Driveの特定のフォルダに接続(サービス連携)しないといけません。

これがTwitterでbot投稿する時は,
「すでに手動で投稿済みのメディアファイル」には
Twitterのサーバー上で一意な画像URLが割り振られていて,
「その 画像URLをbot本文内にテキストとして書いておくだけ」で
自動的に画像や動画を表示してくれる。という仕様だったのに。

ブルースカイも同じ仕様にしてくれませんかね。

それで,画像投稿がちょっとめんどいと感じたので
画像をタイムラインにbotで流すために,代替手段として引用投稿をする んです。
本来の「引用」の用途と違いますけどね。

そして「どうして,画像をタイムラインに流したいのか?」というと
それは,私の場合は外国語を勉強するためのアカウント(語学bot)をたくさん作りたいので
外国語の文章や,発音の方法などが書かれた「勉強用の画像」を定期投稿したいんです。
自分で書いたPDF資料の断片的なスクショみたいなものを。
「繰り返しは記憶の母」なので,期間を置いて,同じ情報を繰り返し投稿したい。

botの文章で,勉強用の情報を文字で伝えるとしても
1投稿内の文字数には上限があります。
情報を圧縮して,取り入れやすい形・目に焼き付けやすい形で
つまり「画像で投稿したい」という事がよくありますのでね。

感想

え~感想ですが…まあ引用投稿,たしかにできるんですが。
あんま「便利」とは思わないかな…。

先述の通り,もしGASからも「画像を手軽にbot投稿できる」なら,こんな事しなくて済む んですよ。

それに,Twitter(X)でbot投稿する際には
引用したい投稿(ツイート)のhttp URLを,単にメッセージ本文内に含めておけば
それだけで引用RTができてましたよ。
メッセージ本文のテキスト編集だけで完結しており,通信をいじる必要は無かった。

Blueskyで引用投稿する時も,それぐらい簡単に出来たらいいのになあ…。
今後期待してますから,なんとか改良してもらえませんかね? よろしくお願いします。

関連リンク

『(GASサンプルコード) Bluesky APIにbot投稿する際,ハイパーリンク・ハッシュタグ・メンション記法などを本文に埋め込む方法(たった450行で,Googleスプレッドシートから自動投稿する時のfacetsの使い方が分かる)』
https://qiita.com/rwanda_go_tan/items/2ad80c8e14f0d1f884c1


『facets付与の汎用的なライブラリ。GASでBlueskyにbot投稿する時に使える便利関数。ぴったり千行のサンプルコードで,GoogleスプレッドシートからブルスカAPIに自動ポスト時にハイパーリンク・ハッシュタグ・メンションを自動付与』
https://qiita.com/rwanda_go_tan/items/9ccea753db72c3e08538


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


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


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

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?