調整さんで絶対に回答させるline botを作ってみた【5】

どうも、ナナオです。 今回はCSVをパースして、未回答の人が誰かを取得する処理を実装していきます。 CSVパース処理の仕様 とりあえず、調整さん側の動きについて確認するためにイベントを作ってみます。 調整さん - 簡単スケジュール調整、出欠管理ツール 作ったイベントはこんな感じです。 作成したイベントに関連するURLは以下のようになっています。 イベントページのURL https://chouseisan.com/s?h={イベントID} CSVダウンロードのURL https://chouseisan.com/schedule/List/createCsv?h={イベントID}&charset=utf-8&row=choice イベントを作成してすぐにCSVを取得すると、以下のようなフォーマットになっています。 テストイベント 日程 11/15(水) 19:00〜 11/16(木) 19:00〜 11/17(金) 19:00〜 コメント これに回答すると以下のようになります。 テストイベント 日程,nanao 11/15(水) 19:00〜,◯ 11/16(木) 19:00〜,△ 11/17(金) 19:00〜,× コメント,test 回答者が増えると以下のようになります。 テストイベント 日程,nanao,test_1,test_2 11/15(水) 19:00〜,◯,×,△ 11/16(木) 19:00〜,△,△,△ 11/17(金) 19:00〜,×,◯,△ コメント,test,test_1,test_2 名前からLineのアカウント名を推測するという方法もあるんですが、ここはシンプルに回答者の数とグループ内の人数が合わない場合に通知するという感じにしましょう。 (ここまでやってて思ったんですが、調整さんだとちょっとカスタマイズ性にかけてしまいますね。。今後の改修ポイントかも) パース処理を実装 CSVをパースするためにまずはcsvモジュールを依存関係に追加します。 csv = "1.3.0" bytes = "1.5.0" # reqwestからのレスポンスボディをバイトで取得するため 前回、line_clientモジュールという名前で実装しましたが、調整さんへのリクエストも処理させたいのでhttp_clientという名前に変更して調整さんへのリクエスト処理を実装します。 // src/http_client.rs // ...中略... // 調整さんのCSV取得エンドポイントのレスポンスを表す構造体 pub struct GetChouseiCsv { title: String, csv_text: String, } // LineClient -> HttpClientに変更 pub struct HttpClient { client: Client, } impl HttpClient { // ...中略... // 調整CSVを取得する pub async fn get_chousei_csv(&self, event_id: String) -> Result<GetChouseiCsv, reqwest::Error> { let url = format!("https://chouseisan.com/schedule/List/createCsv?h={}&charset=utf-8&row=choice", event_id); let res_body = self.client.get(url).send().await?.text().await?; let lines: Vec<&str> = res_body.lines().collect(); let title = lines[0]; let csv_text = lines[2..].join("\n"); Ok(GetChouseiCsv{ title: title.to_string(), csv_text }) } } 実装できました。 ...

2023年11月15日 · にあえん

調整さんで絶対に回答させるline botを作ってみた【4】

もう4回目の記事になるんですね。ナナオです。 今回は設定された期日まで15分おきにメッセージを送れるようにしてみたいと思います。 実装をモジュール化 今までの実装でメインモジュールがだいぶごちゃごちゃしてきたので分離してあげます。 LineGroupの構造体はmodel.rsを作ってそちらに移動します。 use sqlx::FromRow; use chrono::NaiveDate; use serde::{Serialize, Deserialize}; #[derive(FromRow, Serialize, Deserialize)] pub struct LineGroup { pub id: String, pub deadline_date: NaiveDate, } DBのline_groupに対する処理はrepository.rsを作ってそちらに移動します。 use crate::model::LineGroup; use sqlx::PgPool; pub struct LineGroupRepository<'a> { pool: &'a PgPool } impl<'a> LineGroupRepository<'a> { pub fn new(pool: &'a PgPool) -> Self { Self { pool } } pub async fn select(&self, id: String) -> Result<Option<LineGroup>, sqlx::Error> { let line_group = sqlx::query_as(r"SELECT * FROM line_group WHERE id = $1;") .bind(id) .fetch_optional(self.pool) .await?; Ok(line_group) } pub async fn update(&self, data: &LineGroup) -> Result<(), sqlx::Error> { sqlx::query(r"UPDATE line_group SET deadline_date = $2 WHERE id = $1;") .bind(&data.id) .bind(&data.deadline_date) .execute(self.pool) .await?; Ok(()) } pub async fn insert(&self, data: &LineGroup) -> Result<(), sqlx::Error> { sqlx::query(r"INSERT INTO line_group (id, deadline_date) VALUES ($1, $2);") .bind(&data.id) .bind(&data.deadline_date) .execute(self.pool) .await?; Ok(()) } } 署名認証処理はutils.rsを作ってそちらに移動します。 ...

