「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メソッドを新しくスラッシュコマンドに追加で登録します
4.いざ実行
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/













