うつろぐ

文章

「Slackbotの独自性って何」ってなった人が作ったクソSlackbot

総コン Advent Calendar 2017 - Adventar の12日目の記事です。

大学の研究室のゼミで「PythonでSlackbotを作ろう」みたいな流れになったのでそこで作ったSlackbotについて記事を書きます。

 

 

概念的な話

実装以前の「何を作るか?」みたいな話です。

Slackbotの独自性って何

今思えばこんなにこれについて悩む必要なかったというか、Slackbotの開発を始めるまで「Slackbotが動く」ということ自体が楽しい、みたいなことが想定できていなかったんだけど、当時「Slackbotに出来て他のbot(LINE@、Twitterbotなど)に出来ないことってなんだ???」ということについてはだいぶ悩んでました。

特にSlackは閉じたツールなので、「みんなに使ってもらう」という点についてはとても弱いのです。なのでその閉じた中で何が出来たら嬉しいかをずっと考えてました。

 

公開とか、機能を試してもらうとか

「面白対話botを作ってみんなに使ってもらう」みたいなことだとやっぱりLINE@一強な感じもします。対話の実装はSlackbot並みに簡単っぽいし、公開もTwitterとかにリンクやらを投げるだけだし。一方「一定間隔で何かをするbot」だとTwitterbotでしょうね。

Slackはやはり閉じたツールなので一般公開には向きません。当時はこのことばっかり気になってたのですが、よく考えてみたらSlackにも「使ってもらう」観点でのメリットがありました。

 

Slackbotはチームの一員として発言することになるので、「チームに仲間が一人加わった感」が出ます。これは当時全く意識出来てなかった点です。研究室のSlackに元々いるSlackbotが動いている様子を見ている感じだと、面白い奇抜な機能よりも、AIっぽさを優先するとこの「チームに仲間が一人加わった感」が強くなるのだと思います。

 

Slackの印象

で当時は「一般公開に向かない」ということしか考えてなかったのでここで詰まりそうになったのですが、Slackの特徴として「PCアプリにもスマホアプリにも通知が飛ぶ」というのを意識し始めていていました。これはLINEにも当てはまるのですが、僕の中のSlackに対する印象として

「LINEと比較してPCとスマホのどちらでもよく起動する」

というのがありました。LINEぜんぜん開きませんね。起動するのにエネルギーが要ります。というか僕のLINEに来るインフォメーションが今の所成人式の同窓会関連と心理学(大学の講義)のグループワーク関連しかないのが問題なのですが。


あと、制作物の方向性の決め手になった出来事が一つあって、なんと先日僕はゼミの日程が変わったことを完全に忘れていて結果的にゼミをブッチしてしまった。これはたぶんTrelloの通知が標準でオフになっているのが悪い。間違いない。これがきっかけで

「Slackにある重要事項を通知させるのに別のアプリが必要なのは馬鹿らしい」

という感じになってきました。

結果何になったか

上のようなことを経た結果、

「メモしたチーム内の重要事項を通知してくれる君」

になりました。:tada: 
 

実装

実装(実装)

よくあるサービス未満の何かをつくった

http://iida.nkmr.io/*Proj/Hatena_melTest/index.php

取り敢えず「メモしたチーム内の重要事項を通知してくれる君」の「メモ」の部分をPHPで作りました。これはSlackに紐づいてるものではなく動作サンプルです。CSS全く書いてないのはゆるして

諸々の操作は適当にやることにして、重要な部分だけ。

f:id:Distorted_Unchi:20171212011455p:plain

このようなメモを作ると、通知時刻の2017/12/12 01:15になると
 
f:id:Distorted_Unchi:20171212011820p:plain

このようなリプライが届く、という想定です。ユーザー名をhereにしてあげると全員に通知出来るし、ユーザー名で特定ユーザーに通知出来る。

 

これに追随したSlackbot

紐づいたSlackチームのチャンネルにリプライ通知を送信する、というようなSlackbotをPythonで作成しました。

機能として、

・通知時刻になったらリプライ通知送信(1分ごと)
コマンドライン的なメモの作成(create:ユーザーID,タイトル,説明,通知時刻)
・指定ユーザーページ表示(userpage:user)
・ホーム表示(home)
 
があります。後半二つは若干おまけっぽい。

だいたいのソースコードはこちら。サンプルなので一部アドレスやパスワードは変えてあります。

# -*- coding: utf-8 -*-

from slackbot.bot import Bot
from slackbot.bot import respond_to
from slackbot.bot import listen_to
from slacker import Slacker
import slackbot_settings

from datetime import datetime
import threading

import json
import requests
import re

#------------------------------------------

slack=Slacker(slackbot_settings.API_TOKEN)

#------------------------------------------

def timer_minute():
    resJson=requests.get('http://iida.nkmr.io/*Proj/Hatena_melTest/dumperAPI.php?username=xxxxxx&password=xxxxxx').json()
    for i in range(len(resJson)):
        if format(resJson[i]['due']) == datetime.now().strftime('%Y/%m/%d %H:%M'):
            slack.chat.post_message(
                "melbot_test",
                '@'+format(resJson[i]['author'])+': 「'+format(resJson[i]['title'])+'」の通知時刻になりました: http://iida.nkmr.io/*Proj/Hatena_melTest/edit_form.php?id='+format(resJson[i]['id']),
                as_user=True,
                link_names=True
            )

    timerM=threading.Timer(60.0,timer_minute)
    timerM.start()


