どうも、ナナオです。
今回は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 })
}
}
実装できました。
ただcsv_text
をもう少し詳細にパースしてあげたいですね。
pub struct GetChouseiCsv {
title: String,
csv_text: String,
member_info_map: HashMap<String, MemberInfo>, // 追加
}
// メンバーの出欠状態やコメントを含む構造体を定義
#[derive(Debug)]
pub struct MemberInfo {
availability_for_candidate_dates: HashMap<String, String>,
comment: Option<String>,
}
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");
let csv_bytes = csv_text.as_bytes();
let mut reader = csv::Reader::from_reader(csv_bytes);
let headers = reader.headers().unwrap();
let member_name = headers.iter().map(|x| x.to_string()).collect::<Vec<String>>()[1..].to_vec();
let mut reader = csv::Reader::from_reader(csv_bytes);
let mut member_info_map: HashMap<String, MemberInfo> = HashMap::new();
for record in reader.records() {
let record = record.unwrap();
if &record[0] == "コメント" {
for (i, m) in member_name.iter().enumerate() {
let comment = record[i + 1].to_string();
if member_info_map.contains_key(m) && !comment.is_empty() {
let member_info = member_info_map.get_mut(m)
.expect("メンバー情報を格納したマップからの情報取得に失敗しました");
member_info.comment = Some(comment);
}
}
}
else {
let day = record[0].to_string();
for (i, m) in member_name.iter().enumerate() {
let status = record[i + 1].to_string();
if member_info_map.contains_key(m) {
let member_info = member_info_map.get_mut(m)
.expect("メンバー情報を格納したマップからの情報取得に失敗しました");
let dates = &mut member_info.availability_for_candidate_dates;
dates.insert(day.clone(), status);
} else {
let availability_for_candidate_dates = HashMap::from([
(day.clone(), status)
]);
member_info_map.insert(m.clone(), MemberInfo { availability_for_candidate_dates, comment: None });
}
}
}
}
Ok(GetChouseiCsv{ title: title.to_string(), csv_text, member_info_map })
}
}
はい、これで結構ちゃんとパースが実装できました。
グループ内の人数取得を実装
パース処理によって出欠に答えたメンバー数を取得できるようになったので、あとはグループ内に何人いるかを取得できるようにします。
Messaging APIリファレンス | LINE Developers
このエンドポイントではボットを除いたグループ内の人数が何人いるかを教えてくれます。
返却されるオブジェクトもかなり単純なので、関数では数値のみ返却するようにしてあげればよいでしょう。
impl HttpClient {
// ...中略...
pub async fn count_group_members(&self, id: String) -> Result<i64, reqwest::Error> {
let res: Value = self.client.get(format!("https://api.line.me/v2/bot/group/{}/members/count", id))
.bearer_auth("{アクセストークン}")
.send()
.await?
.json()
.await?;
let count = res.as_object()
.expect("グループ人数取得のレスポンスJSONをオブジェクトに変換できませんでした")
.get("count")
.expect("グループ人数取得のレスポンスJSONにcountプロパティが定義されていません")
.as_i64()
.expect("グループ人数取得のレスポンスJSONのcountプロパティが正しく数値に変換できませんでした");
Ok(count)
}
}
実装できました。
まとめ
念願のパース処理が完成しました。
あとは調整さんのイベントIDをline_groupテーブルに設定できるようにしてあげて、通知ができるようにしてあげればいったん完成です。
調整さんを使わずに実装したほうが自由度高そうなので、これが完成したらもうちょっと洗練された方法を考えます。
ではまた。