Help us understand the problem. What is going on with this article?

Googleカレンダーの予定からバイトの給料を計算して教えてくれるSlack botが欲しい!

はじめに

Googleカレンダーとても便利ですよね.Gmailと連動できるし,PCから開けるし,シンプルだし.ただ,不満に思う点があります.それがバイトの給料を計算してくれないことです.シフト管理アプリと連動してくれれば良いのに...
と言うことで,気になってた各種APIとかAWSとかを初心者なりに使いつつ,作ってみました.

使ったもの

  • Slack
  • AWS
  • Googleカレンダー
  • Python

アーキテクチャ全体像

以下のような構成でbotを作成しました.

仕組み

手順

  1. Slack APIのサイト上でbot作成用のアプリを作成する
  2. AWS上にSlackイベントを受け取るエンドポイントを作成する
  3. GoogleAPIを使って,Googleカレンダーからバイトの給料を計算するプログラムを作成
  4. AWS lambdaに導入

【手順1】【手順2】Slack botを作成し,AWS上にエンドポイントを作成する

この手順は,以下の記事にとてもお世話になりました.記事の通りに進めると,問題なくできると思います.
AWS初心者でもわかる! ブラウザ上で完結! AWS+Slack Event APIを使ったSlackボット超入門

【手順3】Googleカレンダーからバイトの給料を計算するプログラムを作成

Googleカレンダーの予定をPythonを使って取得する方法は以下の記事を参考にしました.
Google Calenderの予定をPython3から取得する

次に,公式のサンプル公式リファレンスを参考に,給料を計算するプログラムを作っていきます.

プログラムは大きく以下に分かれます.

  • handle_slack_event():エントリポイント
  • MakePayMsg():ユーザテキストに対応した給料を計算し,メッセージを作成
  • CalculatePay():日給を計算

フルコードはgithubにあるので,そちらを参照してください.

handle_slack_event()

エントリポイントです.ユーザが送信したテキストを解析し,それを元にメッセージを投稿します.

# -----エントリポイント-----
def handle_slack_event(slack_event, context):

    # 受け取ったイベント情報をCloud Watchログに出力
    logging.info(json.dumps(slack_event))

    # Event APIの認証
    if "challenge" in slack_event:
        return slack_event.get("challenge")

    # ボットによるイベントまたはメッセージ投稿イベント以外の場合
    # 反応させないためにそのままリターンする
    # Slackには何かしらのレスポンスを返す必要があるのでOKと返す
    # (返さない場合、失敗とみなされて同じリクエストが何度か送られてくる)
    if is_bot(slack_event) or not is_message_event(slack_event):
        return "OK"

    # ユーザからのメッセージテキストを取り出す
    text = slack_event.get("event").get("text")

    # 給料計算クラスの宣言
    pay_msg = MakePayMsg()

    # ユーザからのテキストを解析して,メッセージを作成
    if 'help' in text:
        msg = '知りたい情報に対応する番号を入力してください!\n'
        msg += '(1)来月の給料\n'
        msg += '(2)今年の給料\n'
        msg += '(3)給料のログ\n'
    elif text == '1':
        msg = '来月の給料は¥{}です!'.format(pay_msg.monthpay())
    elif text == '2':
        msg = '{}'.format(pay_msg.yearpay())
    elif text == '3':
        msg = '給料ログ\n{}'.format(pay_msg.paylog())
    else:
        msg = '\\クエー/'

    # メッセージの投稿
    post_message_to_slack_channel(msg, slack_event.get("event").get("channel"))

    # メッセージの投稿とは別に、Event APIによるリクエストの結果として
    # Slackに何かしらのレスポンスを返す必要があるのでOKと返す
    # (返さない場合、失敗とみなされて同じリクエストが何度か送られてくる)
    return "OK"

MakePayMsg()

ユーザテキストに対応した給料を計算し,メッセージを作成します.

