見出し画像

Bluesky用自動投稿ツールを画像投稿できるように改造した


この記事の続きです。
前回はスプレッドシートにBlueskyに投稿したい文を登録しておいて、一定時間で自動投稿できるツールを作ったわけですが、
「テキスト投稿だけじゃつまらん!画像投稿もできるようにしちゃる!」
ということでツールに機能追加した、という話です。
もはや超簡易版じゃなくなりましたね…

準備

シートを作成する

シート例

当たり前の話ですが、前回のシートから画像に関するデータを入力する列が追加されています。B列に画像のURL、C列に「ALTテキスト」を入力します。
ALTテキストって何?という話ですが、「画像の代替テキスト」と言って要は画像の説明文にあたるテキストなんだとか。
あったほうが親切なんでしょうが、一応今回組んだスクリプトではC列が空欄でも画像投稿ができるようにしています。(その代わり、スクリプトで設定したデフォルトのALTテキストが添付されます)
あと、テキストのみの投稿も可能にしています。上の画像でいえば下半分、画像URLが書かれていない行です。

スプレッドシートの初期設定

前回記事でいう
・USER_IDとAPP_PASSWORDを確認する
・App Scriptを開く
・USER_IDとAPP_PASSWORDを入力する
です。この3点は変わらないので、そちらの記事をご参照ください。

コードの入力

//カスタムメニュー追加
function onOpen() {
    let ui = SpreadsheetApp.getUi();
    ui.createMenu('GAS実行')
    .addItem('実行', 'postSkyMessage')
    .addToUi();
    }
    
//ログイン情報
 const uid = PropertiesService.getScriptProperties().getProperty("USER_ID");
 const passwd = PropertiesService.getScriptProperties().getProperty("APP_PASSWORD");
    
//今日の日付を取得
 const today = new Date();
    
// Access認証情報を取得する
function getAccessToken() {
 //リクエストエンドポイント
 const endpoint = 'https://bsky.social/xrpc/com.atproto.server.createSession' ;
 //認証情報を作成する
  let auth = {
   'identifier': uid,
   'password': passwd
  }
 //リクエストオプション
 const options = {
  'method': 'post',
  'headers': {
    'Content-Type': 'application/json; charset=UTF-8',
   },
  'payload': JSON.stringify(auth),
  };
  //レスポンスを取得する
  const response = UrlFetchApp.fetch(endpoint, options);
  //accessJwtを取得する
  let json = JSON.parse(response.getContentText())
  let accessjwt = json.accessJwt;
  let refreshjwt = json.refreshJwt;
  let did = json.did;
  //スクリプトプロパティに格納する
  let prop = PropertiesService.getScriptProperties();
  prop.setProperty("accessjwt",accessjwt)
  prop.setProperty("refreshjwt",refreshjwt)
  prop.setProperty("did",did)
  }
    
//投稿内容を構築する
function postSkyMessage(){
  //シートを取得
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  //postリストを参照
  var sheet = ss.getSheetByName('postリスト');
  //データのあるセルの内、一番下にあるセルの行番号を取得
  var rows = sheet.getLastRow();
  //今日の日付(年月日)を文字列化
  var todayStr = Utilities.formatDate(today, 'JST', 'yyyy-MM-dd');
  //乱数生成
  var suuji = Math.floor(Math.random() * (rows - 1)) + 2;
  //ランダムに算出した行番号のpost文とURLを取得
  text = sheet.getRange(suuji, 1).getValue();
  imageUrl = sheet.getRange(suuji, 2).getValue();
  altText = sheet.getRange(suuji, 3).getValue();
  postDateCell = sheet.getRange(suuji, 4).getValue();
  //URL付きpost文を生成
  PostMessage = text + ' ' + imageUrl;
  // timestamp関数を呼び出し、PostMessage,suuji,sheet,todayStrを引数として渡す
  timestamp(PostMessage, suuji, sheet ,todayStr); 
  //URL有無により分岐
if(imageUrl==""){
  postonlytext(text);
  }else{
    postContentAndImage(imageUrl, text, altText);
  }
}

//テキストのみ投稿のためのセッションを作成
function createSession() {
  const url = 'https://bsky.social/xrpc/com.atproto.server.createSession'
  
  const payload = {
    identifier: uid,
    password: passwd,
  }
  
  const options = {
    method: 'post',
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
    payload: JSON.stringify(payload),
  }

  const response = UrlFetchApp.fetch(url, options)
  return JSON.parse(response.getContentText())
}

//Blueskyに投稿(画像なし)
function postonlytext(text) {
    const url = 'https://bsky.social/xrpc/com.atproto.repo.createRecord'
    const session = createSession(); 
    Logger.log("After calling createSession"); 
    const payloadNashi = {
      repo: session.handle,
      collection: 'app.bsky.feed.post',
      record: {
        text: text,
        createdAt: new Date().toISOString(),
      },
    }
    try {
    const options = {
      method: 'post',
      headers: {
        'Content-Type': 'application/json',
        Authorization: 'Bearer ' + session.accessJwt,
      },
      payload: JSON.stringify(payloadNashi),
    }
  var response = UrlFetchApp.fetch(url, options); 
  console.log("ポストに成功しました。"); 
  console.log(response);
  } catch (error) {
 console.error("ポスト中にエラーが発生しました: " + error);   
   }
  }