2023年11月11日 · にあえん

調整さんで絶対に回答させるline botを作ってみた【3】

意外とRustってとっつきやすいなと考えを改めました。ナナオです。 今回はメッセージから期日設定などを行えるように実装を修正していきます。 ヤバいさんのコマンドを呼び出す では具体的にどうやって期日設定を行うかといえば、「グループチャット内」で「矢倍くん宛」にメッセージを送ったときに行うようにしましょう。 また、期日設定以外にも調整さんのURLを設定できる必要があります。あとヘルプコマンドもあると便利ですね。 ということで、コマンドを3つ用意します。 調整さんのURL設定 「ヤバいさん https://chouseisan.com/s?h=[調整さんのイベントID]」と送られた場合に実行される 候補日に対してどのくらい回答しているか確認するため 期日設定 「ヤバいさん 2023-01-01」と送られた場合に実行される ヘルプ 「ヤバいさん」とだけ送られる、もしくは対応していないコマンドを送られた場合に実行される 使い方、コマンドの一覧を教えてくれる コマンドといえばコマンドパターンですね。 こんな感じのtraitを実装します。 trait BotCommand { fn execute(&self, value: Value); } これを使って、3つのコマンドを実装します。 struct BotSetDeadlineCommand { pool: PgPool, line_group: LineGroup, }; impl BotSetDeadlineCommand { /// コマンドのコンストラクタ /// DBの更新に必要な値を渡します fn new( pool: PgPool, group_id: &str, deadline_date: &str, ) -> Self { let line_group = LineGroup { id: group_id.to_string(), deadline_date: NaiveDate::parse_from_str(deadline_date, "%Y-%m-%d") .expect("日付型への変換に失敗しました") }; Self { pool, line_group } } } impl BotCommand for BotSetDeadlineCommand { fn execute(&self, value: Value) {} } // それ以外のコマンドも一応用意しておく struct BotSetUrlCommand; impl BotCommand for BotSetUrlCommand { fn execute(&self, value: Value) {} } struct BotHelpCommand; impl BotCommand for BotHelpCommand { fn execute(&self, value: Value) {} } まずは期日設定を行えるようにしましょう。 ...

2023年11月9日 · にあえん

調整さんで絶対に回答させるline botを作ってみた【2】

開発楽しい、ナナオです。 前回まででMessaging APIのwebhookとの疎通確認ができました。 今回はもう少し踏み込んで期日設定を行えるようにしていきます。 ShutleにPostgresを導入する まず、設定された期日を保存しておくためのDBを用意します。 これにはShuttleが提供しているPostgresを使用します。 Shuttle Shared Databases - Shuttle Docs また、基本的にDBクライアントとしてはSqlXを使用することが推奨されているようなので、依存関係を追加しておきます。 [dependencies] axum = "0.6.20" serde_json = "1.0.108" shuttle-axum = "0.31.0" shuttle-runtime = "0.31.0" shuttle-shared-db = { version = "0.31.0", features = ["postgres"] } sqlx = { version = "0.7.2", features = ["runtime-tokio-native-tls", "postgres"] } また、SqlX CLIもインストールしておきます。 cargo install sqlx-cli SQLX CLIでルートパッケージからマイグレーションファイルを追加しておきます。 sqlx migrate add init これでmigrationsというディレクトリがルートパッケージに作成されるはずです。 続けてマイグレーションファイルにグループテーブルを作成するSQLを実装します。 とりあえずグループIDと期日があれば十分でしょう。 -- Add migration script here CREATE TABLE line_group ( id varchar(33) primary key, deadline_date date ); ちなみにグループIDは33桁の文字列で、C[0-9a-f]{32}という正規表現で表されます。 LINEプラットフォーム用語集 - グループID ...

2023年11月6日 · にあえん

ローカルのWebサーバーを一時的に外部に公開!ngrokを試してみた

