Blueskyをはじめて、いろいろできるようなので、
Node.jsで投稿してみたときの備忘録(*´ω`*)
楽だけど、ちょっと癖がある感じがする。。
利用するパッケージ(@atproto/api
)
- bluesky-social/atproto: Social networking technology created by Bluesky
- atproto/packages/api · GitHub
⚠️ This is not production-ready software. This project is in active development ⚠️
実稼働向けのソフトウェアではない。現在開発中です。
という注意書きがあるけど、公式のAPIクライアントが用意されている感じ。
ログイン
ログインはこんな感じ。BskyAgent
でagentを作ってlogin
する。
import { BskyAgent, RichText } from "@atproto/api"; const service = "https://bsky.social"; const identifier = "ログインするメールアドレス"; const password = "App Password"; // Agentの初期化 const agent = new BskyAgent({ service }); // ログイン await agent.login({ identifier, password });
APIトークンの仕組みはないけど、App Passwordという発行できるパスワードがあるので、
それを使うといい感じ。
- App Passwordsのページ: https://bsky.app/settings/app-passwords
- SettingsページのAdvanced->App passwordsから
テキストの投稿
シンプルなテキストはこんな感じ
await agent.post({ text: "なるほど", });
URLやメンションをリンクする
これだと、URLやメンションなどのリンクは付与されない感じなので、
RichText
を使って、検出・変換をする必要がある。
const rt = new RichText({ text: "なるほど...\nhttps://github.com/bluesky-social/atproto" }); // リンクやメンションの自動検出・変換 await rt.detectFacets(agent); await agent.post({ text: rt.text, fasets: rt.facets, });
リンクカードを表示する
これだとただリンクされただけのテキストになる。
OGP画像などのリンクカードも自分でデータを渡さないといけない。
内容はこんな感じ。
const embed = { $type: "app.bsky.embed.external", external: { uri: "クリック時のURL", thumb: { $type: "blob", ref: { $link: "Bluesky上にある画像のID" }, mimeType: "image/jpeg", // mimeType size: 40627 // 画像のサイズ }, title: "リンクカードのタイトル, description: "リンクカードのdescription" } } await agent.post({ text: rt.text, fasets: rt.facets, embed: embed, });
このembed
に設定する情報を取得するために、
- テキストからURLの取得
- URLからOGP情報を取得
- 画像をアップロードしてIDを取得
- 投稿
をおこなう必要がある。
テキストからURLの取得
detectFacets()
を実行したあとだと、
検出された結果が入っているのでそれを活用する。
const rt = new RichText({ text: "なるほど...\nhttps://github.com/bluesky-social/atproto" }); await rt.detectFacets(agent); console.log(JSON.stringfy(rt, null, 2)); // { // "text": "つまりは。。\nhttps://github.com/\nこういうことか?", // "facets": [ // { // "index": { // "byteStart": 19, // "byteEnd": 38 // }, // "features": [ // { // "$type": "app.bsky.richtext.facet#link", // "uri": "https://github.com/" // } // ] // } // ] // }
少し長くて、煩雑だけど、最初に見つかった
facets[].features[].uri
を返す。
// RichTextからURLを取得する async function findUrlInText(rt: RichText): Promise<string | null> { if (rt.facets.length < 1) return null; for (const facet of rt.facets) { if (facet.features.length < 1) continue; for (const feature of facet.features) { if (feature.$type != "app.bsky.richtext.facet#link") continue; else if (feature.uri == null) continue; return feature.uri as string; } } return null; }
URLからOGP情報を取得
これらを使って、みつけたURLから情報を取得する。
そのままだとアップロードで失敗する場合もあるので、
画像サイズを小さくしておく必要があるっぽい。
- open-graph-scraper ... OG情報の取得
- sharp or imagescript ... 画像サイズの変換
慣れているsharpを使って例。
type OgInfo = { siteUrl: string; ogImageUrl: string; type: string; description: string; title: string; imageData: Uint8Array; }; async function getOgInfo(url: string): Promise<OgInfo> { // open-graph-scraperでURLからOG情報を取得 const { result } = await ogs({ url: url }); // fetchで画像データを取得 const res = await fetch(result.ogImage?.at(0)?.url || ""); const buffer = await res.arrayBuffer(); // sharpで800px二リサイズ const compressedImage = await sharp(buffer) .resize(800, null, { fit: "inside", withoutEnlargement: true }) .jpeg({ quality: 80, progressive: true }) .toBuffer(); // OgInfoを返す return { siteUrl: url, ogImageUrl: result.ogImage?.at(0)?.url || "", type: result.ogImage?.at(0)?.type || "", description: result.ogDescription || "", title: result.ogTitle || "", imageData: new Uint8Array(compressedImage), }; }
画像をアップロードしてIDを取得
OG情報とOG画像データを取得できたので、
Bluesky側にアップロードする。
import { BskyAgent } from "@atproto/api"; type UnPromise<T> = T extends Promise<infer R> ? R : never; type UploadImageResponse = UnPromise<ReturnType<BskyAgent["uploadBlob"]>>; async function uploadImage(ogInfo: OgInfo): Promise<UploadImageResponse> { return await this.agent.uploadBlob(ogInfo.imageData, { encoding: "image/jpeg", }); }
型定義がうまく見つけられなかったので、パズルを解いている感じに。。
投稿する
const rt = new RitchText({ text: [ "つまりは。。", "https://github.com/", "@kirapuka.bsky.social", "こういうことか?", ].join("\n"), }); await rt.detectFacets(agent); // テキストからURLの取得 const url = await findUrlInText(rt); // URLからOGP情報を取得 const ogInfo = await getOgInfo(url); // 画像をアップロードしてIDを取得 const uploadedRes = await uploadImage(ogInfo); const embed = { $type: "app.bsky.embed.external", external: { uri: ogInfo.siteUrl, thumb: { $type: "blob", ref: { $link: uploadedRes.data.blob.ref.toString(), }, mimeType: uploadedRes.data.blob.mimeType, size: uploadedRes.data.blob.size, }, title: ogInfo.title, description: ogInfo.description, }, } } // 投稿 await agent.post({ text: rt.text, fasets: rt.facets, embed: embed, });
できた(*´ω`*)!!
以上!! これでいろいろできそう。。(*´ω`*)
おまけ: その他のAPI
@atproto/apiのREADMEを見ると、
いろいろできるっぽい。
// Feeds and content await agent.getTimeline(params, opts) await agent.getAuthorFeed(params, opts) await agent.getPostThread(params, opts) await agent.getPost(params) await agent.getPosts(params, opts) await agent.getLikes(params, opts) await agent.getRepostedBy(params, opts) await agent.post(record) await agent.deletePost(postUri) await agent.like(uri, cid) await agent.deleteLike(likeUri) await agent.repost(uri, cid) await agent.deleteRepost(repostUri) await agent.uploadBlob(data, opts) // Social graph await agent.getFollows(params, opts) await agent.getFollowers(params, opts) await agent.follow(did) await agent.deleteFollow(followUri) // Actors await agent.getProfile(params, opts) await agent.upsertProfile(updateFn) await agent.getProfiles(params, opts) await agent.getSuggestions(params, opts) await agent.searchActors(params, opts) await agent.searchActorsTypeahead(params, opts) await agent.mute(did) await agent.unmute(did) // Notifications await agent.listNotifications(params, opts) await agent.countUnreadNotifications(params, opts) await agent.updateSeenNotifications() // Identity await agent.resolveHandle(params, opts) await agent.updateHandle(params, opts) // Session management await agent.createAccount(params) await agent.login(params) await agent.resumeSession(session)