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テキストも表示されます。
テキストのみの投稿もできました。
あとがき
今回は苦労しました。というのは、4月にBlueskyのプロトコルが仕様変更されたそうで、それ以前に作られた投稿ツールのスクリプトを参考にしても、そのまま拝借するわけにはいかなかったんです。
それで、新仕様に合わせて自力でスクリプトを組まざるを得なかったと。
何しろGASにここまでのめりこんだのは初めてだったもので、まともに動くスクリプトになかなかこぎつけられませんでした。
まあ、一応それなりに動くツールができてよかったです。まだ直すべき部分はありそう(特に画像データの容量制限に対応してない点とか)ですが、それはこのツール動かしながら追々直していこうと思っています。
この記事が気に入ったらサポートをしてみませんか?