2
0

More than 3 years have passed since last update.

Rustに入門する〜6日目 始業・終業タイミングでSlackのステータスを変更する〜

Last updated at Posted at 2020-05-15

初めに

前回の続きです。
少し期間が空いたのは色々あった...というよりサボっていた面が多いです。
少しコードを見るとdocstringがないとか抜きにしてあまりにもコードがひどいことに気づきました(commitに[WIP]を書くレベルです)。
...がこれに手をつけてるといつになるんだという形でしたので、
その点も踏まえて記事を書こうと思います。

こんな内容ですが一旦最終日予定です。

本日の内容は以下です。

  • SAMのテンプレートにスケジューラを追加する
  • 昨日までのコードを結合して表題の機能を実現する
    • 平日は始業・終業タイミングでステータスを変更
    • カレンダーの予定を見て「休」を含む終日スケジュールがあれば上記より優先して変更
      • 有給・半休等のケースを意図していますが条件がゆるすぎるのでそのうち見直し予定です。
    • 土日祝日は朝ステータスを変更
  • 祝日のデータを取得する
    • ユーザのスケジュールとは別に持ってるらしいので別途取得します。

SAMのテンプレートにスケジューラーを組み込む

SAMのテンプレートのざっくり動作確認については1日目を参考。

前回のテンプレートではLambda関数を呼び出すためのトリガーが存在しなかったのでEventsを組み込みます。
組み込むものは以下の通りです。

  • 月〜金曜日の朝09:30に起動するスケジューラー
    • 実行時にパラメータeventType="morning"をプログラム側に引き渡す
  • 月〜金曜日の朝08:30に起動するスケジューラー
    • 実行時にパラメータeventType="evening"をプログラム側に引き渡す
  • 土日の朝09:30に起動するスケジューラー
    • 実行時にパラメータeventType="holiday"をプログラム側に引き渡す

曜日はプログラム側で判定してもいいですがあらかじめ呼び出す側で判別しておくほうが楽な気がするのでそうします。

スケジューラを入れたいのでEventsに以下を設定します。

スケジュール
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-property-function-schedule.html

cron式の記載は以下の通りです。

Schedule Expressions for Rules
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/ScheduledEvents.html

template.yml
AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Parameters:
  SchedulerEnabled:
    Type: String
    AllowedValues:
      - True
      - False
    Default: False

Resources:
  ChangeSlackStatusProject:
    Type: "AWS::Serverless::Function"
    Properties:
      FunctionName: ChangeSlackStatus
      Handler: main
      Runtime: provided
      CodeUri: ./rust.zip
      Events:
        MorningEvent:
          Type: Schedule 
          Properties:
            Name: SlackStatusChangeMorningEvent
            Enabled:
              Ref: SchedulerEnabled
            Schedule: cron(30 9 ? * 2-6 *)
            Input: "{\"event_type\": \"morning\"}"
        EveningEvent:
          Type: Schedule 
          Properties:
            Name: SlackStatusChangeEveningEvent
            Enabled:
              Ref: SchedulerEnabled
            Schedule: cron(30 18 ? * 2-6 *)
            Input: "{\"event_type\": \"evening\"}"
        HolidayEvent:
          Type: Schedule 
          Properties:
            Name: SlackStatusChangeHolidayEvent
            Enabled:
              Ref: SchedulerEnabled
            Schedule: cron(30 9 ? * 1,7 *)
            Input: "{\"event_type\": \"holiday\"}"

ちなみにイベントは流用する予定でなければ
名前に関数名とかつけておかないとCloudWatch側でイベント名が被ったりしてわからなくなる可能性があります。
あえてつけないで自動生成に任せるのも手ですが。

デフォルトはスケジューラを無効化にして
samconfig.tomlparameter_overrides = "SchedulerEnabled=True"にして有効化を...
と思ったのですがなぜかうまく行かないので検証中

Google Calendar APIで祝日を取得する

自分が確認する限りはカレンダーIDは ja.japanese#holiday@group.v.calendar.google.com でした。
自分のカレンダーIDの確認と同じ要領でGoogleカレンダーの右上の「設定」を開き左の「日本の祝日」で確認しました。

祝日自体が一つのカレンダーとして存在するみたいなので3日目に作ったやつにカレンダーIDでこれを指定すれば取得できそうです。
と思いましたが404となりました。いかに同じ現象のQAがありました。
https://stackoverflow.com/questions/36406779/google-calendar-api-public-holidays

いまいち理解できなかったので、
調べた結果 japanese__ja@holiday.calendar.google.com でいけるらしいのでこれでやってみます。
(このアドレス個人のサイトでは出てくるんですが公式ドキュメントのどこかにあるんでしょうか...)

