出社/退社のWeb打刻とSlackでの出社/退社報告を自動で行うコードを書いた

**Windows向けの話です。Macの人は工夫すればなんとかなると思います。**

## はじめに
朝、会社に着いたらまずすることってなんでしょう?
**そう、打刻ですね。** 
特に、私は現在アルバイトの身分であり、時給制なので、**打刻は1分1秒を争います。** 

うちの事業部では、打刻と同時に、Slackのチャンネルへ出社/退社報告をする決まりになっています。
ところが、私はいつもWeb打刻が終わると安心してしまって、Slackへの報告を忘れてしまいます。
(生まれつき注意欠陥な性質もあり…)
**これでは他のメンバーが私が出社しているか否か確認できません。**

なので、コマンド一発で打刻とSlackへの報告を同時に行えるようにしました。

## リポジトリ
https://github.com/are-38-a/syussha-taisha-dakoku-Slack

## しくみ 

* PythonのGUI操作ライブラリPyAutoGUIを使う
* Windowsの機能「ファイル名を指定して実行」を使う
* Slackアプリとslack_sdkを使う

## 解説

### 前提
* Python 3.10.7
```requirements.txt
﻿MouseInfo==0.1.3
numpy==1.24.1
opencv-contrib-python==4.7.0.68
Pillow==9.4.0
PyAutoGUI==0.9.53
PyGetWindow==0.0.9
PyMsgBox==1.0.9
pyperclip==1.8.2
PyRect==0.2.0
PyScreeze==0.1.28
pytweening==1.0.4
slack-sdk==3.19.5
```

### 1. 打刻画面を「ファイル名を指定して実行」で開けるようにする

これは簡単です。
まず、任意のフォルダを用意します。
次に、そのフォルダにPATHを通します。
PATHを通すというのは、環境変数の```PATH```にディレクトリのパスを追加するということです。

https://qiita.com/shuhey/items/7ee0d25f14a997c9e285

私の会社では打刻画面などのアプリをMicrosoftのSSOで表示できるマイアプリポータル(MyApps)というシステムが導入されています。
そこからリンクURLを引っ張ってきてインターネットショートカットを作成し、PATHを通したフォルダに配置すれば「ファイル名を指定して実行」で開けるようになります。
ショートカットの名前は`dakoku`にしました。
ちなみに、「ファイル名を指定して実行」はショートカットキー「Win」+「R」で開くことができます。


(MyAppsなんて便利なもんねえよって人はSeleniumでうまいことやりましょう。↓)

https://qiita.com/negishitakuro/items/ee26173e90b051441c88

### 2. Slackアプリを作り、Pythonで扱えるようにする。

https://boul.tech/slack-api-intro/

上記を参考にしました。
今回必要なスコープはbotの```chat:write```のみなので、それを設定しました。
また、環境変数にAPIキーを入れたものの、万が一流出することを気にして**社内IPからのみアクセスできるように`Restrict API Token Usage`を設定しました。** 
(在宅勤務でもVPN繋ぐので、と言うか社内IPからじゃないとそもそも打刻画面すら開かないので問題ない。)

また、アプリをチャンネルに招待しておきます。

場所(物理:会社出社、論理:在宅勤務の意)と、行動(出社/退社)を指定して、Slackに通知を送る関数をPythonで書きます。

```sender.py
import os
from slack_sdk.web import WebClient

def send_syussya_taisya_to_slack(place: str, action: str, channel: str, token: str) -> dict:    
    client = WebClient(token=token)

    # メッセージ設定
    mes: str = 'メッセージが設定されていません'
    if(place == 'butsuri' and action == 'syussya'):
        mes = ':butsurisyussya:'
    if(place == 'butsuri' and action == 'taisya'):
        mes = ':butsuritaisya:'
    if(place == 'ronri' and action == 'syussya'):
        mes = ':ronrisyussya:'
    if(place == 'ronri' and action == 'taisya'):
        mes = ':ronritaisya:'        

    # メッセージ送信
    response = client.chat_postMessage(text=mes, channel=channel)

    return response
```

【追記】上の例に関してはこんな場合分けしなくても単純な文字列結合でいけるのですが、:butsurisyussya: なんて一般的ではなさそうなので、いい感じにメッセージを変えることを想定しています。

### 3. Pythonで画像認識して打刻、Slack報告を行う

PyAutoGUIには画像認識して座標を取得、マウスクリックなどを行う機能があります。
これを使って以下のようにコードを書きました。