# -----給料を計算し,メッセージを作成する-----
class MakePayMsg():
    def __init__(self):

        self.SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
        self.now = datetime.datetime.now()
        self.events = self.get_event() # Googleカレンダーから取り出したイベント
        self.pay_log = self.make_paylog() # 今年分の給料ログ

    # ---Googleカレンダーからイベントを取り出す---
    def get_event(self):
        creds = None
        if os.path.exists('token.pickle'):
            with open('token.pickle', 'rb') as token:
                creds = pickle.load(token)
        # If there are no (valid) credentials available, let the user log in.
        if not creds or not creds.valid:
            if creds and creds.expired and creds.refresh_token:
                creds.refresh(Request())
            else:
                flow = InstalledAppFlow.from_client_secrets_file(
                    'credentials.json', self.SCOPES)
                creds = flow.run_local_server(port=0)
            # Save the credentials for the next run
            with open('/tmp/token.pickle', 'wb') as token:
                pickle.dump(creds, token)

        service = build('calendar', 'v3', credentials=creds)

        # バイトのシフトを登録しているカレンダーを選択
        calender_id = os.environ['CALENDER_ID']

        page_token = None
        events = service.events().list(calendarId=calender_id, pageToken=page_token).execute()
        return events

    # ---今年分の給料ログを作成する---
    def make_paylog(self):
        pay_log = []
        cal = CalculatePay(1013, 1063, 1.25, 22) # 時給情報を入力

        # eventからバイトの開始時間と終了時間を取り出し,給料計算する
        for event in self.events['items']:
            # 開始時間と終了時間をdatetimeに変形
            stime = event['start']['dateTime']
            stime = datetime.datetime(
                int(stime[0:4]), int(stime[5:7]), int(stime[8:10]),
                int(stime[11:13]), int(stime[14:16]))
            etime = event['end']['dateTime']
            etime = datetime.datetime(
                int(etime[0:4]), int(etime[5:7]), int(etime[8:10]),
                int(etime[11:13]), int(etime[14:16]))

            # 給料計算をする期間
            # (x-1)年12月~x年11月に働いた分がx年の給料
            if self.now.month != 12:
                sdate = datetime.date(self.now.year-1, 12, 1)
                edate = datetime.date(self.now.year, 11, 30)
            else:
                sdate = datetime.date(self.now.year, 12, 1)
                edate = datetime.date(self.now.year+1, 11, 30)

            # 1年分の給料をログとして記録
            if (stime.date() >= sdate) and (etime.date() <= edate):
                # 開始時間と終了時間から1日分の給料計算
                daypay = cal.calculate(stime, etime)
                # 働いた分が翌月の給料になるように調整
                if stime.month==12:
                    daypay_dir = {'date':stime.date(), 'month':1, 'pay':daypay}
                else:
                    daypay_dir = {'date':stime.date(), 'month':stime.month+1, 'pay':daypay}
                pay_log += [daypay_dir]
        pay_log = sorted(pay_log, key=lambda x:x['date'])
        return pay_log

    # ---来月の給料を表示するメッセージを作成---
    def monthpay(self):
        mpay = 0
        for i in self.pay_log:
            if self.now.month!=12:
                if i['month'] == (self.now.month+1):
                    mpay += i['pay']
            else:
                if i['month'] == 1:
                    mpay += i['pay']
        return mpay

    # ---1年分の給料を表示するメッセージを作成---
    def yearpay(self):
        mpay_list = [0] * 12
        for i in self.pay_log:
            mpay_list[i['month']-1] += i['pay']
        msg = ''
        for i, mpay in enumerate(mpay_list):
            msg += '{}月 ¥{:,}\n'.format(i+1, mpay)
        msg += '\n合計¥{}'.format(sum(mpay_list))
        return msg

    # ---1年分のログを表示するメッセージを作成---
    def paylog(self):
        msg = ''
        month = 0
        for i in self.pay_log:
            while i['month'] != month:
                msg += '\n{}月\n'.format(month+1)
                month += 1
            msg += '{} ¥{:,}\n'.format(i['date'], i['pay'])
        return msg

ここで注意点ですが,Lambda が書き込みできるのは/tmp配下のファイルのみです.
そのため,あるファイルへの書き込み処理をする場合,ローカルで実行した時にはエラーが起きなかったのに,Lambdaで実行したところ[Errno 30] Read-only file systemが発生すると言うことがあります.
このプログラムでもエラーが出たため,以下のように変更しました.

変更前
with open('token.pickle', 'wb') as token:
    pickle.dump(creds, token)
変更後
with open('/tmp/token.pickle', 'wb') as token:
    pickle.dump(creds, token)

CalculatePay()

日給を計算します.

# -----日給を計算する-----
class CalculatePay():
    def __init__(
        self, basic_pay, irregular_pay, night_rate, night_time):

        self.basic_pay = basic_pay # 平日の時給
        self.irregular_pay = irregular_pay # 土日祝日の時給
        self.night_rate = night_rate # 深夜給の増額率
        self.night_time = night_time # 深夜給になる時間

    # ---日給を計算---
    def calculate(self, stime, etime):
        night_time = datetime.datetime(stime.year, stime.month, stime.day, self.night_time)

        if stime.weekday() >= 5 or jpholiday.is_holiday(stime.date()):
            pay = self.irregular_pay
        else:
            pay = self.basic_pay

        if etime >= night_time:
            normal_time = self.get_h(night_time - stime)
            night_time = self.get_h(etime - night_time)
            daypay = normal_time * pay + night_time * (pay * self.night_rate)
        else:
            normal_time = self.get_h(etime - stime)
            daypay = normal_time * pay

        return round(daypay)

    # ---x時間y分→h時間表示に変換---
    def get_h(self, delta_time):
        h = delta_time.seconds / 3600
        return h

【手順4】AWS lambdaに導入

Googleカレンダーから給料を計算するプログラムを書く上で,いくつかのモジュールをインストールしました.
何もせずにAWS Lambdaでこのプログラムを実行すると,ModuleNotFoundErrorが出ます.
様々なモジュールををAWS Lambda上でも使用できるようにするために,Python用のAWS Lambdaデプロイパッケージを作成します.簡潔に言うと,プロジェクトディレクトリにすべての依存モジュールをインストールし,実行ファイルと一緒にzipでアップロードします.
これは,以下のサイトの通りに実行すれば良いです.
【Python】AWS Lambdaで外部モジュールを使用する

実行結果

最近Googleカレンダーにバイトのシフトを記録し始めたので,データが少ないです.
このbotを導入したので,今後はシフト管理もGoogleカレンダーでしていきたいと思います.
(バイト先がどこかわかりやすいアイコンだなあ)

helpの表示

スクリーンショット 2019-11-28 0.37.29.png

来月の給料の表示

スクリーンショット 2019-11-28 0.36.52.png

今年の給料

スクリーンショット 2019-11-28 0.37.05.png

給料のログ

スクリーンショット 2019-11-28 0.42.47.png

その他

スクリーンショット 2019-11-28 0.45.04.png

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away