連結する

必要なものは出揃いましたので連結します。
呼び出している関数は多少変更はあるものの3日目5日目で実装したものです。
(簡単にしか確認してないのでバグがあるかもしれないですね)

main.rs
use tokio;
use std::fs::File;
use std::io::BufReader;

use lambda_runtime::{error::HandlerError, lambda, Context};
use serde::{Serialize, Deserialize};

extern crate my_google_controller;
extern crate my_slack_controller;
use my_google_controller::google_calendar;
use my_slack_controller::{slack_profile, slack_general};


#[derive(Deserialize, Clone)]
struct CustomEvent {
    event_type: String
}

#[derive(Serialize, Clone)]
struct CustomOutput {
    message: String,
}

#[derive(Deserialize, Clone)]
struct MyConfig {
    slack_user_id : String,
    event_var: EventVariable
}

#[derive(Deserialize, Clone)]
struct EventVariable{
    morning: slack_profile::SlackStatus,
    evening: slack_profile::SlackStatus,
    holiday: slack_profile::SlackStatus
}

fn main() {
    lambda!(my_handler);
}


fn my_handler(e: CustomEvent, ctx: Context) -> Result<CustomOutput, HandlerError> {
    let http_client = async{
        let config: MyConfig = serde_json::from_reader(BufReader::new(File::open("./config.json").unwrap())).unwrap();

        let slack_token = slack_general::get_access_token("./slack_secret.json").await;
        let mut profile = slack_profile::get_profile(&slack_token,&config.slack_user_id).await;


        let change_status: Option<slack_profile::SlackStatus> = match &*e.event_type.clone() {
            "morning" => {
                let my_event_list = google_calendar::get_today_schedule(&profile.email).await.unwrap();
                Some(if is_today_holiday(&my_event_list).await { config.event_var.holiday } else { config.event_var.morning })
            },
            "evening" => {
                let my_event_list = google_calendar::get_today_schedule(&profile.email).await.unwrap();
                if is_today_holiday(&my_event_list).await {
                    Some (config.event_var.evening)
                } else {
                    None
                }
            },
            "holiday" => {
                Some (config.event_var.holiday)
            },
            unknown => panic!(r#"Unknown EventType: EventType is "{}" "#,unknown)
        };

        match change_status {
            Some(status) => {
                profile.change_status(&status);
                slack_profile::set_profile(&slack_token,profile,&config.slack_user_id).await;
            },
            None => println!("No change status")
        }
    };

    tokio::runtime::Runtime::new().unwrap().block_on(http_client);
    Ok(CustomOutput{
        message: String::from("Success!!"),
    })
}


//Check special or public holidays
async fn is_today_holiday<'a>(personal_event:&'a google_calendar::CalendarEvent )-> bool {
    let japanese_holiday_schedule = google_calendar::get_today_schedule(google_calendar::JAPANESE_HOLIDAY_CALENDAR_ID);
    if !japanese_holiday_schedule.await.unwrap().items.is_empty() { return true; }

    for item in &personal_event.items {
        if item.summary.contains("休"){
            return true;
        }
    }
    false
}

config.json は以下のような記載をしています。

config.json
{
    "slack_user_id": "xxxxxx",
    "event_var": {
        "morning": {
            "status_text": "業務中",
            "status_emoji": ":house_with_garden:"
        },
        "evening": {
            "status_text": "業務終了",
            "status_emoji": ":pray:"
        },
        "holiday": {
            "status_text": "休暇中",
            "status_emoji": ":desert_island:"
        }
    }
}

slack_profile.rs のSlackProfile構造体はステータスの設定用に拡張しています。

slack_profile.rs

#[derive(Debug, Serialize, Deserialize,Clone)]
pub struct SlackStatus{
    pub status_text: String,
    pub status_emoji: String,
    pub status_expiration_from_now: Option<i64>
}

//(略)

impl<'a> SlackProfile{
    pub fn change_status(&mut self,status_info: &'a SlackStatus){
        self.status_text = status_info.status_text.clone();
        self.status_emoji = status_info.status_emoji.clone();
        self.status_expiration =  match status_info.status_expiration_from_now {
            Some(t) => Local::now().timestamp() + t,
            None => 0
        };
    }
}

特に大きくハマったところはありませんでした。
強いていうなら&mut selfの存在を知らなくて所有権がどっか行ってしまったくらいでしょうか。

直近の取り組みとしてなんとかしたい点

