しげぽん日記

2023/12の日記

2023/12/16 0:00:00

Blueskyで作ったおもちゃとその裏の仕組のお話

この記事は Bluesky Advent Calendar 2023 の 16日目 の記事です。Bluesky で私が作ったおもちゃとその裏の仕組みのお話をしたいと思います。

背景

私は @shigepon.net というアカウントで Bluesky5/10 より参加しています。

元々私はエンジニアですので Bluesky に来ても何か作れないかなと思っていたのですが、とりあえず私の自宅の炊飯器でご飯が炊けたら「ご飯が炊けたよ!」ってポストするだけの ボット だけ作って普通に SNS として利用していました。

ところが、カスタムフィードっちゅうもんが案外簡単に作れそうだっていうことを気付かせてもらったので、これはいっちょ作ったろか、っていうことでカスタムフィード職人の道を志す事になりました。

作ったおもちゃ

過去に作ったカスタムフィードはこんなのがあります。

  • サイコロ 1,2,3,4,5,6 という 6 つのポストの中からランダムに 1 つだけを選択して表示するだけのフィード
  • 奈良 奈良の地名、駅名、特産品、観光地をキーワードに含んでそうなポストを Bluesky の初期の頃まで遡ってできるだけ表示しようと頑張るフィード。表示対象になるフィードは私がサーバをたててから langs に ja を入れて発言した方のポストのみですので、ja が入ってない方や最近発言されてない方は対象外です。また、奈良の地名と同名、あるいは、一部に含むようなキーワードを含んでいてもそのポストもフィードに含めてしまいますが、これは仕様です。奈良県っぽく見えるポストは全部可能性として表示して、見る人に奈良かどうかを判断してもらう想定で作っています。そういう仕様にしたのは、厳密に奈良か否かをポストの内容だけで判断するのが難しかったのと、おもしろかったからです。
  • ランダム 奈良同様に langs に ja を入れて発言された方を対象に、自身の過去のポストの中からランダムに 10 コ選択して表示するフィードです。自分のポストしか対象にならないので、他の人にはその内容が見えませんし、見る度に表示されるポストが変わるので、刹那的に楽しむものになっています。
  • ゲームブック ゲームブックのシナリオを作成するとそのゲームブックを遊ぶためのカスタムフィードを公開できるようにするための仕組です。私が用意するのはその仕組だけでカスタムフィードはシナリオを作成された方のアカウントで公開されるようになっています。これまでに作成されたゲームブックは以下になっています。
    • gamebooktest001 @so-asano.com さんの作品。いわゆるゲームブックっぽいテイストの短編シナリオ。
    • pool of bluesky @ubanis.com さんの作品。やってみればわかると思います。
    • ジオマンシー占い @msonrm.bsky.social さんの作品。ジオマンシーという占いをカスタムフィード上で再現した作品。画像添付も利用しもらって占いの世界を表現されています。
    • blue_love @so-asano.com さんの作品。「ギャルゲー第 1 弾です!」とのことです。選択肢も複数ありランダムも利用されて狙ったエンディングまですすめるにはそこそこ歯応えがあります。
    • Bluesky ちゃん と謎解き空中散歩 @kawaiirailroads.millie-may.net さんの作品。無料で公開して良いのか?と思うくらいに綿密に作り込まれた謎解きゲーム。ゲームブックの可能性を最大限に活用した力作です。

とまぁこんな感じです。

動かしているサーバ

動かしているサーバがどうなってるのかと言うと、カスタムフィードを提供するフィードジェネレータを自宅サーバ上で動作させています。なのでカスタムフィードに何を出すのかというアルゴリズムを自分で書いたプログラム次第で好き勝手表示させている(SkyFeed のような決められた仕組の中ではなく AtProto の限界まで自由にできる)のです。

また、奈良とかランダムについてはElasticsearchを動かしてそこにインデックスを作成しています。ここに入れるポストを格納する方法が以下の基準になっています。

  1. Firehoseに流れてきたポストのうち langs に ja が含まれるもの
  2. 1.の条件に合致したポストのうち過去1日間以内に発言したアカウントの全ポストの取り込みを行っていない場合に全ポストの取り込み

なのでlangsにjaを入れて発言した人のポストからその人の最初のポストまでがElasticsearchに入って、ランダムや奈良で参照される事になります。langsにjaを入れているけれども日本語ではないポストも中には存在しますが数が少ないので放置しています。

