Edited at

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

More than 1 year has passed since last update.


現在、セキュリティーが厳しい現場にいるため、外部サイトへのアクセスが制限されています。

そのため、今までは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(<タイトル>)


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

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

https://github.com/r1chardj0n3s/parse

この部分は自信がなかったので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は使えるみたいです。

パーサー部分はかなり汚いコードになってしまいました。やっぱりパーサーは宣言的に書くべきなんでしょうかね。

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