WebHookのテストどうすればいいのか悩んでます、ナナオです。 今回はローカルのHTTPサーバーを簡単に外部URLとして公開することができるngrokを触ってみたいと思います。 インストール Windows11であれば、以下のコマンドでインストールできるようです。 winget install Ngrok.Ngrok ただ、僕の環境だとうまくインストールできなかったので、chocolateryを使用しました。 choco install ngrok インストールできました。 ローカルサーバーを外部URLで公開する 適当にdockerでNginxサーバーを実行してみます。 docker run -d -p 8000:80 nginx:latest では、ngrokで外部URLを取得してみましょう。 ngrok http 8000 外部URLが払い出されたので、早速アクセスしてみます。 エラーになってしまいました。 どうやらHTMLを表示するにはサインアップが必要なようですね。 以下のページからサインアップを済ませます。 ngrok - Online in One Line サインアップができたら以下のような画面が表示されるので、この画面にあるコマンドを実行します。 ngrok config add-authtoken [認証キー] この状態で再度外部URLを取得すると、先ほどとは違う画面が表示されるようになります。 この画面の「Visit Site」をクリックすることで、ローカルのNginxサーバーにアクセスすることが可能になります! 外部URLからローカルのNginxにアクセスできた! まとめ すごい…すごすぎる…。 これでWebHookの実装がめちゃくちゃ楽になりそう。 今回は紹介していませんが、Node・Rust・Go・PythonでSDKがあったり、HTTP以外にもTCPで公開できたり、Basic認証もOAuthもOpenID Connectも使えるし、開発者にとってはうれしいことしかない機能が盛りだくさんで失神してしまいそうです。 Ngrok docs 少し使ってみただけでももうハードユーズ確定な予感がします。とりあえず使っているすべてのPCに入れたい。。

2023年11月4日 · にあえん

Rustのshuttleを使ってみる【3】

Shuttleはいいぞ~、ナナオです。 今回で三回目になります、Shuttleのチュートリアル記事です。 複数のサービスを動かす shuttle_runtime::Serviceを実装してあげることで、Webサービスとスケジューラを同時に動かすようなユースケースが実装可能になります。 Page Not Found こちらのチュートリアルではdiscordのBOTを動かしていますね。 以前までの実装にスケジューラを追加して、継続的にログにhello worldを出力させてみましょう。 ただactix-webでの実装がわからなかったので、axumの実装に直しました。 また、スケジューラにはtokio-cron-schedulerを使用しました。 依存関係は以下のとおりです。 [dependencies] axum = "0.6.20" shuttle-runtime = "0.31.0" tokio = "1.26.0" tokio-cron-scheduler = "0.9.4" では、以下が完成したコードです。 use axum::{routing::get, Router, response::IntoResponse}; use tokio_cron_scheduler::{Job, JobScheduler}; async fn hello_world() -> impl IntoResponse { "Hello World!" } pub struct MultipleService { router: Router, scheduler: JobScheduler, } impl MultipleService { pub async fn new() -> Result<MultipleService, shuttle_runtime::Error> { let router = Router::new() .route("/", get(hello_world)); let mut scheduler = JobScheduler::new().await.unwrap(); scheduler.add( Job::new("1/10 * * * * *", |_uuid, _l| { println!("I run every 10 seconds"); }).unwrap() ).await.unwrap(); Ok(Self { router, scheduler, }) } } #[shuttle_runtime::async_trait] impl shuttle_runtime::Service for MultipleService { async fn bind(mut self, addr: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { let server = axum::Server::bind(&addr); let (_runner_hdl, _axum_hdl) = tokio::join!(self.scheduler.start(), server.serve(self.router.into_make_service())); Ok(()) } } #[shuttle_runtime::main] async fn main() -> Result<MultipleService, shuttle_runtime::Error> { MultipleService::new().await } 順番に説明します。 ...

2023年11月3日 · にあえん

Rustのshuttleを使ってみる【2】

