「SlackBotにまじめに取り組む」の1回目
#この記事は
いままで、旧タイプのレガシートークンでIncoming Webhookを使ってSlackと連携していました。少し前にSlackのAppの仕様が変わったこともあって、レガシートークンではなくSlack Appの仕組みをちゃんと勉強してみることにしました。
今回は、Slack AppにあるIncoming Webhookとslash commandまでやってみます。Event APIは次の機会で。
##今回使う環境
- Heroku
- PHP 7.3
- Laravel 6.0.3
- Clear DB
- Windows
- PHP 7.2.11
- Laravel 6.0.3
- Maria DB Ver 15.1
いつもどおり、基本的にはWindowsでの作業です。
Slash CommandでSlackからの呼びかけを受け取るために、インターネット上にHTTP/Sのサーバーを立てる必要があります。今回は、Laravel6系をHeroku上にデプロイして試しますが、Laravel6の新機能要素はゼロです。
#まず下準備
##Laravel 6 をインストール
C:\slalack-bot
composer create-project laravel/laravel --prefer-dist .
php artisan -V
Laravel Framework 6.0.3
#動作確認
php artisan serve
http://127.0.0.1:8000/
#いつもの画面が出ることを確認
##Laravel をHerokuにデプロイ
C:\slalack-botにProcfile
を作る
web: vendor/bin/heroku-php-apache2 public/
Heroku上にアプリを定義してデプロイ
アプリ名はslalack-bot
#Heroku app 作成
heroku create slalack-bot
https://slalack-bot.herokuapp.com/ | https://git.heroku.com/slalack-bot.git
git init
Initialized empty Git repository in C:/slackbot/.git/
heroku git:remote -a https://git.heroku.com/slalack-bot.git
set git remote heroku to https://git.heroku.com/slalack-bot.git
git commit -m "application init"
git push heroku master
Laravelの.env
の必要最低限をHerokuのconfigへ
heroku config:set APP_NAME="slalack-bot"
heroku config:set APP_ENV="production"
heroku config:set APP_KEY="base64:GlmfutBxM/hJJBdNuQyI267djR55srF15nJaoLV2QZo="
heroku config:set APP_DEBUG="true"
heroku config:set APP_URL="https://slalack-bot.herokuapp.com/"
https://slalack-bot.herokuapp.com/ にアクセスして、Laravelの画面が出ることを確認
#SlackにAppを登録
Slackに独自のワークスペースがある前提で進めます。
ブラウザでSlackのAPIページへいく
https://api.slack.com/apps?new_app=1
新規App画面が出るので、適当に入力し```Create App``ボタンを押す
AppNameはあとで変えられるので、適当につけておく。いったんこんな感じで。
##incoming webhookを試す
まずは、独自Appの動作確認のためにincoming webhook
を試してみます。ひと昔前はWebhookが単独でメニュー化されていましたが、最近は自分で定義したAppの一部に組み込まれるようになりました。
Webhookは作成時にChannelを指定します。昔はメッセージを送るときに無差別に送るチャンネルを変更できましたが、最近は推奨されていないです。複数チャンネルに送りたい場合には、この手順を繰り返しチャンネルごとにWebhookを用意しないといけません。
webhookを登録すると、こんな感じにメッセージの送信先URLが発行されます。
このURLを誰かに知られるとワークスペースにメッセージ送られ放題になるので、気を付けましょう。
curlコマンドのワンライナーもコピーできますが、Windowsのコマンドプロンプトではシングルクオートの囲みがエラーになるので、ダブルクオートに書き換えます。("の中の"は\でエスケープも)
curl -X POST -H "Content-type: application/json" --data "{\"text\":\"HelloWorld\"}" https://hooks.slack.com/services/aaa/bbb/ccc
##Slash Commandを試す
本命のスラッシュコマンドをやります。
###1.コマンドの受信用のHTTP/Sアプリを作る
artisanコマンドでコントローラを作り
php artisan make:controller SlalackBotController
Controller created successfully.
最初はdebugメソッドを用意。
内容は、(Slackから)受け取ったパラメータを全部ログに書き出しつつ、簡単なメッセージだけ返却するだけです。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Log;
class SlalackBotController extends Controller
{
public function debug(Request $request){
\Log::info($request->all());
$date = date('Y-m-d H:i:s');
$responseText = 'debug method are called at ' . $date;
return response()->json(['text'=>$responseText]);
}
}
Slash CommandのレスポンスはJSONで戻す必要があります。 Laravelのresponse()->json()
を使うと、PHPの配列を自動でJSONにしてくれて、レスポンスヘッダーにapplication/json
が自動で付与されるので便利です。
https://readouble.com/laravel/5.5/ja/responses.html#json-responses
###2.routeの設定
web.phpではなくてapi.phpに登録します。この場合、api/debug
がエンドポイントになります
Route::post('/debug', 'SlalackBotController@debug');
###3.ログの設定
HerokuにデプロイしたLaravelのログを見るのは面倒なので、SlackにLogを送ってしまう。最初に作ったIncoming Webhookにログが送られるようにします。(ここは必須ではないです)
'channels' => [
'stack' => [
'driver' => 'stack',
#slackを追加
'channels' => ['daily','slack'],
'ignore_exceptions' => false,
],
.envファイルにWebhookのURLを設定する
LOG_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/aaa/bbb/ccc
Heroku側にも設定を追加
heroku config:set LOG_SLACK_WEBHOOK_URL="https://hooks.slack.com/services/aaa/bbb/ccc"
###4. 変更した内容をHerokuへデプロイ
git add .
git commit -m "log to slack"
git push heroku master
これで、https://slalack-bot.herokuapp.com/api/debug でPOSTを受けられるようになります。
###5.Slash Command として登録
Slack AppのBasicInformationからSlach Commandsへ
Create New CommandからSlashコマンドの設定をする
Request URLは、先ほど作ったhttps://slalack-bot.herokuapp.com/api/debug
にしています。ここは各自環境により変わります。それ以外の部分はおまかせです。
これでいったん準備完了です。
Slash Commandの登録完了したときに、permissionが変わったから再インストールするようにメッセージが出たら、メッセージに従って作業してください。
###6.いざ実行
Slackを開いて、スラッシュで呼びかけてみます
正しく設定されていると、Botの裏で動いているLaravelのLogがSlackのWebhookにも届きます
スラコンのお試し、無事完了。
ログを見てみると、スラッシュコマンドに書いたメッセージは、Laravelの$Request
の中のtext
から取れるってわけですね。PostメソッドでJSONが送られているはずなのに、LaravelではRequestのインスタンスで透過的に取れるようにしてくれます。
##Slash Commandを試す(2)
Slash Commandは3秒以内に200のレスポンスを返すことが求められています。もし、処理に時間がかかって返せない場合には、Slack CommandのPOST内容に含まれているresponse_url
を覚えておいて、あとからHTTPでPOSTすれば、Slash Commandを送ってきた相手にメッセージが返せます。
URLの有効期限は30分以内、メッセージを送るのは5回までと決まっています。
ということで、response_url
を使うバージョンを試してみました。
###1.Guzzleをインストール
LaravelからHTTPSアクションを起こすので、Guzzleをインストール
composer require guzzlehttp/guzzle
###2.コントローラにdelayメソッドを追加
Guzzleクライアントは、json
という名前のキーでPHPの配列を渡すと、自動的にリクエストヘッダーにapplication/json
をセットし、BodyにJSON文字列を渡してくれるので、実装がとても簡単です。
http://docs.guzzlephp.org/en/stable/request-options.html#json
public function delay(Request $request){
$responseUrl = $request->input('response_url');
$text = $request->input('text');
\Log::debug($responseUrl);
\Log::debug($text);
$guzzle = new \GuzzleHttp\Client();
$option = [];
$option['debug']=false;
$option['verify']=false;
$option['http_errors']=false;
##5回以上はエラーになるはずなので6回
for( $i=0; $i<6; $i++){
$option['json']=['text'=> "$text : reply from bot ($i)" ];
$response = $guzzle->post($responseUrl, $option);
$body = $response->getBody();
$data = (String)$body;
if( $response->getStatusCode() != 200 ){
\Log::error($data);
break;
}
}
$date = date('Y-m-d H:i:s');
$responseText = 'Direct reply at ' . $date;
return response()->json(['text'=>$responseText]);
}
上で作ったdelayメソッドAPIとして登録する。
Route::post('/delay', 'SlalackBotController@delay');
###3.Slack Appに追加
delayメソッドを新しくスラッシュコマンドに追加で登録します
6回目の呼び出しは404のエラーになり、Log::errorで書き出ししたものもSlackに無事到着。その後にDirectなレスポンスも届きました。
#Laravelらしく、Queueに入れてから処理する
ここまでの実験で、何をやる必要があるかは理解できました。
せっかくLaraveで作っているので、LaravelのQueueの仕組みを使ってJobを実行して、Slackに返信を行うようにしてみます。
やることは、
- Slashコマンドを受ったら、メッセージの内容をDBに保存し
- Laravelのキューに入れ、Slackにはいったんメッセージを返す
- キューに入ったジョブを実行し、Slackを呼び出しした人へ返答する
##1.データベース設定
Slackからのデータを一時的に保存する必要があるので、HerokuでClearDBのAddonを入れます。あわせてClearDBの接続パラメータをHerokuのConfigに必要なパラメータを設定しておきます。
heroku config:set DB_CONNECTION="mysql"
heroku config:set DB_HOST="us-cdbr-iron-east-02.cleardb.net"
heroku config:set DB_PORT="3306"
heroku config:set DB_DATABASE="heroku_xxxxx"
heroku config:set DB_USERNAME="bd048xxxxxx"
heroku config:set DB_PASSWORD="xxxx"
#これも必要なので一緒に
heroku config:set QUEUE_CONNECTION="database"
##2.Job Queueの準備
Jobを保存する仕組みを構築します。Laravelの基本的な仕組みをそのまま使います。
###必要なテーブルの準備
php artisan queue:table
Migration created successfully!
database/migrationsにファイルができます。不要なファイルもできるので消しておきます。
- 2014_10_12_000000_create_users_table.php (いらない)
- 2014_10_12_100000_create_password_resets_table.php (いらない)
- 2019_08_19_000000_create_failed_jobs_table.php
- 2019_09_22_215540_create_jobs_table.php
MySQLを使っている場合で、jobsテーブルのup
メソッドのqueue
カラムで以下のエラーが出たら、migrationのファイルを編集して凌ぎます。
Illuminate\Database\QueryException : SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table `jobs` add index `jobs_queue_index`(`queue`))
今回は、indexを作らないように修正して避ける
public function up()
{
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
//ここ編集
//$table->string('queue')->index();
$table->string('queue');
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
###Jobクラス作成
php artisan make:job ProcessSlackMessage
Job created successfully.
Jobs/ProcessSlackMessage.phpが作られるので、Slackにメッセージを送るように実装をします。
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Log;
class ProcessSlackMessage implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $slackMessage = null;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(\App\SlackMessage $slackMessage)
{
//
$this->slackMessage = $slackMessage;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$id = $this->slackMessage->id;
\Log::debug('handling job ' . $id);
$guzzle = new \GuzzleHttp\Client();
$option = [];
$option['debug']=false;
$option['verify']=false;
$option['http_errors']=false;
$message = 'ID:' . $id . "\n" . $this->slackMessage->text . 'への返信です';
$option['json']=['text'=> $message ];
$response = $guzzle->post($this->slackMessage->response_url, $option);
$body = $response->getBody();
$data = (String)$body;
if( $response->getStatusCode() != 200 ){
\Log::error($data);
}else{
$this->slackMessage->delete();
\Log::debug('SlackMessage ID ' . $id .' was deleted.');
}
}
}
コンストラクタでSlackMessageを受けとるようにしておくと、Queueに入れるときに関連づけたSlackMessageのインスタンスが渡ってきます。あとはhandle
メソッドで、SlackMessageに入っている情報でお仕事をし、SlackのAPIをコールすると返答ができます。
##3. SlackMessageのモデル作成
SlackのAPIで渡された値をそっくりそのまま入れておくテーブルを作ります
###migrationファイルを作成
php artisan make:migration create_slack_messages_table
Created Migration: 2019_09_22_221816_create_slack_messages_table
database/migrations/2019_09_22_221816_create_slack_messages_table.phpが作られる。upメソッド
を以下のように実装して、テーブルが作られるようにする。
public function up()
{
Schema::create('slack_messages', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('token');
$table->string('team_id');
$table->string('team_domain');
$table->string('channel_id');
$table->string('channel_name');
$table->string('user_id');
$table->string('user_name');
$table->string('command');
$table->string('text');
$table->string('response_url');
$table->string('trigger_id');
$table->timestamps();
});
}
###SlackMessageモデルを作成
php artisan make:model SlackMessage
Model created successfully.
SlackMessageモデルクラスはfillable
にカラム名を宣言するだけ
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class SlackMessage extends Model
{
protected $fillable = [
'token', 'team_id', 'team_domain', 'channel_id', 'channel_name',
'user_id','user_name','command','text','response_url','trigger_id'
];
}
##4.Slackから受け取るAPI
SlalackBotControllerにqueueメソッドを作ります。Slackからのリクエスト内容をSlackMessageテーブルに保存してJobへDispatchする。5行だけとは。
public function queue(Request $request){
$slackMessage = new \App\SlackMessage();
$slackMessage->fill($request->all());
$slackMessage->save();
\App\Jobs\ProcessSlackMessage::dispatch($slackMessage);
return response()->json(['text'=>'saved ' . $slackMessage->id]);
}
APIとして呼び出せるよう設定
Route::post('/queue', 'SlalackBotController@queue');
###Slack Appに登録
Appに新たにスラッシュコマンド/queue
を登録します。もう手慣れた感じです。
##5.Herokuにデプロイ
###GIT
git add .
git commit -m "implements queue"
git push heroku master
###テーブルのmigration
heroku run php artisan migrate
Running php artisan migrate on ⬢ slalack-bot... up, run.5569 (Free)
**************************************
* Application In Production! *
**************************************
Do you really wish to run this command? (yes/no) [no]:
> yes
Migration table created successfully.
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated: 2019_08_19_000000_create_failed_jobs_table (0 seconds)
Migrating: 2019_09_22_215540_create_jobs_table
Migrated: 2019_09_22_215540_create_jobs_table (0 seconds)
Migrating: 2019_09_22_221816_create_slack_messages_table
Migrated: 2019_09_22_221816_create_slack_messages_table (0.01 seconds)
###テーブル確認
Windowsからmysqlコマンドで接続
mysql -u (user) -p(pass) -h xxx-cleardb.net heroku_74cea6efa72d039
MySQL [heroku_74cea6efa72d039]> show tables;
+----------------------------------+
| Tables_in_heroku_74cea6efa72d039 |
+----------------------------------+
| failed_jobs |
| jobs |
| migrations |
| slack_messages |
+----------------------------------+
4 rows in set (0.18 sec)
MySQL [heroku_74cea6efa72d039]> desc jobs;
+--------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+---------------------+------+-----+---------+----------------+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| queue | varchar(255) | NO | | NULL | |
| payload | longtext | NO | | NULL | |
| attempts | tinyint(3) unsigned | NO | | NULL | |
| reserved_at | int(10) unsigned | YES | | NULL | |
| available_at | int(10) unsigned | NO | | NULL | |
| created_at | int(10) unsigned | NO | | NULL | |
+--------------+---------------------+------+-----+---------+----------------+
7 rows in set (0.18 sec)
MySQL [heroku_74cea6efa72d039]> desc failed_jobs;
+------------+---------------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------------------+------+-----+-------------------+----------------+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| connection | text | NO | | NULL | |
| queue | text | NO | | NULL | |
| payload | longtext | NO | | NULL | |
| exception | longtext | NO | | NULL | |
| failed_at | timestamp | NO | | CURRENT_TIMESTAMP | |
+------------+---------------------+------+-----+-------------------+----------------+
6 rows in set (0.18 sec)
MySQL [heroku_74cea6efa72d039]> desc slack_messages;
+--------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+---------------------+------+-----+---------+----------------+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| token | varchar(255) | NO | | NULL | |
| team_id | varchar(255) | NO | | NULL | |
| team_domain | varchar(255) | NO | | NULL | |
| channel_id | varchar(255) | NO | | NULL | |
| channel_name | varchar(255) | NO | | NULL | |
| user_id | varchar(255) | NO | | NULL | |
| user_name | varchar(255) | NO | | NULL | |
| command | varchar(255) | NO | | NULL | |
| text | varchar(255) | NO | | NULL | |
| response_url | varchar(255) | NO | | NULL | |
| trigger_id | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+--------------+---------------------+------+-----+---------+----------------+
14 rows in set (0.18 sec)
##6.いざ実行
id=42として登録したようだ。
###データ確認
slack_messagesテーブル
id=42を検索
MySQL [heroku_74cea6efa72d039]> select * from slack_messages where id=42\G
*************************** 1. row ***************************
id: 42
token: 1YxL3GCEx7R9CFgal0LvIDpK
team_id: TDWRKFV0S
team_domain: kanaxx
channel_id: CDWNT347M
channel_name: channel2
user_id: UDX2143CM
user_name: kanaxx-user
command: /queue
text: キューへ入れー!!
response_url: https://hooks.slack.com/commands/TDWRKFV0S/756504982386/NMTbFokC89DGibmiqvTrxnaY
trigger_id: 762861257665.472869539026.78157a4a62d1c306e2fd26f2c6c27257
created_at: 2019-09-23 11:12:34
updated_at: 2019-09-23 11:12:34
1 row in set (0.19 sec)
入ってる!
jobsテーブル
jobsテーブルも確認
MySQL [heroku_74cea6efa72d039]> select * from jobs\G
*************************** 1. row ***************************
id: 32
queue: default
payload: {"displayName":"App\\Jobs\\ProcessSlackMessage","job":"Illuminate\\Queue\\CallQueuedHandler@call","maxTries":null,"delay":null,"timeout":null,"timeoutAt":null,"data":{"commandName":"App\\Jobs\\ProcessSlackMessage","command":"O:28:\"App\\Jobs\\ProcessSlackMessage\":9:{s:15:\"\u0000*\u0000slackMessage\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":4:{s:5:\"class\";s:16:\"App\\SlackMessage\";s:2:\"id\";i:42;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";}s:6:\"\u0000*\u0000job\";N;s:10:\"connection\";N;s:5:\"queue\";N;s:15:\"chainConnection\";N;s:10:\"chainQueue\";N;s:5:\"delay\";N;s:10:\"middleware\";a:0:{}s:7:\"chained\";a:0:{}}"}}
attempts: 0
reserved_at: NULL
available_at: 1569204754
created_at: 1569204754
1 row in set (0.19 sec)
よくわからないけど、1個だけ何か入ってる!
###Queueの処理
Queueに溜まっているJobを一つ処理してみます。artisan
のqueue:work
で実行します。
heroku run php artisan queue:work --once
Running php artisan queue:work --once on ⬢ slalack-bot... up, run.7817 (Free)
[2019-09-23 11:17:49][32] Processing: App\Jobs\ProcessSlackMessage
[2019-09-23 11:17:49][32] Processed: App\Jobs\ProcessSlackMessage
無事終了。
うまくいくと、この後にSlackにメッセージが届きます。
届いた!
本番環境では、php artisan queue:work
でバックグラウンド処理をするか、ジョブスケジューラなどから、定期的にartisanコマンドを叩く必要があります。
https://readouble.com/laravel/6.0/ja/queues.html#running-the-queue-worker
###データの再確認
QueueとSlackMessageを処理したあとのデータの確認
MySQL [heroku_74cea6efa72d039]> select * from slack_messages where id=42\G
Empty set (0.20 sec)
MySQL [heroku_74cea6efa72d039]> select * from jobs\G
Empty set (0.20 sec)
無事、両方とも空っぽですね。すばらしい
#まとめ
SlackのAPIの中で、Webhookで何かを一方的に送り付ける方法と、Slashコマンドできっかけをユーザからもらって反応する方法を試しました。まぁ、返答している内容がイマイチなので、もうちょっと楽しくしたいところです。本当は、SlackコマンドをもらってからHistoryを読み出したり、@コマンドで話しかけられたいのですが、それはもうちょっと勉強と実験が必要なようです。
Queueに入れて遅延実行がこんなに簡単にできるとは思わなかった。今回分かったことは、やっぱりLaravelが便利だということです。
#参考資料
Slack Botの種類と大まかな作り方
https://qiita.com/namutaka/items/233a83100c94af033575
Slack API - Slash Commands
https://api.slack.com/slash-commands
MySQLのインデックスサイズに767byteまでしかつかえない問題と対策
https://blog.e2info.co.jp/2017/04/17/mysql%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AB767byte%E3%81%BE%E3%81%A7%E3%81%97%E3%81%8B%E3%81%A4%E3%81%8B%E3%81%88%E3%81%AA%E3%81%84/