第 2 回はきっとある
- 環境
- 概要
- 環境作成
- twitch の API まわりの仕様
- Webhook を受けるために ngrok を使う
- React の router 周り
- github actions で各プラットフォーム用にビルド
- todo
- 雑多
概要
Twitch の配信開始を通知するネイティブアプリケーションを作り始めました。
単純に rust を学びたかったのと、やる気減退期をやっと脱しそうだったので久々に趣味でアプリ書きたかったというのが簡単な動機です。
以前作ってたなんとか生放送の通知アプリみたいな感じ。
予め配信開始の監視先リストに目標となる配信者を追加しておいて、後は勝手に配信開始を待機して通知するものです。
やっとベースとなる機能の実装が一段落したので一旦まとめます。
環境作成
tauri-app のインストール
React + TypeScript を選択しています。
$ npm create tauri-app@latest $ npm install
npm run tauri dev するまで
WSLg を使えるならそれがいいと思うのですが、自分の環境ではどうにも使えなかったので、明確に無効にしてあります。
// %userprofile%\.wslconfig [wsl2] memory=4GB swap=0 guiApplications=false
VcXsrv をインストールし、起動時に設定を行います。
- Extra settings > Disable access control にチェック
エラー対応等のため、以下を ~/.bashrc に追加します。
# (WebKitWebProcess:13130): Gdk-ERROR **: 01:37:54.674: The program 'WebKitWebProcess' received an X Window System error. export DISPLAY=$(ip route | grep 'default via' | grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'):0 export WEBKIT_DISABLE_COMPOSITING_MODE=1
これで npm run tauri dev を実行すればビルドされて GUI が表示するはずですが、
もうちょっと何かしら設定を行う必要があったかもしれない
twitch の API まわりの仕様
アクセストークン
放送開始の通知を受けるためにはクライアント ID とアクセストークンが必要になります。
クライアント ID は Twitch Developers のコンソールから開発中のアプリを登録することで取得できます。
アクセストークンには user access token と app access token の 2 種類あります。
それぞれ取得の方法も利用の仕方も異なりますが、大体は以下の公式 docs に書いてあります。
Authentication | Twitch Developers
Twitch の API はエンドポイントに応じてどの種類のアクセストークンが必要かが変わりまして、
大雑把に言えば user access token は特定の Twitch ユーザを認証してトークンを得るため、"投票の開始"や"ユーザの BAN"といった Twitch 上での操作を行うのに必要で、
app access token はアプリ自体の認証だけでいいため、情報の取得全般に向いています。
EventSub
API に対して HTTP リクエストを飛ばすだけならもちろん Pull 型ですが、
放送開始の通知は Push 型となるため、アプリ側で Twitch からの通信を待ち受ける仕組みが必要です。
Twitch では WebSocket と Webhook の 2 つが用意されています。
基本的に、WebSocket の場合は user access token が、Webhook の場合は app access token が必要になります。
しかし、上記の通信路を用意したところで誰が放送開始しようが通知は飛んできません。
「誰が配信したら通知してくれ」と Twitch にお願いする必要があります。
上記の配信開始や配信終了、raid やチャンネルフォロー等のイベントを Twitch では EventSub と呼称します。
通知を飛ばすようにお願いするため、EventSub を購読(Subscription)しなければいけません。
ここで問題となるのが EventSub の購読コストです。
どの EventSub も購読時にコストというものが支払われまして、あまりに大量の EventSub を購読するとサーバに負荷がかかる等の諸問題が発生するためか、
支払えるコストには限界があります。
さて、簡単に言えば WebSocket(user access token) では放送開始の EventSub のコストが 10 までしか払えません。
放送開始の対象とするユーザがアプリを認証してくれればコストが 0 になるようですが、それは非現実的です。
逆に Webhook(app access token) ではコストが 10000 も払えるため、放送終了 EventSub も合わせれば 5000 件のユーザを監視できることになります。
最初は実装が楽なことから WebSocket の方を進めていましたが、
上記のコスト事情から Webhook を選択せざるを得なくなりました。
Webhook の HMAC 検証
セキュリティのため、Webhook によるリクエストに twitch-eventsub-message-signature ヘッダが付与されており、内容は HMAC-SHA256 となっています。
堅牢なアプリを目指すため、リクエストを受け取るたびに内容が正しいかどうか検証する必要があります。
正直なところ公式 docs の js の記述を rust で書き直すだけで大丈夫です。
Handling Webhook Events | Twitch Developers
HMAC 鍵は指示されている通り、ascii 文字を 64 文字とし、EventSub の購読時に API を通して送信します。
これは検証の時に必要なのでアプリで持っておくとし、基本的にランダム生成としています。
暗号化するメッセージですが、twitch-eventsub-message-id ヘッダと twitch-eventsub-message-timestamp ヘッダと raw の body を連結したものになります。
これは普通に文字列連結で構いません。
そしてダイジェストを得る際のエンコーダですが、公式 docs で .digest('hex') をコールしているのに気づかずしばらく時間がかかってしまいました。
rust なら data_encoding::HEXLOWER で構いません。
まだ汚いのであまり晒したくはないのですが以下のような感じです。
use data_encoding::HEXLOWER; use hmac::{Hmac, Mac}; use sha2::Sha256; // id + timestamp + 生の body の順に連結し、HMAC-SHA256 ダイジェスト を作成する let message = header.get("twitch-eventsub-message-id").unwrap().to_str(). unwrap().to_string() + header.get("twitch-eventsub-message-timestamp").unwrap().to_str().unwrap() + &body_raw; type HmacSha256 = Hmac<Sha256>; let mut mac = HmacSha256::new_from_slice(&secret.as_bytes()).unwrap(); mac.update(&message.as_bytes()); let digest = HEXLOWER.encode(&mac.finalize().into_bytes());
ちなみに、もしダイジェストが twitch-eventsub-message-signature から得られるものと異なる場合、4xx エラーを返すように指示されています。
Twitch CLI
開発用に EventSub を任意のタイミングで発火するためのツールが用意されていました。
ubuntu であれば基本は homebrew 経由のみでしかインストールできません。
アプリの secret は Twitch Developers のコンソール上から取得します。
# インストールスクリプトの実行 $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # 以下の指示に従う ==> Next steps: - Run these two commands in your terminal to add Homebrew to your PATH: (echo; echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"') >> /home/{ユーザ名}/.bashrc eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" # Twitch CLI のインストール $ brew install twitchdev/twitch/twitch-cli # アプリの id/secret を設定する $ twitch configure
ここまで設定すれば EventSub の発行等のテストが行えます。
実際に配信が始まりそうな人を登録して notification イベント待つとかやってたらめちゃくちゃ開発効率悪いので、絶対に当該ツールの使用をオススメします。
# HMAC challenge のテスト $ twitch event verify-subscription streamup -F {コールバック URL} -s {HMAC の secret} # 放送開始 EventSub の発火テスト $ twitch event trigger streamup -F {コールバック URL} -s {HMAC の secret} --to-user {ユーザ ID}
テスト項目の確認はみんな大好き公式 docs で!
Test webhook events | Twitch Developers
Webhook を受けるために ngrok を使う
ngrok はローカルのネットワークサービスを外部公開するためのサービスです。
2023 年の 7 月前後に free 版でも静的サブドメインが取得できるようになっており、かなり開発が楽になりました。
ngrok のトンネルを構築し、axum router をグリーンスレッドで稼働することで外部からの Webhook 通信を受けるようにしています。
(エラーハンドリングをまだしてないので書き直します)
(全然セキュアじゃないのでいろいろ考えます)
// Cargo.toml ngrok = { version = "0.13.1", features= ["axum"] } // トンネル構築処理 use ngrok::{config::TunnelBuilder, tunnel::UrlTunnel}; let session = ngrok::Session::builder( .authtoken(ngrok_authtoken) .connect() .await; if let Err(e) = session { println!("{:?}", e.to_string()); return Err(format!("session creating: {}", e.to_string())); }; let tunnel_wrapped = session.unwrap() .http_endpoint() .domain(ngrok_domain) .listen() .await; if let Err(e) = tunnel_wrapped { println!("{:?}", e.to_string()); return Err(format!("tunnel listening: {}", e.to_string())); }
少しでも隠蔽できるようにコールバック URL は毎回ランダムなパスにしています。
React の router 周り
tauri や electron で react-router を使用する時、
基本的には 1 画面上で操作を行う UI となるため、router としては HashRouter を選択するのが丸いです。
/#{path} に対して GET リクエストをするようになります。
全てのリクエストが一旦 / に飛び、パスはハッシュで扱います。
createHashRouter v6.20.1 | React Router
createHashRouter() に渡す配列はそれぞれリクエストした #{path} に応じて呼び出したい element を設定していくことになります。
上記 URL では親(/) に対し、子(#team)を設定しています。
つまり、/#team に GET リクエストをした際、まず Root モジュールが取得され、子モジュールとして Team が取得される流れになります。
今回のアプリでは左側にメニューを設置し、選択した内容に応じて右側の内容を変化させていますが、そういった場合に役立つのが Outlet モジュールです。
現在のリクエスト先パスから判断し子モジュールとして取得されたものが Outlet モジュールとして利用できるイメージです。
私のアプリとは内容が若干異なりますが、以下のようにすれば固定で表示したい左側メニュー(Navigation)と、パスに応じて取得先を変えたい右側(Outlet)が定義できます。
const Root= () => { return ( <div className='outer'> <Navigation /> <div className='container'> <Outlet /> </div> </div> ) }
github actions で各プラットフォーム用にビルド
これ本当に最高でたまらん
- tauri.conf.json の identifier を任意の被らないものにします
- github のリポジトリ設定を変更
- Settings > Actions > General > Workflow permissions > Read and write permissions
- workflow を作成: .github/workflows/build.yaml
- https://github.com/tauri-apps/tauri-action の Uploading the artifacts to a release を参照
- 以下のようにすれば手動で実行できる: github の Actions から Run Workflow を手動実行すればいい
- on: workflow_dispatch
なんと簡単な設定を最初にしておくだけで win, macos, ubuntu 向けのアプリを任意のタイミングでビルドしてくれます。
私は手動にしました。
todo
雑多
C# ぶりに自分にかなり上手くはまった言語でした。
実務でも触りたいくらいには本当に好きになってきている。
(遍歴: C# -> C/C++ -> PHP, js(vue) -> rust, js(react))
linter がとにかく丁寧に指示してくれるので書きにくさはそこまで感じないです。
所有権/ライフタイムあたりの概念が学習を難しくさせているとは思いますが、個人的には技術基盤のない初心者ほど手を付けてほしい言語です。
なんでこれが流行んねえんだろうなぁと思ってます。
(なんなら先入観のせいで私も手付けるの遅かったし)
オンでもオフでもモンハンに追われてる毎日なのでここからまた時間はかかるかと思いますが、
一般公開できるくらいには改修していきたいです。
rust おもろい。
Servo
これ来たら tauri が本当に最強になってしまうので期待してます。
Rust製ブラウザエンジンの「Servo」、アプリに組み込み可能なクロスプラットフォーム対応WebView化を目指す。Electron代替を目指す「Tauri」への組み込み実現へ - Publickey
こっちは受け入れ側の tauri-apps。
毎日のようにネットワークが動いているのが本当に素敵
Commits · tauri-apps/wry · GitHub