今やatprotoアプリケーションは数多ありますが、その中でもatprotoオタクとしてお気に入りのサービスにAT Profileがあります。1
かなりatproto風味の強いサービスなのですが、ドキュメントが全然無いので面白い部分が全然見えません。
というわけで、関係者でもなんでもないですが、実装から読み解いた機能と使い方を紹介します。
以下、atproto公式ガイドで説明されている程度のatproto知識を前提とします。
AT Profileとは
その名の通り、プロフィールページを作れるサービスです。
デフォルトではBluesky情報を使った素朴なものですが、ウェブアプリ開発で使われるVueフレームワークが利用できるため、自由度が非常に高いです。開発者のJoey氏はMySpace風プロフィールを作ったりしています。
また、atprotoデータを取り込みやすくなっているのも特徴です。Blueskyに限らず、全atprotoサービスで所有する公開データ(record)を簡単に参照できます。
データは元サービスから随時取得されており、例えばデフォルトで表示されるアイコンや紹介文はBlueskyで更新されたらAT Profileでも追従します。
基本的な使い方
AT Profileでは基本的にvueファイルをベタ書きします。乱暴な言い方をするなら少し特殊なHTMLファイルです。
これからAT Profile独自機能を含めたvueファイルの簡単な説明をしますが、正直Vueがあんまり分かってないので、話半分に聞いてください。
ログインして編集画面を開くと、style
, div
, script
の3要素があるのが見えます。プロフィールページでは、この内容が概ねそのままbody
にぶち込まれます。2
普通のHTMLと違うところとして、変数や制御構造(テンプレート構文)を使うことができます。
例えばデフォルトプロフィールの以下の部分。
<h1 class="title">
{{ profile.displayName || profile.handle}}
<template v-if="status.length">- {{status[0].value.status}}</template>
</h1>
二重波括弧で囲まれた部分はテキスト展開と呼ばれるもので、好きなJavaScript式を書くことができます。AT Profileの場合、profile
変数にアカウントデータが入っており、ここではdisplayName
があればそれに、なければhandle
に展開する式になっています。
v-if
属性付きのHTML要素は属性値の評価結果がfalsyであれば描写されません。ここではStatusphereデータstatus
が存在するならその内容を表示するという処理になっています。
ただの文字列にv-if
を適用するためにtemplate
タグを使っていますが、h1
やp
のような普通のHTMLタグにも同じことができます。
ここで使う変数はどこで定義されているかというと、script
タグ内にあります。
createApp({
setup() {
const cursor = useTemplateRef('cursor')
const cursorTop = ref(0)
const cursorLeft = ref(0)
const setPosition = (event) => {
const radius = cursor.value?.offsetHeight / 2
cursorTop.value = event.clientY - radius
cursorLeft.value = event.clientX - radius
}
onMounted(() => {
document.addEventListener('mousemove', setPosition)
})
onUnmounted(() => {
document.removeEventListener('mousemove', setPosition)
})
return {
...window.context,
cursorTop,
cursorLeft,
}
},
}).mount('#app')
このsetup
の返り値のプロパティが変数に使えると思ってください。profile
やstatus
が無いのが気になるかと思いますが、それらはwindow.context
に含まれています。詳細は後ほど。
setPosition
がやっているように、Vueでは値を動的に更新することもできます。細かいVueの使い方はいくらでも資料があると思うので各自調べてください。
atproto的な使い方
さて、ここからが本題です。profile
から一部のBluesky情報にアクセスできるのは分かりましたが、他には何をサポートしているのでしょう?
window.context
には、以下が含まれています。3
名前 | 内容 |
---|---|
profile | 作成者アカウントにgetProfile した結果 |
pds | 作成者PDSエンドポイント |
contextItems | 後述 |
newlinesToLinebreaks | HTML内の改行を表示時にも改行として扱うか(preserve-breaks ) |
publicAgent | public.api.bsky.appへのAgent |
agent | pds へのAgent |
getRecord | 作成者アカウントへのgetRecord |
getRecords | 作成者アカウントへのlistRecord |
atprotoApi | @atproto/apiパッケージへの参照 |
まあ実際に使うのはほぼprofile
だけかと思いますが、時々publicAgent
でgetProfile
したり、agent
でgetBlob
したりはできそうです。
ところで、察しの良い方はstatus
が無いことにお気づきかもしれません。
実はこれ、常にあるとは限りません。設定によってあったりなかったりするからです。4
編集画面の歯車マークを押すと設定が出てきます。
Context設定で、xyz.statusphere.status
コレクションから取得したrecordを最大1個の配列としてstatus
変数に入れるように指定されています。
同様に、app.bsky.feed.post
を指定すればBlueskyの自分の投稿が、com.whtwnd.entry
を指定すればWhiteWindの記事が取得できるというわけです。
最新50件しか取得できませんが、rkeyを指定すれば決まったrecordを引っ張ってくることもできます。さらに多くのrecordを取得したい場合はgetRecords
を呼ぶのもいいでしょう。
つまり、自分のrecordデータを挿入する程度であればスクリプトを全然書かなくてもうまいことやってくれるわけですね。ここが個人的に刺さったポイントです。
ちなみに、実はcontextItems
にはここで指定したContext設定がそのまま入っています。つまり、先ほどの設定画面で指定したcollectionやlimitのことです。
ついでにnewlinesToLinebreaks
が設定画面で指定できることもスクリーンショットから分かりますね。
もちろんこの設定やカスタマイズしたプロフィールページもrecordとしてPDSに保存されるので、その他のatprotoサービスからも参照できます。使い道があるかはともかく。
サンプル
具体的にどんなものが作れるか、少しだけ見てみましょう。サンプルなので見栄えは最低限です。
どんな表示になるか見たい方は山貂のプロフィールページを参照してください。
WhiteWind記事リンク
WhiteWindの記事はcom.whtwnd.blog.entry
に入っています。これで記事リンク一覧を作ってみましょう。
Context設定でcollectionを指定して、entries
という名前で5件取得するよう設定してみます。whtwnd
のrkeyはTIDなので、普通は最新5件が取れるはずです。
あとはentries
をVueから参照するだけ。簡単ですね。
<ul><li v-for="e in entries">
<a :href="`https://whtwnd.com/${profile.did}/${e.uri.slice(-13)}`">{{e.value.title}}</a>
</li></ul>
listRecords
のレスポンスがそのまま入っているので、配列の各要素はuri
, cid
, value
(record値)を持ちます。
whtwnd.comへのリンクはrecordには入っていないので自分で構成します。rkeyはat-uri末尾から切り出すことで取得。
recordには記事本文も丸ごと入っているので、直接表示することもできます。Markdownレンダリングをどうするかは考えないといけませんが。
likeされたBluesky投稿
続いて、recordに入ってない情報を使いたい場合も考えてみます。
WhiteWindなら記事についたコメントはrecordにはありませんし、Blueskyならフォロワー一覧、like数など。購読フィードのような非公開データは難しいですが、公開情報、特にBlueskyのものならpublicAgent
を使えば簡単です。
ここでは自分のBluesky投稿から、1件以上likeがついているものを最新3件表示してみます。
getAuthorFeed
でlikeカウント付きの投稿一覧を取得し、likeされているものを抽出します。
非同期処理もあるためscript
タグ内で処理を書く必要があります。また、HTML側で参照するためにlikedPosts
という変数を用意し、そこに結果を入れることにしました。
const likedPosts = ref("")
const fetchLiked = async () => {
const { data } = await context.publicAgent.app.bsky.feed.getAuthorFeed({
actor: context.profile.did,
})
likedPosts.value = data.feed.filter((p)=>!p.reason && p.post.likeCount > 0).slice(0,3)
}
actor
のアカウント指定はIDベタ書きもできますが、profile
から持ってくると再利用がききます。
スクリプト側ではwindow.context
のフィールドとしてアクセスする点にだけ注意してください。(例ではwindow
は省略していますが、対象は同じです)
追加したスクリプトを使うために、setup
の中でfetchLiked
を呼び出した上で、返り値にlikedPosts
を加えることでHTMLから参照できるようにします。
DOM触らないので即時fetchLiked
呼んでますが、Vueのお作法的にはonMounted(fetchLiked)
の方が安全なんでしょうか。
createApp({
setup() {
// 中略
fetchLiked()
return {
...window.context,
cursorTop,
cursorLeft,
likedPosts,
}
},
}).mount('#app')
ここまできたらあとはWhiteWindの例と同じです。シンプルに本文だけ出力してみました。
<ul><li v-for="p in likedPosts">
<q>{{p.post.record.text}}</q>
</li></ul>
publicAgent
だけでも用途はそれなりに広がると思います。getPosts
を使えば「likeした投稿一覧」とかも作れますね。
ただし、searchPosts
はDOS対策でpublicエンドポイントからは使えません。
他のエンドポイントを使いたい場合はatprotoApi
から新しいAgent
を作ったり単にfetch
したりできますが、認証はできないものと思ってください。
うっかりスクリプトにapp passwordを埋め込めば世界中に晒される羽目になります。
雑感
atprotoサービスにはBlueskyのプロフィール情報を引っ張ってくるものが多いですが、独自のプロフィールを持つもの(例: Tangled)もあります。
lexicon.communityでは過去にプロフィール情報を一つのcollectionにまとめようという提案もありました。
大体のサービスでは必要なものですが、そこにどんな情報を載せるかはサービスの性質によって様々です。
AT Profileのシンプルな使い方の一つは、様々なサービスのプロフィールrecordを並べて、サービス横断のプロフィールページを作ることです。更新は勝手に追従してくれますし、一緒に投稿を見せたりもできます。
テンプレート化されているので、一度誰かが作ってしまえばコピペするだけで共有もできるでしょう。
様々な形態のサービスにまたがった単一のアカウントを運用できるのはatprotoの代表的な特徴ですが、AT Profileはそれを活かした数少ないサービスだと思います。
PDSlsやmicrocosmのようなインフラではなく、サービスとしてこういうものが出てくるというのは琴線に触れるものがありました。
個人的には、こういう仕組みをモジュール化したような埋め込みビュー定義が、かつてatprotoに期待したものでした。Blueskyのプロトコル開発者達も似たような構想を時折口にするものの5、今でもそこを目指しているか定かではありません。
AT Profileのような試みが、そんな未来に繋がる道になることを願います。
Footnotes
-
同じくらい好きなサービスとして、atprotoの使い方が上手いTangledと、 atprotoサービスの先駆者であるWhiteWindがあります。あくまでオタク基準ですが。 ↩
-
body
といってもiframe内のbody
で、プロフィールページのヘッダ/フッタ(AT Profileロゴ等が表示されている部分)には干渉できません。iframe内はatprfl.comという別ドメインになっています。 ↩ -
なお、作成者がStatusphere使ってない場合でも、設定されている場合は
status
は空リストとして参照できます。 ↩ -
例えば、外部サービスの情報をカード形式で埋め込むというアイディアを検討していることが開発者の発言で示されています。2023年に日本で行われたBluesky meetupでも、おそらくこれに関連して、Bluesky Socialを様々なatprotoサービスへの導線にしたいという旨のコメントがありました。 ↩