【Laravel】チャットワークAPI+ウェブフックで在庫数・確認ボットをつくる

さてさて、この間LaravelでLINEにチャットボットをつくるという記事では、「送信したテキストをQRコードにして返信する」というシステムをつくってみました。

ちなみに、LINEはプライベート用として長年使わせてもらっていて、その影響から、このブログで使っているイラストのLINEスタンプを販売していたりします。(後発だったこともあり、まったく売れてませんが・・・😅)

では、お仕事用は・・・??というと、メインでチャットワークを利用させてもらっています。私がLinuxを使っている理由に通ずるところがあるのですが、「必要最低限だけ」というところがとても気にいっています。

そして、前述したLINEボットを作成したあとで、チャットワークにウェブフックがあるか確認してみたところ、バッチリ存在していましたので今回はちょっとお仕事用に使えそうな機能を考えてみました。

それは、ズバリ!

営業さんが出先で在庫数をすぐ確認できる

というものです。

きっと、今回の記事を応用すれば色々なことが実現できると思います。
ぜひ皆さんのお役に立てると嬉しいです😊✨

開発環境: Laravel 5.8

チャットワークのウェブフックを使えるようにする

ウェブフック用のチャットルームをつくる

メインのチャットルームを使ってもいいのですが、今回はウェブフックのテストですので、専用のルームを作っておきましょう。

まず、ログインして画面左上にある「+」ボタンをクリック。するとポップアップが表示されるので、その中の「グループチャットを新規作成」をクリックします。

次にグループチャットの内容を入力するフォームが表示されるので、以下のように入力して「作成」ボタンをクリックしてください。

これで新しいグループチャットが作成されることになりますが、この作成されたチャットルームのIDがすぐ後で必要になるので、取得しておきましょう。IDはURLに含まれています。

以下のように「rid」の後に続く数字をコピーしておいてください。

ウェブフックを登録する

では、Webhookの登録ページにアクセスして「新規作成」ボタンをクリックします。

するとウェブフックに関するフォームが表示されますので、以下のように入力して「作成」ボタンをクリックします。(※ ここで先ほどのルームIDが必要になります)

※ なお、Webhook URLは、https://*******/chatwork_bot/stockになりますので、適宜ドメイン部分を変更して登録してください。

登録ができたら、以下のようにトークンを取得することができますので、先ほどのルームIDと一緒にLaravelの.envに書き込んでおきましょう。

(.envの例)

CHATWORK_WEBHOOK_ROOM_ID=(先ほど設定したルームID)
CHATWORK_WEBHOOK_TOKEN=(あなたのウェブフック・トークン)

チャットワークAPIが使えるようにする

チャットワークのウェブフックは受信のみで、メッセージ送信するにはAPIが必要になります。

そのため、このAPIにアクセスするためのトークンも取得しておきましょう。

まず、API Tokenのページにアクセスします。
するとパスワードを求められますので入力してボタンをクリックしてください。

すると、APIトークンが表示されますので、これもLaravel.envに追加しておきましょう。

CHATWORK_API_TOKEN=(あなたのAPIトークン)

Laravel側の作業

ルートをつくる

まずはじめにChatworkのウェブフックがアクセスしてくるURLが必要ですので、routes/web.phpに以下のルートを追加します。

Route::get('chatwork_bot/stock', 'ChatworkBotController@stock');

モデルとマイグレーションをつくる

次に、商品データを管理するモデル「Product」をつくります。
以下のコマンドを実行してください。

php artisan make:model Product -m

