AT Profile勝手チュートリアル

@yamarten.bsky.social

今やatprotoアプリケーションは数多ありますが、その中でもatprotoオタクとしてお気に入りのサービスにAT Profileがあります。1

かなりatproto風味の強いサービスなのですが、ドキュメントが全然無いので面白い部分が全然見えません。
というわけで、関係者でもなんでもないですが、実装から読み解いた機能と使い方を紹介します。

以下、atproto公式ガイドで説明されている程度のatproto知識を前提とします。

AT Profileとは

その名の通り、プロフィールページを作れるサービスです。
デフォルトではBluesky情報を使った素朴なものですが、ウェブアプリ開発で使われるVueフレームワークが利用できるため、自由度が非常に高いです。開発者のJoey氏はMySpace風プロフィールを作ったりしています。

AT Profileによるプロフィールページ例

また、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タグを使っていますが、h1pのような普通の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の返り値のプロパティが変数に使えると思ってください。profilestatusが無いのが気になるかと思いますが、それらはwindow.contextに含まれています。詳細は後ほど。

setPositionがやっているように、Vueでは値を動的に更新することもできます。細かいVueの使い方はいくらでも資料があると思うので各自調べてください。

atproto的な使い方

さて、ここからが本題です。profileから一部のBluesky情報にアクセスできるのは分かりましたが、他には何をサポートしているのでしょう?

window.contextには、以下が含まれています。3

名前内容
profile作成者アカウントにgetProfileした結果
pds作成者PDSエンドポイント
contextItems後述
newlinesToLinebreaksHTML内の改行を表示時にも改行として扱うか(preserve-breaks)
publicAgentpublic.api.bsky.appへのAgent
agentpdsへのAgent
getRecord作成者アカウントへのgetRecord
getRecords作成者アカウントへのlistRecord
atprotoApi@atproto/apiパッケージへの参照

まあ実際に使うのはほぼprofileだけかと思いますが、時々publicAgentgetProfileしたり、agentgetBlobしたりはできそうです。

ところで、察しの良い方は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はそれを活かした数少ないサービスだと思います。
PDSlsmicrocosmのようなインフラではなく、サービスとしてこういうものが出てくるというのは琴線に触れるものがありました。

個人的には、こういう仕組みをモジュール化したような埋め込みビュー定義が、かつてatprotoに期待したものでした。Blueskyのプロトコル開発者達も似たような構想を時折口にするものの5、今でもそこを目指しているか定かではありません。
AT Profileのような試みが、そんな未来に繋がる道になることを願います。

Footnotes

  1. 同じくらい好きなサービスとして、atprotoの使い方が上手いTangledと、 atprotoサービスの先駆者であるWhiteWindがあります。あくまでオタク基準ですが。

  2. bodyといってもiframe内のbodyで、プロフィールページのヘッダ/フッタ(AT Profileロゴ等が表示されている部分)には干渉できません。iframe内はatprfl.comという別ドメインになっています。

  3. コードから読み取っただけなので、仕様として安定はしていないかもしれません。

  4. なお、作成者がStatusphere使ってない場合でも、設定されている場合はstatusは空リストとして参照できます。

  5. 例えば、外部サービスの情報をカード形式で埋め込むというアイディアを検討していることが開発者の発言で示されています。2023年に日本で行われたBluesky meetupでも、おそらくこれに関連して、Bluesky Socialを様々なatprotoサービスへの導線にしたいという旨のコメントがありました。

yamarten.bsky.social
山貂

@yamarten.bsky.social

一般atprotoオタク
投稿中の「#1234」のような数字は原則的にGitHub上のatprotoリポジトリのものを指す

atproto関連の資料等はlinkat参照
https://linkat.blue/yamarten.bsky.social

Post reaction in Bluesky

*To be shown as a reaction, include article link in the post or add link card

Reactions from everyone (0)