LINEBotSDKで500Errorが出ても諦めないで

Webhook URLで 500 Internal Server Error

LINEdeveloperでWebhookURLを指定しますが、ここでVERIFYを押すとサーバーへと接続確認ができます。そしてつまづく人が多いのがこのWebhookURLじゃないでしょうか。もちろん私も、
f:id:JDengineer:20170112145227p:plain

500 Internal Server Errorで詰んだ人、まだ諦めるのは早いです!

LINE developersで確認した時に500エラーが出ていても、他のところがちゃんと出来ていればBotは動きます。PCからの確認だけで実機のアプリで試してない人は今すぐ試してみてください。もしかしたらエラーが出ていても動くかも!



LINE Bot SDK tinyをコピペしてオウム返しbotを作る


実機で試してもうまくいかなかった方はどこかが間違っているので一から作ってみましょう。

前提

  • PHP 5.5.9
  • Heroku
  • Cloud9
  • LINE bot SDK 使用
    • line-bot-sdk-php:MessagingAPIをフルスタック実装したオブジェクト指向インターフェース。
    • line-bot-sdk-tiny今回はこちらのバージョンを使用。最低限のシンプルなインターフェースと機能を実装。全ての機能は実装していない。



ディレクトリ構成


  • public/
    • callback.php
    • LINEBotTiny.php
  • composer.json(composer.lock はcomposer installで自動生成)
  • Procfile

最低限この4つのファイルが必要です。では一つずつ作っていきましょう。



Herokuを動かすための必須ファイルProcfileを作成


Procfileとは、

Herokuのプラットフォーム上にあるアプリケーションのdynosにより実行されるコマンドが 何であるかを宣言するためのメカニズム。アプリケーションにおけるプロセスタイプのリストのこと
procfile · herokaijp/devcenter Wiki - GitHub

サーバーはApacheかNginxか選ぶことができます。私は今回Apacheを使用しました。

Apache

web: vendor/bin/heroku-php-apache2 public/

webプロセスを実行するとapacheサーバーが起動する。作成したファイルはpublic/に配置。

Nginx

web: vendor/bin/heroku-php-nginx


書き方

<process type>: <command>

プロセスタイプに宣言されたコマンドを実行します。 プロセスタイプは自由に命名できるが、webプロセスタイプは、HerokuのルーティングからHTTPトラフィックを受信する唯一のプロセスであるため特別です。


プロセスタイプの確認と実行

Botを使用する際はwebプロセスタイプを必ず実行させてください。そうしないと動かないです。

現在Herokuで実行しているプロセスタイプを確認

heroku ps 

webプロセスタイプを実行

heroku ps:scale web=1

webプロセスタイプを終了

heroku ps:scale web=0

任意のプロセスタイプを実行

heroku ps:scale <process type>=1

注意:Herokuは全アプリで1000時間の無料時間を分け合う制度(2016.6.1~)のため、節約したい人はこまめにプロセスタイプを終了すべし (2017.01.11現在)



HerokuでPHPを動かすためのcomposer.jsonファイルを作成


HerokuでPHPアプリケーションを動かすためにcomposer.jsonが必須です。

The Heroku PHP Support will be applied to applications only when the application has a file named composer.json in the root directory. Heroku PHP Support

しかし今回は何も書くことがないので、ファイルを作ってルートディレクトリに置いておくだけで大丈夫です。 fullバージョンのSDKを使用したい人は記述が必要です。詳しくは以下の記事で。

www.jd-enjineer.site



LINEBotTiny.phpをコピペしてpublic/ に配置


LINEBotTiny.php

