るいすときのこの物語

オタクエンジニアの雑記

【PHP】LINE Messaging APIで田村ゆかりさんの公式サイト更新通知BOTを作りなおした #yukarin


従来のLINE BOT APIが廃止予定ということで 新たに発表されたLINE Messaging APIに移行しました。

 

line

作ったものはこんな感じ普通です。 LINE Messaging API は管理画面から背景色とかアカウント設定できる項目はめっちゃ増えてて LINE BOT APIよりは個性的なものが作れそう(^o^)

qr 田村ゆかりさんに興味がある人、待ってます\(^o^)/

 

 

LINE Messaging API

誰でも手軽に叩けるAPIです。 BOTも作成できるし、管理画面も用意されてて分かりやすい。

無料で作成するBOTはユーザーの発言に対しての返信はできる(REPLY_MESSAGE) が BOTから直接メッセージを送信(PUSH_MESSAGE)するのは月2万ぐらいかかる。

しかし友達登録上限50人という制限付きBOTでは REPLY_MESSAGE & PUSH_MESSAGE どちらも使える。

従来のLINE BOT APIと変わったことはそんなことなくて 移行難易度はそんなに高くないと思います。

 

Webhookの設定

今回実装にあたって少しだけハマったところは Webhookの設定で、CloudflareのSSL機能を使ったサーバーを指定すると Webhookが飛んでこないという仕様だった。

LINE BOT APIでは使えたんですけどね、いずれか使えるようにはなりそう。 ちなみにLet's Encryptでは可能です。

 

 

実装

今回もPHPです。 特に理由はありません。楽だからです。

 

callback

<?php
require_once './lib/callback_class.php';

/* LINE */
$CHANNEL_SECRET = '';
$CHANNEL_ACCESS_TOKEN = '';

/* TWITTER */
$CONSUMER_KEY = '';
$CONSUMER_SECRET = "";
$ACCESS_TOKEN = "";
$ACCESS_TOKEN_SECRET = "";

$json_input = file_get_contents('php://input');
$yows = new YOWS($CHANNEL_SECRET, $CHANNEL_ACCESS_TOKEN, $json_input);

if ($yows->type === "unfollow") {
    $yows->unRegister($yows->userid);
} elseif ($yows->type === "follow" && $yows->checkRegister($yows->userid) === 0) {
    $profile = $yows->getProfile($yows->userid);
    $yows->register($yows->userid);
    $yows->pushMessage($yows->userid, "登録ありがとうございます ??3 hearts?\n\n・田村ゆかり公式サイト\n・ファンクラブサイト\nの更新をお知らせ致します??light bulb?\n\nなお、当アカウントをブロックすることで利用の停止ができます ??content?");
} elseif ($yows->type === "follow" && $yows->checkRegister($yows->userid) === 1) {
    $yows->pushMessage($yows->userid, "既に登録済みです。");
} else {
    die();
}
<?php
require_once 'twitteroauth/twitteroauth.php';

class YOWS
{
    private $CHANNEL_SECRET = '';
    private $CHANNEL_ACCESS_TOKEN = '';
    public $header = [];
    public $data, $userid, $type = '';

    public function __construct($CHANNEL_SECRET, $CHANNEL_ACCESS_TOKEN, $data)
    {
        $this->CHANNEL_SECRET = $CHANNEL_SECRET;
        $this->CHANNEL_ACCESS_TOKEN = $CHANNEL_ACCESS_TOKEN;
        $this->data = $data;
        $this->getallheaders();
        $this->checkSignature();
        $this->setVariables();
    }

    private function getallheaders()
    {
        $header = '';
        foreach ($_SERVER as $name => $value) {
            if (substr($name, 0, 5) == 'HTTP_') {
                $header[strtoupper(str_replace(' ', '-', ucwords(str_replace('_', ' ', substr($name, 5)))))] = $value;
            }
        }
        $this->header = $header;
    }

    public function checkSignature()
    {
        if (base64_decode($this->header["X-LINE-SIGNATURE"]) === hash_hmac('sha256', $this->data, $this->CHANNEL_SECRET, true)) {
            $this->data = json_decode($this->data, true);
        } else {
            die();
        }
    }

    private function setVariables()
    {
        foreach ($this->data as $row) {
            $this->type = $row[0]['type'];
            $this->userid = $row[0]['source']['userId'];
        }
    }

    /**
     * @param $USERID
     * @return int 0=not register / 1=registerd
     */
    public function checkRegister($USERID)
    {
        try {
            $pdo = new PDO('mysql:dbname=; host=', '', '',
                array(PDO::ATTR_EMULATE_PREPARES => false));

            $sql = "SELECT * FROM line WHERE userid='${USERID}'";
            $stmt = $pdo->query($sql);

            foreach ($stmt as $row) {
                $query_result = $row['id'];
            }

            empty($query_result) === true ? $result = 0 : $result = 1;

            return $result;

        } catch (PDOException $e) {
            $this->debug('Error:' . $e->getMessage());
            die();
        }
    }

    public function register($USERID)
    {
        try {
            $pdo = new PDO('mysql:dbname=; host=', '', '',
                array(PDO::ATTR_EMULATE_PREPARES => false));

            $stmt = $pdo->prepare('INSERT INTO line (userid, date) VALUES (:register_userid, :date)');
            $stmt->execute(array(':register_userid' => $USERID, ':date' => $this->getDate()));

        } catch (PDOException $e) {
            $this->debug('Error:' . $e->getMessage());
            die();
        }
    }

