HALFの裏側 ― 半閉鎖的公開ブログが1週間でできるまで
先日,ブログを作りました。HALFといいます。
一般的なブログとはちょっと違って, 私がTwitterでフォローしてる人のみが閲覧できるブログ です。(作るに至った経緯とかはHALF内で書いてるので,読みたい人がいたらTwitterでフォローするとフォローバックするかもしれません,技術のことはこの記事以上には書いてないです。)この記事では,HALFを作っている技術を書いていこうかなと思っています。
半閉鎖的公開に必要なもの
いわゆるフォローしてる人じゃないときは絶対に弾く,みたいなガチガチなのはいらなくて,そういうものをもとめてこのブログに来た人はちょっと違うかもしれないです。
今回の目標は, 「microCMSの記事全体が取れるAPIをFirebaseで制限付きで取得させる」 ことです。
ガチガチにするにはNext.js等でSSRにしてmicroCMSとの連携をさせる必要があります。あるいはフロントだけで完結させるにはFirestoreに記事データを入れる必要があります。今回はそういうガチガチじゃなくて,もう少しゆるい感じの,いうなれば「リバースエンジニアリングしたら簡単に誰でも記事が読めちゃうようになるけどフォローしてる人にはそんな人いないし,やられてもそんなに被害はないのでまぁいいや」程度の仕組みです。
1. フォローしてる人をFirestoreに格納する
まずはTwitterでAPIの登録をして,keyを手に入れます。
やりかたはぐぐってもらえれば阿鼻叫喚含めいろいろ見られるのでやってみてください!
私は次のようなbatchをGoで書いてFirestoreに格納しています。
集計のところで,「フォローしてるけどFirestoreにはない人」と「Firestoreにはあるけどフォローしてない人」を計算します。その結果を,前者はputして,後者はdeleteすることで整合性を保つようにしています。
keyはTwitterのIDです。IDというのは私で言う CreatorQsF (screen_name
)ではなく,Twitterで割り振られているIDです。
このへんで調べられます。
並列処理したりして(errgroupとcontextの組み合わせまじ便利…),500フォローくらいだと遅くともdepsのresolve含め30秒以内くらいにはすべての処理を終われるようにしてあるので,GitHub Actionsのscheduleを使ってbuildからrunまでを1日1回走らせるようにしています。
本当はbuildしたものをDockerにしたりするといいんでしょうけど,dockerのpullとgo getのどっちが速いのか考えるとちょっと微妙な気がしたのでこういう設計になってます (GHCRでbinary置けると嬉しいんだけどなぁ)。
いちばんいいのはGitHub Releasesで自動でreleaseしたバイナリを落としてきて走らせるのがいいんでしょうけど,private repoなのでできるかわからずこういう突貫工事感のある設計になってます。
あと,GitHub Actionsをこういう用途に使っていいかが若干微妙だったんですが,利用規約を読む限りでは問題なさそうだったのでこの実装にしてあります。
今後GAEに移行するとか,Cloud SchedulerとCloud Runとか,build含めなければもっと短いのでそういうのもありですね (Schedulerはお財布との相談ですが)
ちなみになんですが,FirestoreはThreadsafeっぽい?のでこうしてますが,実際のところはソースをきちんと読んでないのでわかりません。
2. Firebase AuthenticationとFirestore Security Rules
ここからの情報があまりインターネットにはなかったので今回書いていきたいところだったんですが,結論から言うと,上記の仕組みで許可したいユーザーのTwitterのIDをkeyにしておくと簡単に完成します。
みなさん御存知の通り,Firestore Security Rulesでは get
を使ってFirestoreからの情報を取得できます (このとき取得するのも読み出し回数に含まれるのでご注意を)
get(/databases/$(database)/documents/PATH_TO_TWITTER_USER_LISTS/$(request.auth.token.firebase.identities["twitter.com"][0])).data != null
こんなかんじのruleを書くと,たとえば私のTwitterのIDは 984521858
なので,request.auth.token.firebase.identities["twitter.com"][0])
は 984521858
になります。あとはわかってもらえると思います。
request.aith.token.firebase.identities
には認証しているサービスのIDが入ります。ただし screen_name
とかのほうは手に入らないのでご注意を。
この辺の詳しいことはここに書いてあります。
あと私が調べても出てこなかったのでここに書くんですが,Rulesでは,
get(/databases/(database)/documents/users/$(request.auth.uid)).data.admin) == true
のような書き方でもいいですし (コードはhttps://firebase.google.com/docs/rules/rules-and-auth?hl=ja より抜粋,下記は一部改変)
get(/databases/(database)/documents/users/$(request.auth.uid)).data) != null
のように != null
で書いてもいいようです。ちゃんと意図した動き(フィールドが存在しなければnullになる)っぽいです。
3. FirestoreとmicroCMS
正直山場は越えてしまったのであとはもう書くことそんなに無いのですが,私は上記のRulesで読み出しに制限をかけているドキュメントにmicroCMSのAPIキーを保存しています。
そしたらもう記事を取得するだけですよね。ほんとmicroCMSさんにはお世話になってます……。
あ,最近?SDKができたんですよ!
とはいえ自分はkeyを用意してからclientを作る方式と今回はちょっと仲が悪かったので,自前のhooksで組んでます。ややこいqueryを書く時が来たら移行させてもらいます。
4. React Query
今回はAPIの呼び出しにReact Queryを使ってみました。cacheの保存期間その他に Infinite
が指定できるので,そうすると一度取得すればページを再読込しない限りリクエストが発生しないのでお得です。
今回ちょっと引っかかったのが,API KeyをFirestoreからgetして,それを利用してmicroCMSにリクエストを出す,だったので,hooksを素直に組むとAPI Keyがundefinedになることがあるんですが,useQuery
の呼び出し順を変えるわけにはいかないためどうしようかな…と思っていたところ,
enabled
というoptionをReact Queryはとれて,これがtrueのときだけrequestを投げることができるらしくこれを使いました。
ということは,useQuery
で走るfetchの中ではAPI Keyは常にtruthyだよな,と思って non-null assertionをしてたのですが,
どうも refresh
関数を呼ばれると enabled
によらず実行されることがあるそうで,optionalのままfetchの中で例外を投げるほうがいいらしいです。
余談ですがこの話をTwitterに書いたら速攻書いた方からreply飛んできてすごかったです。
その他
あとは普通です。私はNext.jsがあまり好きではないのでNetlifyホスティングでreact-router-dom v6を使ってます。
デザインについては自分のデザインシステムからちょいちょいつまんできました。
なかなかキレイめにできて触ってて楽しいです。やっぱりものを作るのはいいですね。
おわり
Discussion