```main.py
import logging
import pyautogui
import time
import sender
import traceback
import os

def main(place: str, action: str, channel: str) -> None:
    # ストリームハンドラの設定
    stream_handler = logging.StreamHandler()
    stream_handler.setLevel(logging.INFO)
    stream_handler.setFormatter(logging.Formatter("%(asctime)s@ %(name)s [%(levelname)s] %(funcName)s: %(message)s"))

    # ファイルハンドラの設定
    file_handler = logging.FileHandler(
        f"./syussya_taisya.log"
    )
    file_handler.setLevel(logging.INFO)
    file_handler.setFormatter(
        logging.Formatter("%(asctime)s@ %(name)s [%(levelname)s] %(funcName)s: %(message)s")
    )

    logging.basicConfig(
        level=logging.NOTSET,
        handlers=[stream_handler, file_handler])

    # ロガーの設定
    logger = logging.getLogger(__name__)

    # 打刻画面を開くコマンドを打つ
    pyautogui.hotkey('winleft', 'r')
    time.sleep(0.5)
    pyautogui.typewrite('dakoku')
    pyautogui.press('enter')

    logger.info('打刻画面表示開始')

    # 出勤/退勤ボタンを認識して押す
    if action == 'syussya':
        img: str = './btn_syussya.png'
    elif action == 'taisya':
        img: str = './btn_taisya.png'
    else:
        logger.error('出社/退社が正しく設定されていません')
        return

    logger.info('ボタン認識開始')
    p = None
    while (p == None):
        p = pyautogui.locateOnScreen(img, confidence=0.9)
        time.sleep(0.5)
    x, y = pyautogui.center(p)
    logger.info('ボタン認識')
    pyautogui.click(x, y)
    logger.info('ボタン押下しました')

    # APIキーを環境変数から取得
    try:
        SLACK_API_TOKEN: str = os.environ['SLACK_API_TOKEN']
    except:
        logger.error('APIキーが取得できません')
        logger.error(traceback.format_exc())
        return
    
    # Slackへ送信
    res: dict = {}
    try:
        res = sender.send_syussya_taisya_to_slack(place, action, channel, SLACK_API_TOKEN)
    except Exception as e:
        logger.error('Slackへの送信に失敗しました')
        logger.error(traceback.format_exc())
        return
    
    if res.get('ok'):
        logger.info('Slack(' + channel + ')へ :' + place + action + ': を送信しました')
    else:
        logger.error('Slackへの送信に失敗しました')
    
    logger.info(res)
```

認識する画像はこれらです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2882988/5e69a789-6c2b-1743-4127-7f9205308c1f.png)　
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2882988/0703be72-9a13-d96a-da49-80df5c563454.png)

例外処理やロギング、型の指定など色々使ってみましたが、多少適当なのでそれは許してください。

ボタンを画像認識する部分では、```pyautogui.locateOnScreen```を使用しています。
```pyautogui.locateOnScreen```は画像を認識することができなかったときNoneを返すので、ここに値が入るまで繰り返し画像認識を試みます。
(このコードだと無限ループに入る可能性があります。真面目にやるならリトライ回数に制限を設けてタイムアウトさせましょう。)

参考:

https://note.nkmk.me/python-try-except-else-finally/

https://qiita.com/FukuharaYohei/items/92795107032c8c0bfd19

https://qiita.com/Galvalume29/items/835b65cddaf094c2b3c2

https://future-architect.github.io/articles/20201223/

### 4. コマンド一発で叩けるようにする
さて、main.pyなんて名前のファイルのmainなんていう関数ですが、**このファイルは直接実行しません。** (もっといい命名があるような気はします。)
かわりに、以下のようなファイルを作成しました。
```bs.py
import main
import settings

if __name__ == '__main__':
    main.main('butsuri', 'syussya', settings.CHANNEL_NAME)
```
これを2(物理/論理)×2(出社/退社) = 4 通り作ります。
チャンネル名が変わると面倒なので謎に設定ファイルに切り出しました。
```settings.py
CHANNEL_NAME = "#{YOUR CHANNEL NAME}"
```

そしたら、最後に`bs.py`、`bt.py`、`rs.py`、`rt.py`のそれぞれのファイルのショートカットを作ります。
名前はそれぞれ`bs`、`bt`、`rs`、`rt`としました。
これを件のPATHが通っているフォルダに入れれば完成です。

## 動作

出社してパソコンを開いたら「Win」+「R」からの「bs」→「Enter」(ッターン!)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2882988/6fdd3f18-0a0b-8f92-7b3c-4d02d9e9b4c6.png)

自動で打刻が行われ、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2882988/bdcbf13b-639a-c2c9-729d-51da8778c39f.png)
Slackにも通知が行くことを確認しました。(テスト用のチャンネルに送っています。)

## 懸念
* Slackアプリって勝手に作っていいんだっけ? **怒られたら消します。** 
* **テスト中に何回か本当に打刻しちゃったので、勤怠が荒れ、後で怒られるかもしれない。**
    * ごめんなさい。
    * 手動でちゃんと申請するので許してください。