やりたいこと
2024年2月6日より、招待制を廃止して、誰でも登録できるようになった、短文投稿型SNSの「Bluesky」。
今後、X (旧Twitter) の後身として、注目されています。
そこで、自分が運営している写真展示Webサイトから、「オススメ写真を毎日自動投稿するBot」を Bluesky でやりたいと思いましたので、作ってみました。
完成像
絵にするほどでもないですが、やりたいことは上記の図のようなイメージです。
- Azure Functions のタイマートリガーで定期的に起動する
- 自分の写真展示 Web サイトから、今日の写真とタイトルなどの情報をダウンロード
- Bluesky へ画像のアップロード、投稿
今回の記事は、2 と 3 について説明します。
この記事のターゲット
この記事は以下のお悩みをお持ちの方に、お役立てできると思います。
- Node.js (Typescript) で Bluesky に画像をアップロードして投稿したい!
また、今後の記事で、以下の点についても言及する予定です。
- Bluesky へ定期的に自動投稿できる Bot を作りたい!
- 画像をそのまま投稿するのと、Webサイトカードで投稿するのとでどんな違いがあるか知りたい!
事前準備
SDK の言語選定
まずは皆さんが一番知りたいであろう、 Bluesky への画像投稿について。
2024年2月現在、Bluesky には、Typescript, Python, Dart, Go の4つの言語の SDK が開発されています。もちろん、REST API での提供もありますので、他の言語でも投稿しようと思えばできると思います。
Bluesky Developer APIs
今回は、Official SDK であることと、自分が使い慣れていることから、 Typescript を選択しました。
制限事項 (画像サイズ制限)
2024年2月現在、Bluesky の画像サイズ制限は 1,000,000 byte (1MB弱) です。
このサイズを超える画像をアップロードすると、 413 Request Entity Too Large が返却されます。
Bluesky アプリパスワードの取得
まず事前に、Bluesky に API 経由でアクセスするために必要なアプリパスワードを生成します。
アカウントを作成した時に使ったパスワードでも API 経由での使用は可能ですが、漏洩した時のリスクを考えて、自動生成したものを使うようにしましょう!
https://bsky.app/settings に Web ブラウザでアクセスし、下の方にある「高度な設定」から「アプリパスワード」を選択します。
「アプリパスワードを追加」を押下すると、名前を入力する欄が出てきます。
デフォルトはランダムな単語が入っていますが、いつ、何のために生成したパスワードか分かるような名前にしておくとよいでしょう。
「アプリパスワードを作成」を押下すると、以下のようにパスワードが生成されます。
このパスワードは後で使うので、外部に漏れないよう注意の上、大切に保管しておいてください。
コード
基本的には、以下のチュートリアルに書いていることに則って進めていただければOKです。
Creating a post | Bluesky
ただし、チュートリアルでは若干不親切な所がありますので、実際に記載したコードをお見せしてから、一つ一つ説明していきます。
ソースの全体は以下のリポジトリにコミットしています。
冒頭の部分を埋めれば簡単に試せるようにしていますので、是非ご自身で動かしてみてください。
upload-photo-to-bluesky-nodejs | GitHub
自分のWebサイトから画像を取得する部分
画像取得部分は下記の通りです。
ここは、展開されているWebサイトの仕様次第な所がありますので、あまり深くは説明しません。
誰でもアクセスできる場所に画像が公開されていて、それを GET リクエストで取りに行く想定で書いています。
レスポンスされた内容を、 MIME タイプ含めて Blob
オブジェクトに格納しています。
import axios from "axios";
/**
* 自分のWebサイトから画像を取得する
* @param myWebsite 自分のWebサイトのホスト名 (例: https://example.com)
* @param imagePath 画像のパス (例: /photos/001.jpg)
* @returns 取得した画像の Blob オブジェクト
*/
async function getPhoto(myWebsite: string, imagePath: string): Promise<Blob> {
// 自分のWebサイトから画像を取得する
const photoData = await axios.get<Buffer>(
`${MY_WEBSITE}${IMAGE_PATH}`,
{
responseType: "arraybuffer"
}
);
// 画像の Content-Type を使用して、Blob型オブジェクトに変換する
// content-type レスポンスヘッダには、'image/jpeg' 等の MIME タイプが含まれる想定
return new Blob(
[photoData.data],
{
type: photoData.headers["content-type"]
}
);
}
Bluesky へ投稿する部分
そして肝となる Bluesky への投稿部分です。
手順としては、以下の通りです。
- Bluesky クライアントを作成し、どこのインスタンスへ接続するかを指定する
- ユーザのIDとアプリパスワードを渡し、Bluesky へログインする
- 画像をアップロードする
- 投稿文をリッチテキスト形式で作成し、メンションやURLなどをリンクに自動変換してもらう
- アップロードした画像を紐付け、投稿を作成する
ユーザIDは通常 bsky.social で終わるもの、アプリパスワードは事前準備の章で作成したものを使用してください。
import { BskyAgent, RichText } from "@atproto/api";
/**
* Bluesky に画像を投稿する
* @param photoTitle 写真のタイトル
* @param photoData getPhoto() で取得した画像の Blob オブジェクト
* @param photoUrl 写真へのリンクURL
* @param bskyService Bluesky インスタンスのURL
* @param bskyIdentifier Bluesky ID
* @param bskyAppPassword Bluesky アプリパスワード
*/
async function postPhotoToBluesky(photoTitle: string, photoData: Blob, photoUrl: string, bskyService: string, bskyIdentifier: string, bskyAppPassword: string): Promise<void> {
const agent = new BskyAgent({
service: bskyService
});
// アプリパスワードを使用して Bluesky にログインする
await agent.login({
identifier: bskyIdentifier,
password: bskyAppPassword
});
// 画像をアップロードする
// Blob型オブジェクトを Uint8Array に変換
const dataArray: Uint8Array = new Uint8Array(await photoData.arrayBuffer());
const { data: result } = await agent.uploadBlob(
dataArray,
{
// 画像の形式を指定 ('image/jpeg' 等の MIME タイプ)
encoding: photoData.type,
}
);
// 投稿文の作成
const text = `「${photoTitle}」
${photoUrl}`
// テキストをメンション、リンク、絵文字を含むリッチテキストに変換
const rt = new RichText({
text: text
})
await rt.detectFacets(agent) // メンションやリンクを自動で検出する
// 投稿を作成
await agent.post({
text: rt.text,
facets: rt.facets,
embed: {
$type: 'app.bsky.embed.images',
images: [
{
alt: photoTitle,
image: result.blob, // 画像投稿時にレスポンスをここで渡すことにより、投稿と画像を紐付け
aspectRatio: {
// 画像のアスペクト比を指定 (指定しないと真っ黒になるので注意)
width: 3,
height: 2
}
}
// images は4つまで指定可能
]
},
langs: ["ja-JP", "en-US"],
createdAt: new Date().toISOString(), // 投稿日時を指定する
});
}
詰まりポイント
試していていくつか詰まった箇所がありましたので、共有します。
uploadBlob の引数、何設定すればいいの!?
画像のアップロード時に使用する uploadBlob()
メソッドですが、チュートリアルでは何を指定すればよいのか分かりませんでした。
const image = 'data:image/png;base64,...'
const { data } = await agent.uploadBlob(convertDataURIToUint8Array(image), {
encoding,
})
結論としては、第1引数は string
または Uint8Array
のいずれかで指定します。
結局 Uint8Array
型で渡すことで決着しましたが、もしかすると、base64 エンコーディング文字列を渡すこともできるのかもしれません・・・。
また、encoding
については、画像の MIME タイプで指定するのが良いようです。チュートリアルでは何も書いてなかったので、かなり試行錯誤しました。私は基本写真データを扱うので、JPEG形式での連携、すなわち image/jpeg
を指定することで無事投稿できました。
aspectRatio を指定しないと画像が真っ黒になる!
post()
側で画像の情報を渡す際に指定する aspectRatio
ですが、これを指定しないと画像が真っ黒になって表示されてしまいました。
await agent.post({
text: 'I love my cat',
embed: {
$type: 'app.bsky.embed.images',
images: [
// can be an array up to 4 values
{
alt: 'My cat mittens', // the alt text
image: data.blob,
aspectRatio: {
// a hint to clients
width: 1000,
height: 500
}
}],
},
createdAt: new Date().toISOString()
})
// a hint to clients
って書いてあるからてっきり任意項目かなと思い、最初指定しなかったら画像が真っ黒になってしまったので注意が必要です。
従って、画像のアスペクト比が動的な場合は、ダウンロードした画像を一旦 sharp 等を用いて開き、アスペクト比を解析してやる必要がありそうです。
おわりに
招待制が廃止されて1週間、まだまだドキュメントや Tips が少ない中で、ひとまず画像を投稿するという基本的な所ができて良かったです。
次は、ここで実装した画像投稿のロジックを Azure Functions のタイマートリガーで、定期的に投稿する Bot を作成するまでを記事にしたいと思っています。
一応実装はできているので、近日中にOUTPUTできればと思います。
大変期待している SNS ですので、 X とともに、成長してくれることを期待しています!
宣伝
- 鉄道系のWebサイト・Webアプリを公開する個人サークル「Studio UXM」を運営しています
- Studio UXM
- Webアプリ: 配線略図エディタ
- Webアプリ: 路線図エディタ(仮)
- 鉄道写真展示 Web サイト: Urban eXpress Museum
- Bluesky のアカウントやってます
- X のアカウントも是非フォローお願いします