最近起こった変化

最近、Blueskyのサーバの挙動が変わったようで、以前はカスタムフィードにアクセスがある度にその回数だけフィードジェネレータにアクセスがあったんですけど、最近はカスタムフィードを表示しようとしても勝手にサーバでキャッシュした内容を返却するようになっていて、ちょっとやそっとどうやったところで以前のようなテンポの良いカスタムフィードの挙動にならないのです。まぁ正式サービスじゃないので仕方のない事とはいえ、しょんぼりしているのは事実です。とりあえず、過去に選択したのと同じ分岐をたどると、以前に分岐した時に表示したタイムラインを表示する挙動になってしまったので、カスタムフィードを使ったゲームブックを遊ぶ際には、遊ぶ側である程度慮った操作をする必要が出てきます。ごめんなさい。私が悪いんじゃないって言いたいですけど。

  1. とりあえずフィードを表示して目的とする分岐リンクを踏んでみる
  2. 1.でうまくその先のストーリーが表示されたら大丈夫、また今表示されたポストを見て1.にトライする
  3. 1.でうまくその先のストーリーが表示されずに巻き戻った場合は、今表示されている選択肢は見なかったことにして黙ってリロード(ブラウザのリロードボタン、アプリなら下スワイプ)すると、1.で選択した時に本来表示されるべきポストが見えるはずなので、1.にまた戻ってその先のストーリーを楽しむ

この時、3.の症状になった時に1回しかリロードしないようにしないと、連続して同じ選択肢を何度も選択したことになってしまい、ストーリーが知らないところでずんずんすすんでしまうので、ていねいに操作しないといけないのです。まぁ仕様変更かかるだろうと思いながら公開したので仕方ないですが、このシステムの上でシナリオ作成をしていただいた方には申し訳ないと思っています。

書いているプログラム

さて、さらに仕組の奥の方の話にすすめていきます。技術的な色合いが濃くなっていくのですが、エンジニア枠なので技術の話にも触れていきます。

これらのプログラムは実は Rust で書かれています。Rust 上で AtProto を扱うライブラリは既に他の方が用意されているものもあったのですが、Firehose を使ったライブラリがその時点ではなさそうだった(調査不足の可能性あり)のと、AtProto をたたくことそのものはそんなに難しくなさそうなのと、Lexicon からコードを起こすソフトウェアを書いてみたかったのとで、結局自前で用意してしまいました。この自前で用意したライブラリは、@kojira.io さんに唆されてうっかり crates.io で公開してしまいました。aerostream という形で公開していますし、ソースコードも GitHubshigepon7/aerostream として公開しています。まぁロクにテストコードも書いていないので、自分でツール作りながらへこへこデバッグしているような状況ですので、もし不具合見つけたら GitHub に Issue あげてもらえるとうれしいです。いちおうこのライブラリにはコマンドラインプログラムとしてもビルドできるようになっていって、ログインせずにただ条件に合致したポストをだらだら眺めるだけのアプリとして利用する事もできるようになっています。こういうような道具立てで、Rust のプログラムをコンテナイメージにビルドしています。もっぱらコンテナのベースは ArchLinux を使っています。Rust も最新のライブラリ追いかけないとけっこう動作環境シビアで、ArchLinux のような最新を追いかけるようなディストリビューションを使っておかないとついていけないので。

カスタムフィードを実際に作成する方法

いちおう簡単にですが aerostream を使ってカスタムフィードを自作する方法を説明しておきます。

https://github.com/shigepon7/aerostream/blob/main/examples/feed_generator.rs

ここに過去に作成した「美味」のカスタムフィードのソースコードがあるのでこれを見ながら簡単に解説します。基本的にやることは以下の流れになります。

  1. Algorithm traitを実装する
  2. FeedGenerator のインスタンスを作成する
  3. FeedGenerator の Record を作成する
  4. FeedGenerator のインスタンスに 1. で作成した Algorithm のインスタンスを add_algorithm する
  5. FeedGenerator のインスタンスを start する

美味フィードの場合はこれに加えて Firehose でポストの情報を検索できるようにするために Subscription trait の実装 も行っています。Subscription も Algorithm 同様に FeedGenerator に対して set_subscription することで、Firehose からのイベントを受けられるようになります。

