Slackのbotを作ってみた


ある事情でSlackを使うことになり(仕事じゃないけど)、botが作れるとのことで、まずは作ってみた。
(コード見たら何のためかわかっちゃうけどw)
Hubotを使う方法もあるみたいだが、Hubotを使わくてもWebサーバーとPHPなりのサーバーサイドで動く言語があればできる。
Outgoing/Incomingっていうらしいけど。

というわけでPHPで作ったのでメモ。

Slack botはPOSTでデータを送ってきて、JSONで返答する仕組みです。
リクエストで飛んでくる内容は

  • token
    • トークンです。
  • team_id
    • チームIDです。どこのSlackから送られてきたか判断するのに利用できるのと思います
  • channel_id
    • チャンネルID。チャンネルを判断する際に利用できます
  • channel_name
    • チャンネル名
  • tim estamp
    • 多分送信された時間
  • user_id
    • 発言したユーザのID
  • user_name
    • 発言したユーザの名前
  • text
    • 発言の内容
  • trigger_word
    • このリクエストが送られることになったきっかけのキーワード

返却するJSONの形式です。

{
	"username":"<UserName>"
	"icon_url":"<URL>"
	"icon_emoji":":<name>"
	"channel":"#<channel-name>"
	"channel":"@<username>"
	"text":<message>
}
  • username
    • botのユーザー名
  • icon_url
    • アイコンとして表示する画像のURL
  • icon_emoji
    • 使ったことないから知らない
  • channel
    • 返信するチャンネル名またはユーザー名。チャンネル名を指定する場合は「#チャンネル名」、ユーザーは「@ユーザー名」
  • text
    • 返信メッセージ

もし、自分のbotで何も返したくない場合は、空のJSONを返せばいいようです。
また、送られてきたデータはHTMLエスケープ(であってるかな?HTMLエンティティというのが正しい?)されているので、デコードしてあげる必要があります。

で、作ったソースはこれ

