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

Table of Contents

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

そのため、今まではPCからやっていた打刻がスマートフォンでしかできなくなり、あの小さい画面でユーザーIDを入力して、パスワード、種別を選択して。。。。

めんどくさい

ということでSlack botにやってもらいます。

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

概要

Slackで「おは」を打つと出勤打刻をして、「おつ」を打つと退勤打刻します。これだけです。 主要なライブラリはslackbotとScrapyです。slackbotで投稿された文字を判定し、「おは」と「おつ」ならScrapyでWebサイトに打刻しに行きます。

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

@listen_to('おは')
def start_work(message):

    global user_info
    global channel_info
    global v_info

    user_info       = None
    channel_info    = None

    # ユーザー名を取得
    send_user = message.channel._client.users[message.body['user']]['name']
    user_info = get_user_info(send_user)

    # チャンネル名を取得
    channel_info = message.channel._client.channels[message.body['channel']]

    # ユーザー情報が存在すれば打刻処理を行う
    if user_info:
        run_spider(TimeStampSpider)

        message.reply('{}、おはよう!\r出勤打刻しといたよ^^ {}'.format(send_user, v_info))
    else:
        message.reply('{}さん、おはよう!'.format(send_user))

コールバックの引数で受け取った情報からユーザー名を取りだし、自分(と同期)のユーザー名と一致した場合はScrapyのスパイダーを実行します。別の人ならただ返信します。

ユーザー名の判別

def get_user_info(su):
    '''
    ユーザー情報を取得
    '''
    global fpath
    # json モジュールをインポート
    import json 
    # jsonファイルを読み込む
    f = open(fpath)
    d = json.load(f)
    f.close()
    return [d[i] for i in range(len(d)) if d[i]["slack_user_name"] == su][0]

テキストファイル(JSON)で保存してある自分(と同期)のユーザー名と一致する場合はその情報を返します。 JSONにしたこれといった理由はなく、ただ前にjavascriptを書いたことがあり、数人しか使わないものなので、DBを使うこともないかなーと。

打刻しにいく

class TimeStampSpider(scrapy.Spider):
    '''
    出勤打刻する
    '''

    global user_info
    global g_name
    global g_allowed_domains
    global g_start_urls

    name            = g_name
    allowed_domains = g_allowed_domains
    start_urls      = g_start_urls

    custom_settings = {
            'ROBOTSTXT_OBEY':False
    }

    # ログイン画面
    def parse(self, response):

        f_data = { \
                const.FORM_NAME_02: user_info['id'], \
                const.FORM_NAME_03: user_info['pw'] \
        }
        return scrapy.FormRequest.from_response(
            response,
            formdata=f_data,
            callback=self.after_login,
            dont_filter=True
        )

    # 打刻レコーダー画面
    def after_login(self, response):

        f_data = { \
            const.FORM_NAME_11: user_info['devision'], \
            const.FORM_NAME_16: user_info['department'], \
            const.FORM_NAME_12: '1' \
        }
        return scrapy.FormRequest.from_response(
            response,
            formdata=f_data,
            callback=self.after_attend,
            dont_filter=True
        )

    # 打刻後
    def after_attend(self, response):
        post_ss(response)

最初にアクセスページをstart_urlsに設定し、parse -> after_login -> after_attendの順にコールバックが呼ばれます。

scrapy.FormRequest.from_responseメソッドの

第一引数にはコールバックで受け取ったレスポンス情報をそのままわたし、 第二引数ではフォームのnameとvalueの組み合わせを辞書型で渡します。 第三引数にはレスポンスが返ってきた場合に呼ばれるコールバック、 あとの引数は任意です。

dont_filterは同じページに連続でアクセスした場合にScrapyが内部でフィルターをかけるのを防ぎます。

打刻の証拠にスクショを取る

def post_ss(response):
    '''
    Chromeでスクショを取る。
    GUI環境とChromeがあることが前提
    '''
    # エンコーディング判別
    guess = chardet.detect(response.body)
    # Unicode化
    unicode_data = response.body.decode(guess['encoding'])

    # レスポンスとhtmlファイルにして保存
    global curpath
    fname = curpath + '/tmp.html'
    # 書き込みモードで開く
    f = open(fname, 'w') 
    # 引数の文字列をファイルに書き込む
    f.write(unicode_data) 
    # ファイルを閉じる
    f.close() 

    # htmlファイルを画像ファイルに変換
    tmp_img_file = './my_screenshot.png'
    DRIVER = './chromedriver'
    driver = webdriver.Chrome(DRIVER)
    driver.get('file://' + fname)
    driver.save_screenshot(tmp_img_file)
    driver.quit()

    # slackerで送信
    from slackbot_settings import API_TOKEN
    token = API_TOKEN
    # 投稿するチャンネル名
    global channel_info
    c_name = channel_info['name']
 
    # 投稿
    slacker = Slacker(token)
    slacker.files.upload(tmp_img_file, channels=[c_name], title='打刻後のスクショ')

打刻が完了したら最後の画面のスクショを取ってSlackに投稿します。

  1. レスポンスを受け取り、Unicodeに変換してからhtmlファイルにして保存
  2. seleniumを使ってChromeで開き、スクショを取り画像を保存
  3. 画像をSlackerを使って投稿する

という処理です。ちなみに自分のPCではChromeが立ち上がっているのが確認できますが、VPSで動かしているとスクショが投稿されません。GUIがないと無理だと自分では思っていますが、正しいのでしょうか。

slackbotには画像投稿をする機能がない(気がする。あるのかな)ので、Slackerを使って投稿しています。

苦労したところ

Scrapyの実行

slackbotのプラグイン内でスパイダーを実行すると、プロセスがmainにないとかいう理由でエラーがでました。 いろいろ思考錯誤した結果、ネットで見つけた以下の方法を間に挟むことで解決しました。

def run_spider(spider):
    '''
    スパイダーを実行する関数
    '''
    def f(q):
        try:
            runner = CrawlerRunner()
            deferred = runner.crawl(spider)
            deferred.addBoth(lambda _: reactor.stop())
            reactor.run()
            q.put(None)
        except Exception as e:
            print(e)
            q.put(e)

    q = Queue()
    p = Process(target=f, args=(q,))
    p.start()
    result = q.get()
    p.join()

詳しいことは勉強不足ではっきりしないのですが、別プロセスを立ち上げて、非同期で実行したあと、処理の終了を待つという感じのことをしているようです。。。適当ですみません。javascriptっぽい書き方ですね。

スクショ

slackbotを使ってこのボットを作ろうと始めたのですが、画像を送るところで「あれ、これ使えないのかな」と詰まってしまいました。いろいろ探した結果Slackerに落ち着きましたがボットのライブラリを複数使うのはなんともかっこ悪いですね。

また、スクショの画像に関しても、Scrapyでできればよかったんですが、実力不足で色々あいだに一時ファイルを噛ませてやってます。しかもVPSでは取れないし。ここは是非改善したいです。

これからやりたいこと

残業申請

残業すると、時刻の入力やら、残業理由やら、いろいろとやらなくちゃいけないことが多いので、現時点では対応していません。これを実装してやっと完成かなと思っています。

感想

とりあえず、使えているので毎日が便利になりました。やっぱり、ここが一番うれしいですね。 今回使用したライブラリはチュートリアルを除いて使うのが初めてだったのでリファレンスを見に行ったり、QiitaやStack Over Flowなどを読みながら少しずつ理解しながらやりました。 非同期関連のモジュールはもっと勉強しなきゃなと感じました。

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