実際の挙動としては、カスタムフィードにアクセスがあれば、Algorithm の handler() が呼ばれ、既に蓄積されているカスタムフィードとして表示すべきポストの AtUri の一覧を返します。cursor や limit の処理は FeedPosts にポストの内容を登録しておくことで自動で処理してくれるようになっています。「美味」カスタムフィードの場合は、このポストの一覧は Subscription で受け取ったポストの一覧から必要なものを餞別して FeedPosts に格納するようになっています。一方、カスタムフィードへのアクセスとは別に、 Firehose からは常時イベントが流れ続けていて、こちらは Subscription の handler() でそれを読み出すことができます。一度に送られてきたイベントの中から Post の create と delete だけを選別して、text に「美味」を含むポストの create であれば FeedPosts に追加、delete であれば FeedPosts より削除するという形でカスタムフィードとして表示すべき情報を蓄積していきます。この2つの動作の組み合わせでカスタムフィードを実現しています。FeedGenerator は http を直接ハンドリングでき、FeedGenerator の start() を実行すると 8000番のポートを listen しますので、インターネットからの http アクセスをこのポートに接続すれば、実際にカスタムフィードとして機能するようになります。

これらのプログラムを動作させる物理サーバの構成

ちょっと話を切り換えて自宅サーバの話をするのですが、PC を 7 台ほど組み合わせて Kubernetes のクラスタを組んでいます。これを Cloudflare をリバースプロキシにして公開しています。自宅は固定 IP ではなく普通のフレッツの契約ですので、Cloudflare を DDNS 代わりに利用させてもらっています。WAN 側の IP を調べて Cloudflare に登録している IP から変わっていたら Cloudflare をたたいて DNS のエントリを変えてもらうようにしています。ただ、うちの家は外からの入口のルータに NVR510 を使っていて、ここに 小型 ONU を直接挿す形にしているので、ひかり電話の SIP サーバも NVR510 が担当している形になっています。世の中に SIP サーバ公開すると、アホみたいに攻撃パケットとんできていて、どうも 1 日に 1 回くらい再起動かけてあげないと電話の着信もできなくなってたようなので(詳しい原因の調査までは面倒でやってない)、毎日 AM3 時になると NVR510 を再起動かけるようにしています。ルータ再起動かけると WAN 側 IP が変わるので AM3 時前後はもしかするとカスタムフィードが見えないかもしれません。このへんは自宅サーバの方が楽しいから、という理由でクラウドを使わない私の個人的な嗜好の問題ですので、動いてなかったらごめんなさい。クラウド借りる方が電気代より安いんじゃないかとか思ったりする時もありますが気付かなかったことにします。自宅はエネファームと太陽光発電とどっちもついてる上に蓄電池までついているので停電には強いと信じています。これらのエネルギー機器もほぼ趣味で自宅に導入しちゃいました。

話を脱線させてスマートホームシステムの話

ついでに言うと、ご飯が炊けたよボットは、元々自宅のスマートホームシステム(これも Rust で書いてあって、自分で EchonetLite をしゃべって家電の状況を知ったり制御したり、ネットの情報をスクレイピングで収集して自宅に雨降ってるのか教えてくれたり、JR が遅れてるのか教えてくれたり、川が増水してないか教えてくれたり、監視カメラで動体検出すると教えてくれたりするもの)を動かしていたところに、Bluesky に投げても良さそうな、ご飯が炊けた、という情報だけポストさせるようにしたものになります。スマートホームシステムの本体は UI には Slack を利用していて、家庭内で発生したあらゆるイベントが監視カメラ画像をアニメ GIF にしたものも含めて時系列に並ぶアプリになっています。家の鍵を持たずに出かけた子供が家の鍵を開けて欲しい時には「監視カメラの前で踊れ」という指示を出しているので、時々うちの子供は監視カメラの前で踊ります。そうすると Slack に電子錠の解錠コマンドを遠隔で入力して鍵を開ける仕組みになっています。踊り認証システムです。でも最近、子供も学習したので踊らずに LINE で「鍵開けて」って送ってくるようになりました。その方が確実ですしね。

まとめ

話が脱線しましたので、このへんでそろそろ〆めにかかろうかと思います。こういう家に帰っても技術どっぷり浸かってるような変人が運営するカスタムフィードですので、ちゃんと動いてなくてもブチ切れたりせずに、生暖かい目で見守ってやってください。もし良ければ、動いてないよって優しい口調で教えていただけると泣いて喜ぶと思います。これからもお付き合いよろしくお願いします。