<?php
	date_default_timezone_set ('Asia/Tokyo');

	// Slack Post Data
	// token=XXXXXXXXXXXXXXXXXX
	// team_id=T0001
	// channel_id=C2147483705
	// channel_name=test
	// timestamp=1355517523.000005
	// user_id=U2147483697
	// user_name=Steve
	// text=googlebot: What is the air-speed velocity of an unladen swallow?
	// trigger_word=googlebot:
	// 
	// Respose JSON
	// {
	// 	"username":"<UserName>"
	// 	"icon_url":"<URL>"
	// 	"icon_emoji":":<name>"
	// 	"channel":"#<channel-name>"
	// 	"channel":"@<username>"
	// 	"text":<message>
	// }
	// Link
	// {
	// 	"text": "<URL|Text> for details!"
	// }

	if(array_key_exists("token", $_POST) == true){
		$token = $_POST["token"];
	}
	if(array_key_exists("team_id", $_POST) == true){
		$tram_id = $_POST["team_id"];
	}
	if(array_key_exists("channel_id", $_POST) == true){
		$channel_id = $_POST["channel_id"];
	}
	if(array_key_exists("channel_name", $_POST) == true){
		$channel_name = $_POST["channel_name"];
	}
	if(array_key_exists("timestamp", $_POST) == true){
		$timestamp = $_POST["timestamp"];
	}
	if(array_key_exists("user_id", $_POST) == true){
		$user_id = $_POST["user_id"];
	}
	if(array_key_exists("user_name", $_POST) == true){
		$user_name = $_POST["user_name"];
	}
	if(array_key_exists("text", $_POST) == true){
		$text = html_entity_decode($_POST["text"], ENT_COMPAT, "UTF-8");
	}
	if(array_key_exists("trigger_word", $_POST) == true){
		$trigger_word = html_entity_decode($_POST["trigger_word"], ENT_COMPAT, "UTF-8");
	}

	$returnObject = array();

	if($trigger_word != "testbot"){
	}else{
		$actionMap = array(
			 '/^(今日の)?天気$/'	=> "tenki"
			,'/^運行(状況)?$/'		=> "unkou"
			,'/^グリフ$/'			=> "glyph"

			// -------------
			// 以下はネタ
			// -------------
			,'/^コナミコマンド$/'	=> "konami"
		);

		$text = preg_replace('/^kumabot /', '', $text);

		// 時間に対応するコメント
		$timeComment = array(
			 '日付変わっちゃったよ?'
			,'夜更かしはお肌によくないよ!'
			,'丑三つ時・・・ふふふふ・・・'
			,'3時の・・・おやつの時間じゃないなぁ'
			,'いい加減寝ない?'
			,'新聞来てるかもよ?'
			,'おはようございます'
			,'朝ごはん食べた?'
			,'仕事や学校間に合う?'
			,'今日も頑張りますか'
			,'休憩ですか?'
			,'おなかすかない?'
			,'昼飯!そのあとは散歩でイン活!'
			,'ご飯の後は眠い・・・'
			,'お仕事or学校の人、頑張れ'
			,'3時のおやつ大歓迎!'
			,'この時間って微妙だよね'
			,'そろそろおなかがすいてきた・・・'
			,'暗くなってきた?'
			,'晩飯食った?'
			,'今夜はどこに?'
			,'子供は寝る時間'
			,'夜はこれから!'
			,'家帰った?終電ヤバくない?'
		);

		// 月に対応するコメント
		$monthComment = array(
			 'お年玉くれw'
			,'豆をまいて、チョコを食らう!'
			,'花粉症対策は万全?'
			,'桜は散る・・・シールドも散る・・・'
			,'柏餅食べた?'
			,'梅雨だね。足元に気を付けて!'
			,'暑くなってきたねー'
			,'夏!海!山!かき氷!'
			,'食欲の秋じゃ!'
			,'運動の秋じゃ!'
			,'紅葉みた?'
			,'さーむーいーぞー!'
		);

		$comment = array(
			 "呼んだ?"
			,"熊だからって、ハチミツが好きだとは限らないぞ!"
			,"ご飯の相談?カレーがいいなぁ・・・"
			,"・・・がう?"
			,"ポータルへの水やりはしてる?"
			,"旦那or嫁or彼氏or彼女botを乗り越えろ"
			,"調子に乗ってバースター使ってない?"
			,"リンクアンプをバカにするな!"
			,"相手のCFが育ったらコ・ワ・セ♪"
			,"ワンコはほどほどに・・・"
			,"いまどこ?"
			,"リンク先・・・あってる?"
			,$timeComment[date("G")]
			,$monthComment[date("n")-1]
		);

		// コメントをランダムに選択
		$cmtIdx = rand(0, count($comment) - 1);
		$returnObject = array("text"=>$comment[$cmtIdx]);

		if($text != ""){
			foreach($actionMap as $key => $value){
				if(preg_match($key, $text, $match) == 1){
					$returnObject = $value($match);
					break;
				}
			}
		}
	}

	// アイコンとボット名を設定
	$returnObject['username'] = "testbot";
	$returnObject['icon_url'] = "http://xxxxx.xxxx/xxxxx/icon.png";

	header('Content-Type: application/json');
	$json = json_encode($returnObject);
	echo $json;

	/*************************************************************************
	 *
	 * 天気
	 *
	 **************************************************************************/
	function tenki($match){
		// http://weather.livedoor.com/forecast/webservice/json/v1?city=220010
		// 静岡 220010
		// 網代 220020
		// 三島 220030
		// 浜松 220040
		$ids = array(
		// 静岡
		 '220010'
		// 網代
		,'220020'
		// 三島
		,'220030'
		// 浜松
		,'220040'
		);

		$retText = "";
		foreach($ids as $id){
			$tenkiJSON = HttpRequest('http://weather.livedoor.com/forecast/webservice/json/v1?city='.$id, 'GET');
			$tenkiData = json_decode($tenkiJSON);
			$retText .= $tenkiData->location->city . " ";
			$retText .= $tenkiData->forecasts[0]->telop . " ";
			if($tenkiData->forecasts[0]->temperature->min == null){
				$retText .= ' 最低気温 --';
			}else{
				$retText .= ' 最低気温 ' . $tenkiData->forecasts[0]->temperature->min->celsius . "°";
			}
			if($tenkiData->forecasts[0]->temperature->max == null){
				$retText .= " 最高気温 --\r\n";
			}else{
				$retText .= ' 最高気温 ' . $tenkiData->forecasts[0]->temperature->max->celsius . "°\r\n";
			}
			foreach($tenkiData->pinpointLocations as $pinpoint){
				$retText .= '<'.$pinpoint->link.'|'.$pinpoint->name.'> / ';
			}
			$retText .= "\r\n";
		}

		$ret = array(
			"text" => $retText
		);
		return $ret;
	}

	/*************************************************************************
	 *
	 * グリフ
	 *
	 **************************************************************************/
	function glyph($match){

		$ret = array(
			"text" => "まだ作ってない。ランダムでグリフを表示する予定。"
		);
		return $ret;
	}

	/*************************************************************************
	 *
	 * 運行状況
	 *
	 **************************************************************************/
	function unkou($match){

		$ret = array(
			"text" => "まだ作ってない。JRの運行状況のサイトのHTMLを解析して表示する予定。"
		);
		return $ret;
	}

	/*************************************************************************
	 *
	 * コナミコマンド
	 *
	 **************************************************************************/
	function konami($match){

		$ret = array(
			"text" => "上!上!下!下!右!左!右!左!B!A!"
		);
		return $ret;
	}

	/*************************************************************************
	 *
	 * HTTP通信用
	 *
	 **************************************************************************/
	function HttpRequest($requestURL, $method, $data=""){
		if($data == ""){
			$options = array('http' => array(
			    'method' => $method
			));
		}else{
			$options = array('http' => array(
			    'method' => $method,
			    'content' => http_build_query($data),
			));
		}
		return file_get_contents($requestURL, false, stream_context_create($options));
	}
