LoginSignup
7
4

More than 5 years have passed since last update.

もう毎日の打刻めんどいからSlack botにやってもらおう ―― その2

Last updated at Posted at 2018-01-27

現在、セキュリティーが厳しい現場にいるため、外部サイトへのアクセスが制限されています。
そのため、今まではPCからやっていた打刻がスマートフォンでしかできなくなり、あの小さい画面でユーザーIDを入力して、パスワード、種別を選択して。。。。
めんどくさい
ということでSlack botにやってもらいます。

という記事を前に書きました。
https://qiita.com/kentakozuka/items/eedebcc4275c894c45a3

今回はその改良版についての記事になります。
改良版といってもまるっとほぼ変えてしまったのですが。。。

コードはgithubに載せています。
https://github.com/kentakozuka/dakoku_v2

やったこと

打刻

Slackで「おは」を打つと出勤打刻をして、「おつ」を打つと退勤打刻して、「した」と打つと退勤打刻+残業申請をします。
slackbotで投稿された文字を判定し、「おは」と「おつ」と「した」ならSeleniumでWebサイトに打刻しに行き、結果をスクショで返します。

ユーザー情報登録・変更

Slackでボットに対してメンションを送るとCLIっぽくユーザー登録変更ができます。

ライブラリ

主要なライブラリはslackbot、Slacker、Selenium、parseです。

出勤時:「おは」と投稿された場合

メッセージリスナー

@listen_to('おは')
def start_work(message):
    ''' 出勤打刻をする '''
    mh = MessageHandler( \
            message, \
            Ts.CrawlRunner(Ts.StartStampCrawler()), \
            const.msg_start_stamp, \
            const.msg_start_greeting)
    mh.run_crawler()

メッセージハンドラー

class MessageHandler(Handler):
    '''メッセージハンドリングクラス'''