タダでもうれしいのはOSSだけ、ナナオです。 前回に引き続き、Shuttleを使ってみようと思います。 ワークスペースでプロジェクトをデプロイする shuttleはワークスペースをサポートしています。 これにより、複雑なコードをカプセル化しつつ、可読性高く開発することができます。 (私の最初のイメージでは、ワークスペースのメンバーそれぞれがプロジェクトとして独立した形でデプロイされるのかと思ってたんですが、ルートパッケージが一つのプロジェクトとして扱われるようになる感じでした…) Page Not Found ただし、既存のパッケージをプロジェクトとして独立させるみたいなことをするのは少し厄介です。 というのも、どうやらそもそもローカルにあるCargoディレクトリの移動ということが想定されていないためか、既にデプロイしているディレクトリを移動するとデプロイができなくなってしまいます。 また、内部的にgitのコミットを追っているというのもあり、gitの管理をいったん外すといったことができなくなります。 つまり、新たに作成したワークスペースのメンバーに既存のCargoパッケージを追加する場合、本来はルートパッケージ側でgitを管理したいのにそれができなくなってしまうということが考えられます。 (まぁこの辺は単に私のドキュメントの読み込み不足という可能性もあるので、その辺は指摘あればコメントください) ということで、前回の記事で作成したプロジェクトは一旦リモートのプロジェクトからは削除して、新たにワークスペース単位でリソース管理ができるようにしてみましょう。 まずはベースになるルートパッケージを作成します。 cargo new shuttle-playground 次に、前回作成したプロジェクトを削除します。 # 前回作成したshuttle-playground-nanaoを削除します # 削除前にサーバーを止めておく必要がある cargo shuttle stop # 停止後に削除ができるようになる cargo shuttle project delete これできれいになりました! 次にルートプロジェクト内に前回のshuttle-playground-nanaoを移動します。 ローカルでの名前は変えておきましょうか。 mv shuttle-playground-nanao shuttle-playground/backend backendをワークスペースのメンバーに追加してあげます。 [workspace] members = [ "backend", ] このままのパッケージ名だとありふれているため競合してしまいますが、ルートパッケージ配下にShuttle.tomlを作成することで解決できます。 ここでは以前と同じ名前にしておきましょう。 name = "shuttle-playground-nanao" この状態で、ルートパッケージのファーストコミットを行っておきます。 git add --all git commit -m "first commit" ではデプロイしていきます。 # Shuttleプロジェクトを開始 cargo shuttle project start # プロジェクトにデプロイ cargo shuttle deploy デプロイ後、アクセスした様子が以下になります。 ...

2023年11月2日 · にあえん

Rustのshuttleを使ってみる【1】

Rustで無料で建てられるサーバーがあるだって?やったー!ナナオです。 今回はそんなshuttleが気になったので使ってみます。 Installation - Shuttle Docs Shuttleとは インフラ構築なしでバックエンドをデプロイできるサービスです。 firebaseと似たような、いわゆるBaaSってやつですね。 主な使い方としては簡易なWebサーバーの構築や、Discordクライアントとして使用できるようです。 無料プランの中でデータ永続化サービスとしてPostgresも使えるので、結構充実しています。 #[shuttle_runtime::main] async fn main( // automatic db provisioning + hands you back an authenticated connection pool #[shuttle_shared_db::Postgres] pool: PgPool, ) -> ShuttleRocket<...> { // application code } 以下のライブラリをサポートしているため、これらのライブラリの使用経験があればすぐにでもデプロイができると思います。 Axum Actix Web Rocket Warp Tower Salvo Poem Tide Thruster shuttle-next 早速使ってみる まずはshuttle cliをインストールしておきます。 cargo install shuttle CLIからshuttleアプリケーションを初期化できます。 質問形式でプロジェクトをセットアップできます。 > cargo shuttle init First, let's log in to your Shuttle account. If your browser did not automatically open, go to https://console.shuttle.rs/new-project ✔ API key · ******** What do you want to name your project? It will be hosted at ${project_name}.shuttleapp.rs, so choose something unique! ✔ Project name · shuttle-playground-nanao Where should we create this project? ✔ Directory · C:\Users\Nanao\Project\study\shuttle-playground Shuttle works with a range of web frameworks. Which one do you want to use? ✔ Framework · actix-web Creating project "shuttle-playground-nanao" in "C:\Users\Nanao\Project\study\shuttle-playground" Hint: Check the examples repo for a full list of templates: https://github.com/shuttle-hq/shuttle-examples Hint: You can also use `cargo shuttle init --from` to clone templates. See https://docs.shuttle.rs/getting-started/shuttle-commands or run `cargo shuttle init --help` ✔ Claim the project name "shuttle-playground-nanao" by starting a project container on Shuttle? · yes Project "shuttle-playground-nanao" is ready Your project will sleep if it is idle for 30 minutes. To change the idle time refer to the docs: https://docs.shuttle.rs/getting-started/idle-projects Run `cargo shuttle deploy --allow-dirty` to deploy your Shuttle service. You can `cd` to the directory, then: Run `cargo shuttle run` to run the app locally. APIキーが必要なため、事前にブラウザ側でshuttleのコンソールにログインしている必要があります。 ...

2023年11月2日 · にあえん

最近のRaspberry PI OSの入れ方