-mオプションをつけていると、モデルだけでなくマイグレーションも一緒に作成してくれます。database/migrations/****_**_**_******.phpを開いて中身を以下のように変更してください。

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateProductsTable extends Migration
{
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->integer('stock')->default(0);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('products');
    }
}

変更が終わったら以下のコマンドでマイグレーションを実行します。

php artisan migrate

実行するとテーブルは以下のようになります。

Seederでテストデータをつくる

続いて、テストデータを作ってproductsテーブルにデータを追加しましょう。

php artisan make:seed ProductsTableSeeder

すると、database/seeds/ProductsTableSeeder.phpというファイルが作成されるので、中身を以下のように変更してください。

<?php

use Illuminate\Database\Seeder;

class ProductsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $products = [
            ['code' => 'CUV001', 'name' => 'からだよろこぶ粉チーズ'],
            ['code' => 'FJV002', 'name' => 'デラックスお酢'],
            ['code' => 'ACI003', 'name' => '純正ミネラルウォーター'],
            ['code' => 'FOQ004', 'name' => '大自然の抹茶'],
            ['code' => 'EIN005', 'name' => 'ミルキー豆乳'],
            ['code' => 'KMV006', 'name' => '熟成塩'],
            ['code' => 'MSW007', 'name' => '極上緑茶'],
            ['code' => 'GKW008', 'name' => 'フレッシュチョコレート'],
            ['code' => 'FNO009', 'name' => '健やかお茶'],
            ['code' => 'EFQ010', 'name' => 'プレミアムチーズ'],
            ['code' => 'FXZ011', 'name' => 'オリジナルコーヒー'],
            ['code' => 'MQU012', 'name' => 'からだにやさしい紅茶'],
            ['code' => 'JQS013', 'name' => '最高級ウーロン茶'],
            ['code' => 'AEH014', 'name' => 'みんなのガーリック'],
            ['code' => 'IRS015', 'name' => '今までなかったオリーブオイル'],
            ['code' => 'BMN016', 'name' => 'エクストラコーヒー牛乳'],
            ['code' => 'FJT017', 'name' => '健康マーガリン'],
            ['code' => 'BEQ018', 'name' => '極旨バター'],
            ['code' => 'FKO019', 'name' => '大自然がくれたジャム'],
            ['code' => 'BFW020', 'name' => 'デリシャススライスチーズ'],
            ['code' => 'AJY021', 'name' => 'ヘルシー牛乳'],
            ['code' => 'DNS022', 'name' => '徳用チョコレート'],
            ['code' => 'CHN023', 'name' => '奇跡のコーヒー'],
            ['code' => 'PTW024', 'name' => 'おいしい粉チーズ'],
            ['code' => 'LOZ025', 'name' => '高級豆乳'],
            ['code' => 'JQT026', 'name' => '天然マーガリン'],
            ['code' => 'BMZ027', 'name' => '料理用チョコレート'],
            ['code' => 'MVW028', 'name' => 'デリシャスミネラルウォーター'],
            ['code' => 'BKY029', 'name' => 'プレミアムチョコレート'],
            ['code' => 'ACI030', 'name' => 'からだよろこぶチョコレート']
        ];

        foreach ($products as $product) {

            \App\Product::insert([
                'code' => $product['code'],
                'name' => $product['name'],
                'stock' => rand(0, 100),
                'created_at' => now(),
                'updated_at' => now()
            ]);

        }

    }
}

※ なお、これらのダミーデータはSmartDataさんのサンプルを使わせていただきました。ありがとうございます😊✨

そして、作成したProductsTableSeederdatabase/seeds/DatabaseSeeder.phpに登録しておきましょう。(太字が追加した部分です)

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
         //$this->call(UsersTableSeeder::class);
         $this->call(ProductsTableSeeder::class);
    }
}

これで以下のコマンドを実行するとテストデータが作成されることになります。

php artisan db:seed

テーブル内は以下のようになりました。

コントローラーをつくる

最後にコントローラーをつくります。
以下のコマンドを実行してください。

php artisan make:controller ChatworkBotController

コマンドを実行するとapp/Http/Controllers/ChatworkBotController.phpが作成されているので、中身を以下のように変更してください。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Str;

class ChatworkBotController extends Controller
{
    public function stock(Request $request) {

        // チャットワークからの送信を検証
        $token = env('CHATWORK_WEBHOOK_TOKEN');
        $secret_key = base64_decode($token);
        $request_body = $request->getContent();
        $digest = hash_hmac('sha256', $request_body, $secret_key, true);
        $signature = base64_encode($digest);

        if($signature === $request->header('X-ChatWorkWebhookSignature')) { // 検証成功

            $api_key = env('CHATWORK_API_TOKEN');
            $room_id = env('CHATWORK_WEBHOOK_ROOM_ID');
            $webhook_data = json_decode($request_body, true);
            $message = $webhook_data['webhook_event']['body'];
            $reply_prefix = '【ボットからの返信:在庫数】';

            if(!empty($message) && !Str::startsWith($message, $reply_prefix)) {  // 【重要!】ここで無限ループを回避

                // 商品を検索
                $keywords = explode(' ', trim(mb_convert_kana($message, 's')));
                $query = \App\Product::select('name', 'stock')->take(25);

                foreach($keywords as $keyword) {

                    $query->where('name', 'LIKE', '%'. $keyword .'%');  // AND検索

                }

                $products = $query->get();
                $reply_message = $reply_prefix ."\n";

                if($products->count() > 0) {

                    foreach ($products as $product) {

                        $reply_message .= $product->name .': '. $product->stock ."\n";

                    }

                } else {

                    $reply_message .= '該当する商品が見つかりませんでした。';

                }

                // 検索結果を返信
                $params = ['body' => $reply_message];
                $ch = curl_init();
                curl_setopt($ch, CURLOPT_URL, 'https://api.chatwork.com/v2/rooms/'. $room_id .'/messages');
                curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-ChatWorkToken: '. $api_key]);
                curl_setopt($ch, CURLOPT_POST, 1);
                curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params, '', '&'));
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_exec($ch);
                curl_close ($ch);

            }

        }

        return response('OK', 200);
    }
}

この中で一番大事なのは、なんといっても「無限ループを回避する」部分です。

というのも、ボットを作成するには、まず以下の流れが必要になってきます。

  1. ウェブフックを受け取り、テキストを取得
  2. そのテキストでデータ検索
  3. 検索結果をAPIを使ってメッセージ送信

ところが、この流れで行くと「3番のメッセージ送信ですら再度ウェブフックを引き起こしてしまう」のです。つまり、無限ループが発生してしまい、これ止めるにはapachenginxのウェブサーバーをを再起動するしかなくなってしまうというわけです。(ちなみに、負荷があまりに大きいとチャットワークの方でウェブフックを無効にしてくれるそうです)

しかも、ウェブフックの中身からAPIかどうかを判別をしようにも同じユーザーIDを使って送信してくるので、結局はボットの返信には固定の文章(コード内で言うと【ボットからの返信:在庫数】)をつけ、もし取得テキストにこの文字が含まれていれば、無限ループしてしまうので処理をスキップする、というようにしています。(もしかすると、何か解決法があるのかもしれませんが、この方法しか無いとするとちょっとこのAPIは使いにくいかもしれません😅)

CSRF(クロスサイトリクエストフォージェリ)ミドルウェアを解除する

LINEボットを作成したときと同じくCSRFミドルウェアを解除しないとPOST送信を受け取ることは出来ないのでapp/Http/Middleware/VerifyCsrfToken.phpを開いて以下のように開示します。

※ 詳しい内容については、LaravelでLINEにチャットボットをつくる「CSRF対策を解除する」をご覧ください。

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
     * Indicates whether the XSRF-TOKEN cookie should be set on the response.
     *
     * @var bool
     */
    protected $addHttpCookie = true;

    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        'chatwork_bot/stock'
    ];
}