    public function unRegister($USERID)
    {
        try {
            $pdo = new PDO('mysql:dbname=; host=', '', '',
                array(PDO::ATTR_EMULATE_PREPARES => false));

            $stmt = $pdo->prepare('DELETE FROM line WHERE userid = :delete_userid');
            $stmt->execute(array(':delete_userid' => $USERID));

        } catch (PDOException $e) {
            $this->debug('Error:' . $e->getMessage());
            die();
        }
    }

    private function getDate()
    {
        return date("Y-m-d H:i:s");
    }

    public function pushMessage($USERID, $msg)
    {
        $format_text = [
            "type" => "text",
            "text" => $msg
        ];

        $post_data = [
            "to" => $USERID,
            "messages" => [$format_text]
        ];

        $header = array(
            'Content-Type: application/json',
            'Authorization: Bearer ' . $this->CHANNEL_ACCESS_TOKEN
        );

        $ch = curl_init('https://api.line.me/v2/bot/message/push');
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);

        $result = curl_exec($ch);
        curl_close($ch);
    }

    public function debug($string)
    {
        error_log(print_r($string, true), 3, '/var/log/nginx/error.log');
    }
}

 

LINEからのアクセスかどうかは従来のLINE BOT APIと同じで

public function checkSignature()
    {
        if (base64_decode($this->header["X-LINE-SIGNATURE"]) === hash_hmac('sha256', $this->data, $this->CHANNEL_SECRET, true)) {
            $this->data = json_decode($this->data, true);
        } else {
            die();
        }
    }

こんな感じで署名の確認ができる。

 

 

Push(BOTからユーザーへメッセージ送信)

<?php
require_once 'lib/push_class.php';

$URL = "http://www.tamurayukari.com/";
$old_url = file_get_contents('old_url');

/* LINE */
$CHANNEL_ACCESS_TOKEN = '';

$yows = new Push($URL, $CHANNEL_ACCESS_TOKEN);

$latest_content = $yows->crawler->filter('div#news_table td a')->text();
$latest_url = $yows->crawler->filter('div#news_table td a')->attr('href');

if($latest_url !== $old_url) {
    $msg = "【サイト更新通知BOTよりお知らせ】\r\n公式サイトが更新されました!\r\n\r\n【タイトル】\r\n{$latest_content}\r\n\r\n詳しくはこちらへ!\r\n${latest_url}";

    foreach ($yows->getUserid() as $userid) {
        $yows->pushMessage($userid, $msg);
    }

    file_put_contents('old_url', $latest_url);
}
<?php
require_once 'goutte/vendor/autoload.php';

use Goutte\Client;

class Push
{
    private $CHANNEL_ACCESS_TOKEN, $url = '';
    public $client, $crawler = '';

    public function __construct($url, $CHANNEL_ACCESS_TOKEN)
    {
        $this->client = new Client();

        $this->url = $url;
        $this->CHANNEL_ACCESS_TOKEN = $CHANNEL_ACCESS_TOKEN;
        $this->crawler = $this->client->request('GET', $this->url);
    }

    public function getUserid() {
        try {
            $array_userid = [];

            $pdo = new PDO('mysql:dbname=yukarinotification; host=database.cma0jldtuyey.us-west-2.rds.amazonaws.com', 'luis', 'luism8526',
                array(PDO::ATTR_EMULATE_PREPARES => false));

            $stmt = $pdo->query("SELECT * FROM line");

            foreach ($stmt as $row) {
                $array_userid[] = $row['userid'];
            }

            return $array_userid;

        } catch (PDOException $e) {
            /** ERROR HANDLING  */
            die();
        }
    }

    public function pushMessage($USERID, $msg)
    {
        $format_text = [
            "type" => "text",
            "text" => $msg
        ];

        $post_data = [
            "to" => $USERID,
            "messages" => [$format_text]
        ];

        $header = array(
            'Content-Type: application/json',
            'Authorization: Bearer ' . $this->CHANNEL_ACCESS_TOKEN
        );

        $ch = curl_init('https://api.line.me/v2/bot/message/push');
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);

        $result = curl_exec($ch);
        curl_close($ch);
    }
}

LINEにリクエストを投げるときはCHANNEL_ACCESS_TOKENをヘッダーに載せるだけ。 LINE DeveloperからサーバーのIPをホワイトリストに登録する必要はあります。 今回の話に関係はないですけどGoutte便利だし何より速い。

 

メッセージを送信する関数は

    public function pushMessage($USERID, $msg)
    {
        $format_text = [
            "type" => "text",
            "text" => $msg
        ];
        $post_data = [
            "to" => $USERID,
            "messages" => [$format_text]
        ];
        $header = array(
            'Content-Type: application/json',
            'Authorization: Bearer ' . $this->CHANNEL_ACCESS_TOKEN
        );
        $ch = curl_init('https://api.line.me/v2/bot/message/push');
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
        $result = curl_exec($ch);
        curl_close($ch);

特筆することなし!

 

 

戯言

Webhookに関してはイベント内容がJSON見れば一発で分かるようになったので楽でした。 LINE BOT APIでは type 1 が友達追加時で~ type3 がユーザーからのメッセージとかで 分かりにくかった。

あとはドキュメントも日本語が用意されて、各言語のSDKも用意されているので 比較的容易に移行(作成)できました。

ソースコードはこちら rluisr/yukari-line-botA - github