//Blueskyに投稿(画像あり)
function postContentAndImage(imageUrl, text, altText) {
  try {
   // 画像をアップロード
   var image = createImageForPost(imageUrl);
   Logger.log("ポストimage: %s", image);
   // ポストする際にBlueskyに投げるリクエストに必要な情報の作成
   var record = createRecord(text, image.cid, image.mimeType, image.size, altText);
   Logger.log("record: %s", record);
   // 実際にポスト
   var response = post(record);
   console.log("ポストに成功しました。");
   console.log(response);
    } catch (error) {
   console.error("ポスト中にエラーが発生しました: " + error);
     }
   }
    
// BlueSkyに画像をアップロードする
function uploadImage(imageUrl) {
 let prop = PropertiesService.getScriptProperties();
 let accessjwt =prop.getProperty("accessjwt");
 var blob = UrlFetchApp.fetch(imageUrl).getBlob();
 var options = {
  'method': 'post',
  'headers': {
  'Authorization': 'Bearer ' + accessjwt
    },
  'payload': blob
   };
 var url = 'https://bsky.social/xrpc/com.atproto.repo.uploadBlob' ;
 var response = UrlFetchApp.fetch(url, options);
   return JSON.parse(response.getContentText());
 }
    
// 画像をポストするのに必要な形に関数を加工する
function createImageForPost(imageUrl) {
  var imageData = uploadImage(imageUrl);
  var cid = imageData.blob.ref.$link;
  var mimeType = imageData.blob.mimeType;
  var size = imageData.blob.size
   return {
    'cid': cid,
    'mimeType': mimeType,
    'size': size,
     };
 }
    
// ポストに必要なレコードの作成
function createRecord(text, cid, mimeType, size, altText) {
  return {
   'text': text,
   'createdAt': (new Date()).toISOString(),
   'embed': {
     '$type': 'app.bsky.embed.images',
     'images': [
      {
     'image': {
       '$type': 'blob',
       'ref': {
       '$link': cid
         },
      'mimeType': mimeType,
      'size': size
      },
      'alt': altText
      }
     ]
     },
     'alt': '' // このaltはembedオブジェクト全体の代替テキストです
    };
  };
       
// BlueSkyにポストする
function post(record) {
  // accessjwtを取得する
  let prop = PropertiesService.getScriptProperties();
  let accessjwt = prop.getProperty("accessjwt");
  let did = prop.getProperty("did");
  // 投稿先エンドポイント
  var url = 'https://bsky.social/xrpc/com.atproto.repo.createRecord' ;
  record.embed.images.forEach(function(imageObj) {
  if (!imageObj.alt) {
   imageObj.alt = 'これは本文と同時に投稿された画像です'; // alt属性がない場合はデフォルトのテキストを設定
     }
   });
   var data = {
     'repo': did,
     'collection': 'app.bsky.feed.post',
     'record': record
    };
   var options = {
    'method': 'post',
    'headers': {
    "Authorization": "Bearer " + accessjwt,
    'Content-Type': 'application/json; charset=UTF-8'
   },
    'payload': JSON.stringify(data)
     };
   var response = UrlFetchApp.fetch(url, options);
    return JSON.parse(response.getContentText());
  }
    
//投稿した日時をシートに書き込む
function timestamp(PostMessage, suuji, sheet, todayStr) {
  // 指定された行のD列に値を設定
  sheet.getRange(suuji, 4).setValue(todayStr);
  // E2セルにPostMessageを設定
  sheet.getRange("E2").setValue(PostMessage);
 }

長っ。いや、専門家が見ると短っ!って思うでしょうけど、素人からすればここまで長くなるとは予想外でした。
とにかくApp Scriptにそのままコピペすればいい…はずです。

で、動作確認した結果がこちら。画像はWikipediaからの引用です。

画像付き投稿の例。前回に続いて星座にしているのは、ただの趣味です

画像をクリックすればALTテキストも表示されます。

ALTテキスト

テキストのみの投稿もできました。

画像無しの投稿

あとがき

今回は苦労しました。というのは、4月にBlueskyのプロトコルが仕様変更されたそうで、それ以前に作られた投稿ツールのスクリプトを参考にしても、そのまま拝借するわけにはいかなかったんです。
それで、新仕様に合わせて自力でスクリプトを組まざるを得なかったと。
何しろGASにここまでのめりこんだのは初めてだったもので、まともに動くスクリプトになかなかこぎつけられませんでした。

まあ、一応それなりに動くツールができてよかったです。まだ直すべき部分はありそう(特に画像データの容量制限に対応してない点とか)ですが、それはこのツール動かしながら追々直していこうと思っています。

この記事が気に入ったらサポートをしてみませんか?