<?php
/**
* Copyright 2016 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
*   https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
/*
* This polyfill of hash_equals() is a modified edition of https://github.com/indigophp/hash-compat/tree/43a19f42093a0cd2d11874dff9d891027fc42214
*
* Copyright (c) 2015 Indigo Development Team
* Released under the MIT license
* https://github.com/indigophp/hash-compat/blob/43a19f42093a0cd2d11874dff9d891027fc42214/LICENSE
*/
if (!function_exists('hash_equals')) {
    defined('USE_MB_STRING') or define('USE_MB_STRING', function_exists('mb_strlen'));
    function hash_equals($knownString, $userString)
    {
        $strlen = function ($string) {
            if (USE_MB_STRING) {
                return mb_strlen($string, '8bit');
            }
            return strlen($string);
        };
        // Compare string lengths
        if (($length = $strlen($knownString)) !== $strlen($userString)) {
            return false;
        }
        $diff = 0;
        // Calculate differences
        for ($i = 0; $i < $length; $i++) {
            $diff |= ord($knownString[$i]) ^ ord($userString[$i]);
        }
        return $diff === 0;
    }
}
class LINEBotTiny
{
    public function __construct($channelAccessToken, $channelSecret)
    {
        $this->channelAccessToken = $channelAccessToken;
        $this->channelSecret = $channelSecret;
    }
    public function parseEvents()
    {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            http_response_code(405);
            error_log("Method not allowed");
            exit();
        }
        $entityBody = file_get_contents('php://input');
        if (strlen($entityBody) === 0) {
            http_response_code(400);
            error_log("Missing request body");
            exit();
        }
        if (!hash_equals($this->sign($entityBody), $_SERVER['HTTP_X_LINE_SIGNATURE'])) {
            http_response_code(400);
            error_log("Invalid signature value");
            exit();
        }
        $data = json_decode($entityBody, true);
        if (!isset($data['events'])) {
            http_response_code(400);
            error_log("Invalid request body: missing events property");
            exit();
        }
        return $data['events'];
    }
    public function replyMessage($message)
    {
        $header = array(
            "Content-Type: application/json",
            'Authorization: Bearer ' . $this->channelAccessToken,
        );
        $context = stream_context_create(array(
            "http" => array(
                "method" => "POST",
                "header" => implode("\r\n", $header),
                "content" => json_encode($message),
            ),
        ));
        $response = file_get_contents('https://api.line.me/v2/bot/message/reply', false, $context);
        if (strpos($http_response_header[0], '200') === false) {
            http_response_code(500);
            error_log("Request failed: " . $response);
        }
    }
    private function sign($body)
    {
        $hash = hash_hmac('sha256', $body, $this->channelSecret, true);
        $signature = base64_encode($hash);
        return $signature;
    }
}



SDKを使ったbot制御用のPHPファイル(callback.php)の作成


<?php
define("CHANNEL_ACCESS_TOKEN", "<your access token>");
define("CHANNEL_SECRET", "<your channel secret>");

require_once("./LINEBotTiny.php");

$client = new LINEBotTiny(CHANNEL_ACCESS_TOKEN, CHANNEL_SECRET);

foreach ($client->parseEvents() as $event) {
    switch ($event['type']) {
        case 'message':
            $message = $event['message'];
            switch ($message['type']) {
                case 'text':
                    $client->replyMessage(array(
                        'replyToken' => $event['replyToken'],
                        'messages' => array(
                            array(
                                'type' => 'text',
                                'text' => $message['text'].'ですね。’ // ここを変更すれば返信のメッセージが変わる
                            )
                        )
                    ));
                    break;
                default:
                    error_log("Unsupporeted message type: " . $message['type']);
                    break;
            }
            break;
        default:
            error_log("Unsupporeted event type: " . $event['type']);
            break;
    }
};

SDKのサンプルほぼそのままです。 echo_bot.php

ここまで間違えずに出来たら500エラーが出ていてもBotは動きます!

f:id:JDengineer:20170112151131p:plain



なぜ500エラーになったの?


LINEBotTiny.phpのこのコードか?

$response = file_get_contents('https://api.line.me/v2/bot/message/reply', false, $context);
        if (strpos($http_response_header[0], '200') === false) {
            http_response_code(500);
            error_log("Request failed: " . $response);
        }

それとも、制御用ファイルがメッセージタイプのイベントしか受け付けていないことが原因か? もしわかった方がいたら教えてください。

まだまだわからないことがたくさんで勉強不足を感じますが、返信がくると面白いですので、まずは動かして返事が来ることを目標に試してみてください!