RSSからBlueskyに自動投稿する
https://gyazo.com/8b1130d38a28a5caf56e68c35d811b1a
良ければやってみてください!
ちなみに私が実装しているのは以下です。
個人ブログのRSS
アレンジ
function updateFeedAndPost() の部分をアレンジしてみます。
ご利用は自己責任でお願いします。
Posted = FALSE の行があれば FALSE になっているものを連続投稿
Cosenseだとたくさんページ(カード)ができるので、すべてのページをBlueskyに投稿したい人向け code: js
function updateFeedAndPost() {
/* --------- (A) 新着を取得してシートへ追加 --------- */
const items = getRss(FEED_URL); // RSS取得関数(既存)
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_NAME) || ss.insertSheet(SHEET_NAME);
// ヘッダーが無ければ作成
if (sheet.getLastRow() === 0) {
sheet.appendRow(['Title','Link','Description','FetchedAt',
'Posted','PostedAt','BlueskyURL']);
}
// 既存リンク一覧をセット化して重複を防ぐ
const lastRow = sheet.getLastRow();
const existing = lastRow > 1
? new Set(sheet.getRange(2, 2, lastRow - 1, 1).getValues().flat())
: new Set();
// 新規RSS項目を抽出
const freshRows = (items || [])
.filter(it => it && it.link && !existing.has(it.link))
.map(({title, link, description}) => [
title || '', link || '', description || '', new Date(),
false, '', ''
]);
// シートに追加
if (freshRows.length) {
sheet.getRange(sheet.getLastRow() + 1, 1, freshRows.length, 7)
.setValues(freshRows);
Logger.log(新規追加: ${freshRows.length} 行);
} else {
Logger.log('新規記事なし');
}
/* --------- (B) Posted = FALSE の行があれば投稿 --------- */
const data = sheet.getDataRange().getValues();
const header = data0 || []; const colPosted = header.indexOf('Posted');
if (colPosted === -1) {
Logger.log('Posted列が見つかりません。');
return;
}
// 未投稿行があるか確認
const hasUnposted = data.slice(1).some(row => rowcolPosted === false); if (hasUnposted) {
Logger.log('投稿対象(Posted=FALSE)あり → postUnsentToBluesky() 実行');
postUnsentToBluesky(); // 未投稿行を投稿する関数(既存)
} else {
Logger.log('投稿対象(Posted=FALSE)なし');
}
}
#日記 のタグがついているカードのみをBuleSkyに投稿したい人向け
条件
Description に #日記 文字列がある
Posted = FALSE である
function updateFeedAndPost() と、function postUnsentToBluesky() を変更する
code:js
/**
* ========== 運用用 ==========
* (A) 新着フィードをシートへ追加
* (B) Posted = FALSE の行を Bluesky へ投稿
* ↳ 投稿成功した行は Posted=TRUE, PostedAt, BlueskyURL を更新
*
* これを時間駆動トリガ(例: 15 分ごと)に設定しておく。
*/
function updateFeedAndPost() {
/* --------- (A) 新着を取得してシートへ追加 --------- */
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_NAME) || ss.insertSheet(SHEET_NAME);
// ヘッダー保証
if (sheet.getLastRow() === 0) {
sheet.appendRow(['Title','Link','Description','FetchedAt',
'Posted','PostedAt','BlueskyURL']);
}
// 既存リンクを集合に
const lastRow = sheet.getLastRow();
const existing = lastRow > 1
? new Set(
sheet.getRange(2, 2, lastRow - 1, 1)
.getValues()
.flat()
)
: new Set();
// 新規行を抽出
const freshRows = (items || [])
.filter(it => it && it.link && !existing.has(it.link))
.map(({title, link, description}) => [
title || '', link || '', description || '', new Date(),
false, '', ''
]);
// 追加
if (freshRows.length) {
sheet.getRange(sheet.getLastRow() + 1, 1, freshRows.length, 7)
.setValues(freshRows);
Logger.log(新規追加: ${freshRows.length} 行);
} else {
Logger.log('新規記事なし');
}
/* --------- (B) 条件一致(#日記 & Posted=FALSE)があれば一括投稿 --------- */
const data = sheet.getDataRange().getValues();
const header = data0 || []; const colDescription = header.indexOf('Description');
const colPosted = header.indexOf('Posted');
if (colDescription === -1 || colPosted === -1) {
Logger.log('必要なヘッダー列(Description/Posted)が見つかりません。');
return;
}
// 2行目以降に対象が1つでもあれば true
const hasUnpostedDiary = data.slice(1).some(row => {
return (typeof desc === 'string') && desc.includes('#日記') && (posted === false);
});
if (hasUnpostedDiary) {
Logger.log('投稿対象(#日記 かつ Posted=FALSE)あり → postUnsentToBluesky() 実行');
postUnsentToBluesky(); // 既存の一括投稿関数を1回だけ呼ぶ
} else {
Logger.log('投稿対象(#日記 かつ Posted=FALSE)なし');
}
}
code:js
/**
* Bluesky へ未投稿行を送信し、シートを更新する
* シートに残っている Posted=FALSE の行だけ Bluesky へ投稿し
* 成功したら Posted / PostedAt / BlueskyURL を更新する
* 失敗時したら Posted は FALSE のまま残るので再実行でリトライ可
* 本文は 300 文字制限を自動トリム
* 外部リンクカードにサムネイル画像を付与(og:image → blob アップロード)
*/
function postUnsentToBluesky() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) return;
/* ===== 認証 ===== */
const USER = PropertiesService.getScriptProperties().getProperty('BSKY_USER');
const PWD = PropertiesService.getScriptProperties().getProperty('BSKY_APP_PWD');
if (!USER || !PWD) { Logger.log('USER/PWD 未設定'); return; }
let accessJwt, did, handle;
try {
const loginRes = UrlFetchApp.fetch(
{ method:'post', contentType:'application/json', muteHttpExceptions:true,
payload: JSON.stringify({ identifier: USER, password: PWD }) }
);
if (loginRes.getResponseCode() !== 200) {
Logger.log([LOGIN] ${loginRes.getResponseCode()} : ${loginRes.getContentText()});
return;
}
({ accessJwt, did, handle } = JSON.parse(loginRes.getContentText()));
} catch (e) { Logger.log(LOGIN 例外: ${e}); return; }
/* ===== シート読み込み ===== */
const data = sheet.getDataRange().getValues(); data.shift(); // ヘッダー
const col = { title:0, link:1, desc:2, posted:4, postedAt:5, url:6 };
const bearer = { Authorization:Bearer ${accessJwt} };
const tagsStr = tagsArr.join(' ');
const tagLen = tagsStr.length + 2; // "\n\n" + tags
data.forEach((row, i) => {
// 1) 既に投稿済みならスキップ
// 2) 「#日記」を含まない行はスキップ ←★ これがポイント
if (!desc.includes('#日記')) {
Logger.log([SKIP] row=${i+2} : タグ #日記 なし);
return;
}
if (!url) {
Logger.log([SKIP] row=${i+2} : Link なし);
return;
}
/* ---------- 1) 本文(300 文字制限) ---------- */
const title = trimTitle(rowcol.title, 300 - url.length - tagLen - 1); const text = ${title}\n${url}\n\n${tagsStr};
/* ---------- 2) facet 生成 ---------- */
const facets = buildFacets(text, url, tagsArr);
/* ---------- 3) サムネイル取得 & blob アップロード ---------- */
let thumbBlob;
try {
const thumbUrl = extractOgImage(url);
if (thumbUrl) {
const imgResp = UrlFetchApp.fetch(thumbUrl, { muteHttpExceptions:true });
const blob = imgResp.getBlob();
if (blob.getBytes().length <= 950000) {
const uploadRes = UrlFetchApp.fetch(
{ method:'post', headers: bearer, payload: blob }
);
if (uploadRes.getResponseCode() === 200) {
thumbBlob = JSON.parse(uploadRes.getContentText()).blob;
}
}
}
} catch(e) { Logger.log([THUMB] row=${i+2} : ${e}); }
/* ---------- 4) レコード ---------- */
const record = {
$type : 'app.bsky.feed.post',
text,
facets,
createdAt : new Date().toISOString(),
embed : {
$type : 'app.bsky.embed.external',
external : {
uri : url,
...(thumbBlob ? { thumb: thumbBlob } : {})
}
}
};
/* ---------- 5) 投稿 ---------- */
try {
const resp = UrlFetchApp.fetch(
{ method:'post', contentType:'application/json', headers: bearer,
muteHttpExceptions:true,
payload: JSON.stringify({ repo:did, collection:'app.bsky.feed.post', record }) }
);
Logger.log([POST] row=${i+2} status=${resp.getResponseCode()});
if (resp.getResponseCode() !== 200) return;
const { uri } = JSON.parse(resp.getContentText());
const webUrl = atUriToWebUrl(uri, handle);
const r = i + 2;
sheet.getRange(r, col.posted+1 ).setValue(true);
sheet.getRange(r, col.postedAt+1).setValue(new Date());
sheet.getRange(r, col.url+1 ).setValue(webUrl);
Utilities.sleep(1500);
} catch(e) {
Logger.log([POST 例外] row=${i+2} : ${e});
}
});
}