#------------------------------------------

@respond_to(r'(create:.*,.*,.*,.*)')
def request_func(message,something):
    pattern=r'create:(.*),(.*),(.*),(.*)'
    searchOB=re.search(pattern,something)
    if searchOB:
        print(searchOB.group(1))
        print(searchOB.group(2))
        print(searchOB.group(3))
        print(searchOB.group(4))
    requests.post(
        'http://iida.nkmr.io/*Proj/Hatena_melTest/submitterAPI.php',
        {
            'author':searchOB.group(1),
            'title':searchOB.group(2),
            'body':searchOB.group(3),
            'due':searchOB.group(4),
            'mode':'new',
            'username':'xxxxxx',
            'password':'xxxxxx'
        }
    )
    message.reply('requestしたよ')

@respond_to(r'(userpage:.*)')
def userpage_func(message,something):
    pattern=r'userpage:(.*)'
    searchOB=re.search(pattern,something)
    if searchOB:
        print(searchOB.group(1))
        message.reply('http://iida.nkmr.io/*Proj/Hatena_melTest/userpage.php?author='+searchOB.group(1))

@respond_to(r'home')
def home_func(message):
    message.reply('http://iida.nkmr.io/*Proj/Hatena_melTest')

#------------------------------------------

def main():
    timerM=threading.Timer(60.0,timer_minute)
    timerM.start()
    bot = Bot()
    bot.run()

if __name__ == "__main__":
   main()

 

最初のブロックはライブラリのインポート。

次のブロックではSlackerの設定をしています。

次のブロックでは、「通知時刻になったらリプライ通知送信」の部分を実装しています。threadingを用いて1分ごとに全てのメモの時刻(due)を参照して、分までが現在時刻と一致したらSlackerのchat.post_messageでリプライを送っています(chat.post_ephemeralが何故か動かなかったので代用)。
chat.post_ephemeralをchat.post_messageで代用する際、link_namesをTrueにしておかないと、「@user」などの部分がハイライトされず、平文になってしまい通知も届かないので注意。こうなる。↓
f:id:Distorted_Unchi:20171212014155p:plain

その次のブロックでは、リプライに対する応答を書いています。@respond_toの引数には正規表現が使えます。また、@respond_toの引数の1部分を()で括るとその値が関数の第二引数somethingに格納されます。
somethingは一つしか取れないようで、例えばrequest_funcでやっているように複数に分けて取りたいときは一度全体を()で括ってsomethingに格納したのち、reライブラリのsearchで分けて格納し直す運びになります。

最後のブロックはテンプレですね

ドメインAPIもつくった

 気付いた方もいるかもしれませんが、今回データの参照や追加の際にpythonからmysqlを叩くのではなく、PHPで自ドメインAPI(もどき?)を作成してrequestsで叩いています。というのも、研究室のサーバーにmysql関連のpythonライブラリが入っておらず、教授に依頼してインストールしていただいても良かったのですが、「APIとかそういうの全然作ったことないな」と思ったので何となくやりました。

データ追加の方はinsert実行するだけなのでロクなこと書いてないので省略して、データをjsonで吐き出す方の解説を簡単にしたいと思います。

<?php
function raw_json_encode($input) {

  return preg_replace_callback(
    '/\\\\u([0-9a-zA-Z]{4})/',
    function ($matches) {
      return mb_convert_encoding(pack('H*',$matches[1]),'UTF-8','UTF-16');
    },
    json_encode($input)
  );

}

header('Access-Control-Allow-Origin:http://iida.nkmr.io');
header("Content-Type: application/json; charset=utf-8");

if($_GET['username']==='xxxxxx' && $_GET['password']==='xxxxxx'){
  $sql="
  select * from table;
  ";

  $mysqli=new mysqli("localhost", "username", "password");
  $mysqli->set_charset("utf8");
  $mysqli->select_db("hoge_db");
  $results=$mysqli->query($sql);

  $data=array();

  while($line=$results->fetch_array(MYSQLI_ASSOC)){
    $data[]=array(
      id=>$line['id'],
      author=>$line['author'],
      title=>$line['title'],
      body=>$line['body'],
      due=>$line['due']
    );
  }

  echo raw_json_encode($data);
  $mysqli->close();
}else{
  header("Location: error.php");
  exit;
}
?>

研究室のサーバーのphpのバージョンがゲロ古いのでPDOではなくmysqliを使っています。

ポイントは2つで、まず1つはheader("Content-Type: application/json; charset=utf-8");です。これで出力結果のフォーマットをjsonに指定しています。これは
PHPを用いて3行でAPIを作る!! | Life Tips
を参考にさせて頂きました

もう1つはjson_encodeで日本語が文字コードに変換されてしまうので、
json_encodeで全角文字が文字化けする - PHPプロ!Q&A掲示板
のraw_json_encodeという関数を利用させていただきました。これによるとPHP5.4以降ならjson_encodeにオプションが適用できるらしいですが、研究室のサーバーのphpのバーzyありがとうございました。

こんな感じに出力される
f:id:Distorted_Unchi:20171212021620p:plain

おしまい。

参考文献
PythonでSlackbotを作る(3) – ビットログ
初心者に捧げる対話システムの作り方 - Qiita
PHPを用いて3行でAPIを作る!! | Life Tips
json_encodeで全角文字が文字化けする - PHPプロ!Q&A掲示板