DenoでBlueskyに𝑹𝒊𝒄𝒉 𝒕𝒆𝒙𝒕を投稿する
先月より、Bluesky Socialを利用しています。かつてのThe Bird App[1]のような雰囲気があるSNSです。
Bluesky(というかAT Protocol)はまだβ版ではありますが、APIが公開されており、それを使用して投稿などを行うことが可能です。
個人的に特徴的だと思ったのは、Rich textをサポートしていることです。
将来的にはboldやitalicなども追加される可能性があるようですが、現状はメンションや外部URLへのリンクを作る機能のみとなっています。
本記事ではDenoを使ってBlueskyへ𝑹𝒊𝒄𝒉な投稿を行う方法を説明します。
メンションする
基本のコードを示します。
@yui.bsky.socialへメンションを飛ばすコードです。
import AtprotoAPI from "npm:@atproto/api";
const { BskyAgent, RichText } = AtprotoAPI;
import "https://deno.land/std/dotenv/load.ts";
const service = "https://bsky.social";
const agent = new BskyAgent({ service });
const identifier = Deno.env.get("BLUESKY_IDENTIFIER");
const password = Deno.env.get("BLUESKY_PASSWORD");
await agent.login({ identifier, password });
const text = "@yui.bsky.social こんにちは!"
const rt = new RichText({ text });
await rt.detectFacets(agent); // automatically detects mentions and links
await agent.post({
$type: "app.bsky.feed.post",
text: rt.text,
facets: rt.facets,
});
console.log(rt.text, rt.facets)
認証にはdeno_std/dotenvを使っています。以下のようなenvファイルが必要です。
BLUESKY_IDENTIFIER=xxx.bsky.social
BLUESKY_PASSWORD=xxxxxxxxxxxxxxx
重要なのはpost直前のawait rt.detectFacets(agent);
です。これで生成されるrt.facets
をpostに使わないととメンションになりません(URL文字列は自動でリンクになるようですが、おそらくこれはクライアント側で処理されています)。
このfacetsというのがマークアップ的な情報となっています。
実行してみましょう。read/env/net権限が必要です。
❯ deno run --allow-read --allow-env --allow-net post.ts
@yui.bsky.social こんにちは! [
{
"$type": "app.bsky.richtext.facet",
index: { byteStart: 0, byteEnd: 16 },
features: [
{
"$type": "app.bsky.richtext.facet#mention",
did: "did:plc:xxxxx"
}
]
}
]
表示されたとおり、facetsは配列で、各要素はindex
とfeatures
を持ちます。
index
には適用される範囲を、features
が実際に適用されるマークアップの内容を設定します。
投稿できた
facetの型定義はこちらにあります。
リンクを書き換える
facetsを手動で設定することで、URL文字列と実際のリンク先を変えることができます。
ひとまず、文字列の最初から最後までを範囲として設定してみましょう。
// agent.loginまでは共通なので省略
const text = "https://twitter.com"
const facets = [
{
index: { byteStart: 0, byteEnd: text.length },
features: [
{
$type: "app.bsky.richtext.facet#link",
uri: "https://dic.nicovideo.jp/a/%E3%83%90%E3%83%BC%E3%83%9C%E3%83%B3%E3%83%8F%E3%82%A6%E3%82%B9",
},
],
},
]
// 手動でfacetsを設定したのでdetectFacetsは不要
await agent.post({
$type: "app.bsky.feed.post",
text,
facets,
});
(画像じゃわからないけど)twitterと見せかけてそうではないリンク
リンクを踏むとこのページへ飛びます。
リンクを書き換える:日本語に対応させる
facetsの適用範囲は、byteStart/byteEndの名の通り、バイトインデックスで指定する必要があります。マルチバイト文字(日本語や絵文字)の混じった文章ではtext.length
などが正しく動作しません。
何文字目がリンクになっているのやら
これを防ぐには(new TextEncoder()).encode(string).byteLength
を使う必要があります。
直接インデックスを触るのは煩雑になるので、markdown形式[text](url)
で書いた文字列から、表示される文字列とfacetsを取り出す関数を作りました。
渡された文字列をループして、markdownのリンク文字列を見つけたらfacetに取り出し、リンクがなくなるまで続けます。
// agent.loginまでは共通なので省略
const convert = (src: string) => {
const mdLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/;
const facets = [];
while (true) {
const links = src.match(mdLinkRegex);
if (!links) {
return { text: src, facets };
}
const [matched, anchor, uri] = links;
src = src.replace(matched, anchor);
const byteStart =
(new TextEncoder()).encode(src.substring(0, links.index)).byteLength;
const byteEnd = byteStart + (new TextEncoder()).encode(anchor).byteLength;
facets.push({
index: { byteStart, byteEnd },
features: [{ $type: "app.bsky.richtext.facet#link", uri }],
});
}
};
const {text, facets} = convert("[markdown](https://commonmark.org/)形式でリンクを書くテスト: [🥳](https://bsky.app/)")
await agent.post({
$type: "app.bsky.feed.post",
text,
facets,
});
実はemojiの方もリンクになってるよ
まとめ
参考
以下のリポジトリを参考にしました。
公式のドキュメントはこちら。
コードを書く上ではドキュメントだけではわからない定義などがあったため、atoprotoパッケージの実装を直接確認しました。
-
Twitterを示す符牒 ↩︎
Discussion