?>

先に行っておきますが、まだ作りかけです。
ホントはクラス化して云々ってしようかと思ったけど、とりあえず作ることを優先してこんな感じに。

やってることは、連想配列のキーに正規表現、値に呼び出したいメソッドを設定して、送られてきたtextに正規表現に一致するものがあるかを見て動的にメソッドを呼び出す。
$matchを引数に渡すようにしているけど、まだちゃんと確認できてない。
正規表現で取り出して、それに応じて天気を出したりとかしたいなぁとか考えてる。

とりあえず、こんな感じで組めばできるみたいです。

これだけだと、Slackで動かす前に確認するのが大変なので、こんなHTMLを作って確認できるようにした。

<html>
<head>
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
	<script>
		$(function(){
			$('#btn_send').click(function(){
				$.ajax({
					 url:"./index.php"
					,type:"POST"
					,data:$('#mainForm').serializeArray()
					,success:function(data){
						$('#result').text(data.text);
					}
					,error:function(){
					}
					,completed:function(){
					}
				});
			});
		});	
	</script>
</head>
<body>
<form method="post" action="index.php" id="mainForm">
<textarea cols="40" rows="8">
	// token=XXXXXXXXXXXXXXXXXX
	// team_id=T0001
	// channel_id=C2147483705
	// channel_name=test
	// timestamp=1355517523.000005
	// user_id=U2147483697
	// user_name=Steve
	// text=googlebot: What is the air-speed velocity of an unladen swallow?
	// trigger_word=googlebot:
</textarea>
<table>
<tr>
	<td>token</td>
	<td><input type="text" name="token" value="XXXXXXXXXXXXXXXXXXXXXXXXX"></td>
</tr>
<tr>
	<td>team_id</td>
	<td><input type="text" name="team_id" value="T0001"></td>
</tr>
<tr>
	<td>channel_id</td>
	<td><input type="text" name="channel_id" value="C2147483705"></td>
</tr>
<tr>
	<td>channel_name</td>
	<td><input type="text" name="channel_name" value="test"></td>
</tr>
<tr>
	<td>timestamp</td>
	<td><input type="text" name="timestamp" value="1355517523.000005"></td>
</tr>
<tr>
	<td>user_id</td>
	<td><input type="text" name="user_id" value="U2147483697"></td>
</tr>
<tr>
	<td>user_name</td>
	<td><input type="text" name="user_name" value="Steve"></td>
</tr>
<tr>
	<td>text</td>
	<td><input type="text" name="text" value="kumabot "></td>
</tr>
<tr>
	<td>trigger_word</td>
	<td><input type="text" name="trigger_word" value="kumabot"></td>
</tr>
</table>
<!--
<input type="submit">
-->
<input type="button" id="btn_send" value="送信">
</form>
<textarea id="result" cols="40" rows="10">
</textarea>
</body>
</html>

これで、HTMLからSlackのPOSTを想定した確認ができる。

Slackで直接動かすと、PHPでエラーが出ても無反応で何が起きてるかわかりません。
なので、上のHTMLをChromeで開いて、開発ツールでエラーが出ているか確認。エラー(500 Internal Server Error)が出てたら、ログを確認して、修正という感じでやりました。

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

*

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)