🌗

HALFの裏側 ― 半閉鎖的公開ブログが1週間でできるまで

2021/12/02に公開

先日,ブログを作りました。HALFといいます。
https://half.de-liker.com/
一般的なブログとはちょっと違って, 私がTwitterでフォローしてる人のみが閲覧できるブログ です。(作るに至った経緯とかはHALF内で書いてるので,読みたい人がいたらTwitterでフォローするとフォローバックするかもしれません,技術のことはこの記事以上には書いてないです。)

この記事では,HALFを作っている技術を書いていこうかなと思っています。

半閉鎖的公開に必要なもの

いわゆるフォローしてる人じゃないときは絶対に弾く,みたいなガチガチなのはいらなくて,そういうものをもとめてこのブログに来た人はちょっと違うかもしれないです。
今回の目標は, 「microCMSの記事全体が取れるAPIをFirebaseで制限付きで取得させる」 ことです。
ガチガチにするにはNext.js等でSSRにしてmicroCMSとの連携をさせる必要があります。あるいはフロントだけで完結させるにはFirestoreに記事データを入れる必要があります。今回はそういうガチガチじゃなくて,もう少しゆるい感じの,いうなれば「リバースエンジニアリングしたら簡単に誰でも記事が読めちゃうようになるけどフォローしてる人にはそんな人いないし,やられてもそんなに被害はないのでまぁいいや」程度の仕組みです。

1. フォローしてる人をFirestoreに格納する

まずはTwitterでAPIの登録をして,keyを手に入れます。
https://developer.twitter.com/en
ここで取れます。今回は最近話題のv2ではなく,v1で取得しています。必要なことはv1で満たせたのと,発行できるAPI数が多かったからです。
やりかたはぐぐってもらえれば阿鼻叫喚含めいろいろ見られるのでやってみてください!

私は次のようなbatchをGoで書いてFirestoreに格納しています。

集計のところで,「フォローしてるけどFirestoreにはない人」と「Firestoreにはあるけどフォローしてない人」を計算します。その結果を,前者はputして,後者はdeleteすることで整合性を保つようにしています。

keyはTwitterのIDです。IDというのは私で言う CreatorQsF (screen_name)ではなく,Twitterで割り振られているIDです。
このへんで調べられます。
https://tweeterid.com/

並列処理したりして(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 とかのほうは手に入らないのでご注意を。

この辺の詳しいことはここに書いてあります。
https://firebase.google.com/docs/rules/rules-and-auth?hl=ja#identify_users

あと私が調べても出てこなかったのでここに書くんですが,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ができたんですよ!
https://www.npmjs.com/package/microcms-js-sdk

とはいえ自分はkeyを用意してからclientを作る方式と今回はちょっと仲が悪かったので,自前のhooksで組んでます。ややこいqueryを書く時が来たら移行させてもらいます。

4. React Query

今回はAPIの呼び出しにReact Queryを使ってみました。cacheの保存期間その他に Infinite が指定できるので,そうすると一度取得すればページを再読込しない限りリクエストが発生しないのでお得です。
今回ちょっと引っかかったのが,API KeyをFirestoreからgetして,それを利用してmicroCMSにリクエストを出す,だったので,hooksを素直に組むとAPI Keyがundefinedになることがあるんですが,useQueryの呼び出し順を変えるわけにはいかないためどうしようかな…と思っていたところ,

https://react-query.tanstack.com/reference/useQuery#:~:text=throw an error.-,enabled%3A boolean,-Set this to

enabled というoptionをReact Queryはとれて,これがtrueのときだけrequestを投げることができるらしくこれを使いました。
ということは,useQuery で走るfetchの中ではAPI Keyは常にtruthyだよな,と思って non-null assertionをしてたのですが,

https://github.com/tannerlinsley/react-query/discussions/2281

どうも refresh 関数を呼ばれると enabled によらず実行されることがあるそうで,optionalのままfetchの中で例外を投げるほうがいいらしいです。

余談ですがこの話をTwitterに書いたら速攻書いた方からreply飛んできてすごかったです。

その他

あとは普通です。私はNext.jsがあまり好きではないのでNetlifyホスティングでreact-router-dom v6を使ってます。

デザインについては自分のデザインシステムからちょいちょいつまんできました。

なかなかキレイめにできて触ってて楽しいです。やっぱりものを作るのはいいですね。

おわり

Discussion