最終更新: 2026-04-28
最初はシンプルすぎて壊れた
Blueskyに自動投稿したいと思ったのは、毎朝同じ作業を手でやっているのがバカバカしくなったから。
AI生成したテキストをコピペして、ブラウザを開いて、投稿ボタンを押す。たった1分だけど、毎日続くと地味に消耗する。
最初に書いたコードは本当に最低限だった。
[コード略 — 詳細は元記事参照]
これ、createdAtをハードコードしてるせいで、Blueskyのタイムラインに過去の日付で投稿されてた。
気づいたのは3日後。その間ずっと2026年元日の投稿として流れてたわけで、エンゲージが全然つかない理由がそれだったと思うとちょっと面白い。
正しいタイムスタンプと文字数制限の話
Blueskyは「300文字制限」だけど、これが「Unicodeのコードポイント数」じゃなくて「UTF-8バイト数」で判定されてる、という罠がある。
日本語1文字 = 3バイト。つまり日本語だと実質100文字ちょっとで上限に引っかかる。
[コード略 — 詳細は元記事参照]
createdAtはちゃんとUTCで現在時刻を入れる。
[コード略 — 詳細は元記事参照]
これだけで「あれ、なんか投稿されてるけどタイムラインに出てこない」問題はほぼ解決した。
ハッシュタグをリッチテキストで埋め込む
テキストに #Python自動化 と書いても、ただの文字列として扱われる。
Blueskyのハッシュタグをクリッカブルにするには facets という構造を使う必要がある。
これが最初わからなくて、「なんでタグがリンクにならないんだ」と30分悩んだ。
[コード略 — 詳細は元記事参照]
バイトオフセットで位置指定するのが罠で、文字数じゃなくてバイト数で「どこからどこまでがタグか」を指定する必要がある。
日本語混じりのテキストで len(text[:n]) をそのまま使うとズレる。
セッション管理とレート制限の現実
毎回ログインしてセッションを取得するのは非効率で、しかもレート制限に引っかかる可能性がある。
com.atproto.server.createSessionは1時間に何十回も叩ける設計ではない。
[コード略 — 詳細は元記事参照]
bluesky_session.json としてローカルに保存して使い回す。
これで毎回ログインしなくなった。refreshJwtの有効期限は確か90日ぐらいなので、launchdで毎日動かしてれば実質ずっと使える。
画像つき投稿の落とし穴
テキストだけの投稿に比べて、画像付きはひと手間かかる。
まずBluesky側にアップロードしてblobのCIDを取得し、それをレコードに埋め込む形になる。
[コード略 — 詳細は元記事参照]
画像の上限は1MB。それ以上はアップロードが 400 Bad Request で落ちる。
僕はサムネ生成後にPillowでリサイズしてから渡している。
launchd で毎日3回動かす仕組み
スクリプト単体ができたあと、launchdに組み込んで自動化した。
07:00 / 12:30 / 20:30 の3回。それぞれ生成AIが作ったテキストをキューから取り出して投稿する流れ。
[コード略 — 詳細は元記事参照]
パスワードはBlueskyの設定画面から「アプリパスワード」を発行する。
メインパスワードをplistに書くのは怖いのでアプリパスワードを使っている。これ大事。
まとめ
完成形のコードは200行いかない。ポイントを整理すると:
createdAtは UTCの現在時刻(ハードコード禁止)- 文字数制限はバイト数で判定(日本語は3バイト/文字)
- ハッシュタグは
facetsで位置指定しないとクリックできない - セッションはキャッシュして再利用
- 画像は事前にBluesky側にアップロードしてからCIDを参照
最初の3日間、元日のタイムスタンプで投稿し続けてたのは今となっては笑い話だけど、リアルタイムで確認してなかったのが原因だった。
自動化は「動いた」で終わりにしないで、最初の数回は手動で確認する習慣をつけてからlaunchdに任せるほうがいい。完璧より継続——でも最低限の動作確認はしておく、というバランスが今のところうまくいってる✨
[コード略 — 詳細は元記事参照]