テストしてみる

では、準備が整ったので実際にテストしてみましょう!

送信する内容は「チーズ」です。

結果はこちら↓↓↓

はい!
Seederで用意したデータの中から「チーズ」を含む商品とその在庫数を返信してくれました。

成功です😊

また、もっと絞り込むために「チーズ おいしい」を送信してみます。

結果はこちら↓↓↓

こっちも絞り込みした結果が返ってきました。

お疲れ様でした〜😊✨

おわりに

今回チャットワークのウェブフックとAPIを使ってみましたが、「LINEのチャットボットを作るのとほぼ同じだな」と感じました。

というのも、チャットワークからの送信を検証する部分では、base64_encode()や、hash_hmac()を使って署名を比較する形になっていますが、これはLINEと同じでした。

ただ、LINEと違うのは記事中でも書いた「無限ループ」が発生してしまう可能性です。

LINEの場合は「line-bot-sdk」というパッケージを用意してくれていて、この中に返信するためのメソッドを用意してくれているので、このパッケージを使う限りは無限ループが発生することは、ほぼないと思いました。

せめてAPIからの送信かどうかがわかるフラグがあればいいのですが・・・(もしかしたら、すでに対策されているかもしれないので、また時間ができたらチェックしてみます or 知ってたら教えてください m_ _m)

今回は以上です。

ではでは〜!

この記事が役立ちましたらシェアお願いします😊✨