ラズパイ4を買って3年放置していました、ナナオです。 ミーハーなので買ったんですが、買ったはいいものの使わずに放置していました。(MOTTAINAI) それを最近思い出したので、とりあえず軽量OS入れてk8sサーバーとして使ってあげようかなと思い調べてみると、以前とOSの入れ方が結構変わっていたので備忘録として書いておこうと思います。 Raspberry OSの入れ方 OSイメージを対象のmicroSDに入れるためのツールをダウンロードします。 Windows11の場合、以下のコマンドでダウンロード可能です。 winget install RaspberryPiFoundation.RaspberryPiImager なんと、このツール一つだけで簡単にmicroSDに入れることができるようになったみたいです! 素晴らしいですね。 スタート画面はこんな感じ 適当なSDカードを用意して、これを使ってイメージを入れましょう。 今回使用したいOSにはk8sを入れたいです。 GUIは特にいらないので、軽量版を選択します。 諸項目を選択したら「次へ」を選択します。 事前に設定する項目を編集するかどうか聞かれるので「はい」にしておきます。 ここではwifiやsshといった設定が事前にできます。 SSHの設定をしておけば、いちいちモニタをつなぎなおしたりする必要もないのでとても便利ですね。 設定ができたら保存を押して、「はい」を選択します。 SDカードにまだ何かデータが存在した場合、データは消去されるという警告が出ます。 事前にバックアップはとったのではいで進めます。 イメージの書き込みが開始されます。しばらく待ちましょう。 書き込みが無事完了したらその旨が表示されます。 これで準備はできました! あとはRaspberry PI4に挿してあげれば起動します。 まとめ イメージが簡単にSDカードに入りました。 以前だったらラズパイのOSダウンロードページからISOをダウンロードしてSDカードに入れるのには別のツールを使っていたと思うんですが、これ一本でできるので便利ですね。 放置しててごめんねラズパイ。

2023年11月1日 · にあえん

調整さんで絶対に回答させるline botを作ってみた【1】

早いものでもう11月ですね。ナナオです。 年末に近づいて忘年会シーズンとなってきました。 ただ忘年会の幹事をするとなると全員の日程調整がめんどくさいじゃないですか。 なかなか日程が合わないということもありますが、日程に関する回答をしてくれない人がいると何も決まりませんよね。 ということで今回は、調整さんで絶対に回答させるline botを作っていこうと思います。 仕様 絶対に回答させるのに効果的な方法ってなんだと思いますか? 丁寧にお願いする?相手を信じて待つ? いいえ違います、相手が回答するまで鬼電するのです。 結局これが一番効くんですよね。 ということで、line botの趣旨としては「鬼電」です。 たださすがに電話はできないので、メッセージをちょうどうざいくらいの間隔で送るようにします。 期日を過ぎてからの経過時間でどんどんメッセージ送信量が増えていくみたいな感じにします。 期日まで 未回答者に1日に一回催促のDMが送られる 期日経過~1日 未回答者に1時間に一回催促のDMが送られる 期日経過1日~2日 未回答者に30分に一回催促のDMが送られる 期日経過3日~ 未回答者に15分に一回催促のDMが送られる 設計 簡単にフローにしてみます。 LINEグループにbotを入れる 期日を設定する 期日を確認(全員回答するまで呼び出し続ける) 回答していない人を確認 回答していない人がいる場合、期日に応じた処理(DM送信)を行う なので、プログラムとしては「期日を設定する」ものと「期日を確認する」ものの二つが必要になります。 呼び出される場面がそれぞれ違います。 期日を設定する line botに対して期日を設定するメッセージが送られたとき 期日を確認する 定期的に呼び出す 15分に一回 回答していない人がいる場合、期日からの経過日数によって処理を分ける 期日設定に関してはMessaging APIのWebhookイベントを使用します。 今回はRustアプリを無料でデプロイ可能なクラウドプラットフォームShuttleを利用します! Shuttle - Build Backends Fast Shuttleはよくわからなかったので、事前にちょっと触りつつ予習しておきました。 よければこちらの記事もぜひ。 Rustのshuttleを使ってみる【1】 Rustのshuttleを使ってみる【2】 Rustのshuttleを使ってみる【3】 調整さんから回答していない人の取得実装は、参考になるブログを見つけました。 調整さんリマインダLINE BOTを作ってみた - やらなイカ? どうやら、調整さんのスケジュールIDが分かれば日にち候補をCSV形式でダウンロードができるようです。 (APIキーとか使わなくてもいいのはセキュリティ的にちょっと気になるけど) これを使って「参加者の誰が回答していないか」を取得し、未回答者がいる場合は適切な制裁を加えていきます。 実装のセットアップ まずはLine Messaging APIのチャネルを作成しましょう。 LINE Developers これがline botのベースになります。 ...

2023年11月1日 · にあえん