とりあえず上記のもので(バグはともかく)最低限自分が欲しい機能を実装することができました。
ただしあまりにも造りがひどい部分が多いので記事にするかどうかは別として以下の修正を予定しています。
derive に無駄なものを定義しているので整理するとか、configの配置が悪いとか他にも諸々あります。

適切なエラー処理を入れる

現状unwrap()で無理やり動かしていますが、
このままだとSlackのメールアドレスのGoogleCalendarの情報が取れない(404等)とかでもPanicで落ちます。
なのでawait?等でエラーを返却し、エラーをハンドリングできるようにします。
他にも諸々ごまかしているところはmatchでしっかりハンドリングしたいですね。

複数のエラーの型をどうするかという点について標準機能で可能とのことで4日目のコメントでアドバイスを頂いていますので、
こちらを参考に修正予定です。

関数単位で継承っぽいことをする

上記のコードで以下のものを書いているときにどうしてこんなことになってるんだと絶望していました。
この作りだとSlackのProfileを触るたびに毎回tokenを関数に渡さなければなりません。

slack_profile::set_profile(&slack_token,profile,&config.slack_user_id).await

こうしてしまった背景として以下のような発想をしてしまったからです。

トークンの取得は共通機能だからどこか1箇所で定義して継承して共通させたい!
->でも継承はRustにない...かといって同じ定義のメソッドをコピペでやりたくない...
->じゃあ共通領域に関数を定義してその結果を引数で持たせるか

よくよく考えたらトレイト内の関数の定義として指定した関数を呼び出すだけの同名関数を定義すればできそうです。
例えば今回の場合は以下です(実際にまだ動かしてないですが)。
これで self.xxxx に値を持たせてそれを参照するようにすればなんとかいけそうな気がします。

impl SampleStruct{
    pub async fn get_access_token<'a>(secret_path: &'a str) -> SlackAccessToken {
        slack_general::get_access_token(secret_path).await
    }
}

ただこれが良い方法なのかはわからないですし、
また関数の数自体が増えてくるとこの方法では大変かもしれません。

チュートリアルに書いてある継承がない背景を考えるとある程度めんどくさいとなるのは言語仕様な気はしますが。

https://doc.rust-jp.rs/book/second-edition/ch17-01-what-is-oo.html
継承は、近年、多くのプログラミング言語において、プログラムの設計解決策としては軽んじられています。 というのも、しばしば必要以上コードを共有してしまう危険性があるからです。サブクラスは、 必ずしも親クラスの特徴を全て共有するべきではないのに、継承ではそうなってしまうのです。 これにより、プログラムの設計の柔軟性を失わせることもあります。また、道理に合わなかったり、メソッドがサブクラスには適用されないために、 エラーを発生させるようなサブクラスのメソッドの呼び出しを引き起こす可能性が出てくるのです。 さらに、サブクラスに1つのクラスからだけ継承させる言語もあり、さらにプログラムの設計の柔軟性が制限されます。

これらの理由により、継承ではなくトレイトオブジェクトを使用してRustは異なるアプローチを取っています。

テストコードを書く

実は生まれてこの方テストコードを書いたことないどころか、
サンプルのコードしか見たことがないので、この機会に書いてみたい!と思ってます。

終わりに

一旦の仮締めの記事がこんな微妙な感じのコードで終わってしまったのはなんとも言えないところですが、
半分くらいできたところで投げてばかりの自分としてはなんとか目的のものが動くという段階までたどり着けて良かったと思います。

正直今回記事を書いていなかったら強制力がなかったら絶対にここまで行かないで投げていたので、
実際にどれくらい見られたかは関係なく、誰かが見れる場所にチラ裏感覚でも書いておくというだけで違うなと思いました。
また、このようななぐり書きでもコメントアドバイスを頂き、一人だと解決しなかった場所も解決し感謝しております。

最近はインタプリタ言語を触ることが多くこういった制約が多い言語を触るのは久々で煩わしい部分もありましたが、
(特にRustの制約は厳しいのもあり)
少し書き慣れてくるとむしろそれが良く、考え方も割と好きなのでこれからもちょいちょい使っていけたらと思います。

また今回のコードはgithubにあげていますのでこちらで確認できます(readme更新していなかったりしますが)
https://github.com/nekotouma0114/ChangeSlackStatusProject

実はgitほぼわからない(commit/pushくらいできる)時代以降ぶりにgithubにコードを上げました。今もちょっとわかる程度ですが。
業務はbitbucket+redmineでやっている上に新規で0から構築ということもほぼないので、
githubの場合ブランチどうしよう(普段はredmineのチケット番号使っているので)とか、githubの使い方自体も勉強中です。

2
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0