(中略

    def run_crawler(self):
        ''' クロール共通処理 '''
        # ユーザー名を取得
        send_user = self.get_send_user()
        user_info = super().get_user_info(send_user)

        # チャンネル名を取得
        channel_info = self.get_channel_info()

        # ユーザー情報が存在すれば打刻処理を行う
        if user_info:
            if not super().is_all_fields_filled(send_user):
                self.message.reply('設定されていない項目があるため打刻処理を実行できません。')
            else:
                # クロール
                img_path = self.crawlrunner.run(user_info)
                # スクショを投稿
                self.post_img(img_path)
        else:
            self.message.reply(self.greeting_msg.format(send_user))

        # disconnect
        self.cursor.close()
        self.connect.close()

メッセージが「おは」場合、メッセージハンドラーを実行します。ハンドラーにはクローラークラスのインスタンスを呼び出して、クロールを実行させます。ハンドラー内ではリスナーのコールバックの引数で受け取った情報からユーザー名を取りだし、自分(と同期)のユーザー名と一致し、かつDBに情報がある場合はクロールを実行します。別の人ならただ返信します。

打刻しにいく

class AbsCrawler:
    '''
    Strategyパターン
    クロール実行抽象クラス
    '''

    def __init__(self):
        self.user_info = None
        # ドライバを生成
        self.driver = self.init_driver()
        # 最大の待機時間(秒)を設定
        self.wait = WebDriverWait(self.driver, 20)

    def set_user_info(self, user_info):
        ''' ユーザー情報のセッター '''
        self.user_info = user_info

    @abstractmethod
    def run(self):
        pass

    #######################################
    # ページ遷移
    #######################################

    def proc_with_page_move(self, func):
        ''' ページ読み込みを含む処理 '''
        # 関数実行
        func()
        # ページが読み込まれるまで待機
        self.wait.until(ec.presence_of_all_elements_located)

    def go_page(self, page_url):
        ''' 指定した画面に遷移 '''
        self.driver.get(page_url)

    def go_login_page(self):
        ''' ログイン画面に遷移 '''
        page_url = const.LOGIN_URL
        self.go_page(page_url)

    def go_time_stamp_page(self):
        ''' 打刻画面に遷移 '''
        page_url = const.TIME_STAMP_URL
        self.go_page(page_url)

中略

    #######################################
    # フォーム送信
    #######################################

    def login(self):
        ''' ログインフォーム '''
        loginid = self.driver.find_element_by_id('id')
        password = self.driver.find_element_by_id('pass')

        loginid.send_keys(self.user_info['daim_id'])
        password.send_keys(self.user_info['daim_password'])

        self.driver.find_element_by_name("form01").submit()

    def stamp_start(self):
        ''' 出勤打刻フォーム '''
        self.set_option(const.FORM_NAME_11, self.user_info['office_id'])
        self.set_option(const.FORM_NAME_16, self.user_info['department'])
        self.set_option(const.FORM_NAME_12, '1')

        self.driver.find_element_by_name("form01").submit()
class StartStampCrawler(AbsCrawler):
    ''' 出勤打刻クラス '''
    def __init__(self):
        super().__init__()

    def run(self):
        # ログインページ遷移
        super().proc_with_page_move(super().go_login_page)
        # ログイン実行
        super().proc_with_page_move(super().login)
        ## 打刻画面遷移
        super().proc_with_page_move(super().go_time_stamp_page)
        ## 出勤打刻実行
        super().proc_with_page_move(super().stamp_start)
        # スクショ
        img_path = super().take_screen_shot()
        # ドライバを閉じる
        self.driver.quit()
        # スクショのパスを返す
        return img_path

クローラーの実行でやることは単純で、ブラウザでやっていることと同じことをSeleniumにやらせるだけです。
ポイントとしては以下で余裕をもった時間を設定しておかないとDOMをすべて解析する前にタイムアウトと判断されてエラーがでることです。

# 最大の待機時間(秒)を設定
self.wait = WebDriverWait(self.driver, 20)

また、ドライバは”普通の”ブラウザ(Chromeとか)を設定することもできますが、その場合ブラウザが立ち上がって処理が進むのでGUIがない環境ではPhantomjsを設定しないと動きません。

driver_path = <ドライバのパス>
user_agent = <ユーザーエージェント>
dcap = {
    "phantomjs.page.settings.userAgent" : user_agent,
    'marionette' : True
}
driver = webdriver.PhantomJS(
        executable_path=driver_path,
        desired_capabilities=dcap)

return driver

ちなみに、遷移した最後の画面のスクショをslackに送信するのですが、slackbotでは画像の送信ができないため、一度画像ファイルとして保存したスクショをSlackerで送信しています。

# スクショを投稿する
slacker = Slacker(<API-トークン>)
slacker.files.upload( \
            <ファイルパス>, \
            channels=[<投稿するチャンネル名>], \
            title=self.finish_msg.format(<タイトル>)

ユーザー情報の登録・変更

コマンドのパーサについて、今回は簡単なコマンドのパースだったので、以下のライブラリを使ってパターンマッチで実装しました。

この部分は自信がなかったのでpytestを使ってテストしながら実装しました。やっぱり単体テストすると安心しますね。

実際のコマンドの使いかたはこんな感じです。

使い方: @dakoku_bot コマンド

Slack上で動作する打刻ボット

コマンド:
    show    各種情報を表示します。

            サブコマンド:
                user            ユーザー情報を表示します。
                office          部署情報を表示します。

            入力例 --ユーザー情報を表示する:
                @dakoku_bot show user

    add     ユーザー情報を新規登録します。

            入力例:
                @dakoku_bot add

    set     各種情報を設定します。

            オプション:
                --uid=string    stringにはユーザーID (user id) を入力します。
                --pw=string     stringにはパスワード (password) を入力します。
                --dep=string    stringには所属部署 (department) の番号を入力します。
                --atime=HH:MM   HH:MMには出勤時間 (attendance time) を入力します。
                --ltime=HH:MM   HH:MMには退勤時間 (leaving time) を入力します。
                --cow=string    stringには残業理由 (cause of overtime-work) を入力します。

            入力例 1 --まとめて設定する:
                @dakoku_bot set --uid=0123456789 --pw=mypassword --atime=09:00 --ltime=18:00 --cow=作業過多のため

            入力例 2 --残業理由のみ変更する:
                @dakoku_bot set --cow=作業過多のため

    del     ユーザー情報を削除します。

            入力例:
                @dakoku_bot del

やってみて思ったこと

前回の記事にも書いたのですが、毎日が便利になりました。やっぱり、ここが一番うれしいですね。
前回はScrapyを使っていたのですが、どうしてもある画面でセッションが取得できなくて、かなりハマってしまいました。試行錯誤を重ねたのですが結局できず、Seleniumにすべてを書き換えることになりました。実際、簡単なクローリングならSeleniumの方が格段に楽に書けると感じました。ScrapyでもPhantomjsは使えるみたいです。
パーサー部分はかなり汚いコードになってしまいました。やっぱりパーサーは宣言的に書くべきなんでしょうかね。

最後に便利なライブラリを作っているみなさま、ありがとうございます!

7
4
0

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
7
4