[Laravel]我做了一个LINE BOT,当您发送位置信息时,它会告诉您附近的饺子或烤肉店。


背景

我想做些有趣的事情,所以当我发送位置信息时,我做了一个LINE BOT,告诉我附近的饺子店和烤肉店。
饺子和烤肉的原因是我喜欢它们。

参考

[官方参考]
https://developers.line.biz/ja/docs/messaging-api

[用于PHP的LINE SDK]
https://github.com/line/line-bot-sdk-php

[[LINE Bot]我制作了一个机器人,可以通过位置信息搜索tabelog 3.5或更高版本的优秀餐厅。
(我以此为模型。)
https://qiita.com/NARI_Creator/items/f29112e6f604c86b3c0d

[如何使用可直接使用的源代码获取Gurunavi API]
https://enjoy-surfing.com/gurunavi/

[用于Laravel制作LineBot的模型]
https://qiita.com/sh-ogawa/items/2238e579d7ee538025a0

[使用Laravel在LINE上创建聊天机器人(创建QR代码)]
https://blog.capilano-fw.com/?p=4285#i-4

这很有帮助??谢谢。

规范

1.输入是否要吃饺子或烤肉。
2.发送位置信息
3.

告诉您附近的商店

简单规格如下。 ??

创建之前要做什么

申请heroku

我认为如果您在这里看看,就可以做到。

易于使用,直到您注册Heroku帐户并部署
在heroku上部署Laravel(数据库是MySQL)

如果注册您的信用卡,

heroku每月可以免费使用1000个小时。
使用免费计划时,它每30分钟睡眠一次,因此睡眠后首次访问的响应速度很慢,但是如果完成批处理,似乎没有问题。

LINE Message API注册

创建一个LINE BOT帐户。

官方文件
该公式相当仔细地解释了它,因此我认为您可以边看边做。

实作

安装LINE Messaging API SDK

1
$ composer require linecorp/line-bot-sdk

添加环境变量

1
2
3
#line
LINE_SECRET_TOKEN =
LINE_ACCESS_TOKEN =

您可以从帐户的"基本设置"中引用

令牌。

创建路线

当用户向LINE BOT发送消息时,LINE平台向指定应用程序的URL发送HTTP请求,因此请确定URL。

web.php

1
Route::post('line','LineController@post')->name('line.post');

顺便说一下,请求是通过POST发送的。
之后,我还将创建一个Controller。

创建一个客户端

这次,我在服务类中对其进行了总结。

LineService.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace App\Services;

use LINE\LINEBot;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;

class LineService
{
    /**
     * linesdkの使用開始
     *
     * @return LINEBot
     */
    public static function lineSdk()
    {
        $token = config('services.line.token');
        $secret = config('services.line.secret');

        $httpClient = new CurlHTTPClient($token);
        $bot = new LINEBot($httpClient,['channelSecret' => $secret]);

        return $bot;
    }
}

$token$service是在.env中注册的LINE_SECRET_TOKENLINE_ACCESS_TOKEN,但是它们是在config服务中注册后带入的。

它不是用基本的官方自述文件写的吗?

验证签名

危险请求可能来自生产线平台以外的其他地方,需要进行验证。
当请求来自在线平台时,请求标头将包含称为X-Line-Signature的内容。我要验证这一点,

使用

通道密钥作为私钥,并使用HMAC-SHA256算法获取请求正文的摘要值。
确保摘要值的Base64编码值与请求标头中X-Line-Signature中包含的签名匹配。

由于它被仔细地写为

,因此请按原样进行验证。

LineController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Http\Controllers;

use App\Services\LineService;
use LINE\LINEBot\SignatureValidator;

class LineController extends Controller
{
    public function post()
    {

        $signarure = request()->header('X-Line-Signature');
        $validateSignature = SignatureValidator::validateSignature($httpRequestBody, $channelSecret, $signarure);
        if ($validateSignature) {
            return response()->json(200);
        } else {
            abort(400);
        }
    }
}

目前,如果可以验证签名,则将返回200,否则将返回400

SignatureValidator::validateSignature的操作是
请求主体摘要值是base64编码的,并与请求标头中的X-Line-Signature比较。

SignatureValidator.php

1
2
3
4
5
6
7
8
public static function validateSignature($body, $channelSecret, $signature)
{
    if (!isset($signature)) {
        throw new InvalidSignatureException('Signature must not be empty');
    }

    return hash_equals(base64_encode(hash_hmac('sha256', $body, $channelSecret, true)), $signature);
}

这是什么吗?

1
2
3
4
$channelSecret = config('services.line.secret');
$httpRequestBody = request()->getContent();
$hash = hash_hmac('sha256', $httpRequestBody, $channelSecret, true);
$signature = base64_encode($hash);

注册Webhook URL

接下来,从LINE控制面板中注册Webhook URL。
注册heroku网址(×××××.herokuapp.com / line),然后按验证按钮。
如果出现消息success,可以吗?
如果不阻止控制器返回200,则会收到错误消息。

发送短信后我会回复

目前,当发送短信时,我将返回こんにちは!

LineController.php

1
2
3
4
5
6
7
8
9
10
11
12
        $bot = LineService::lineSdk();

        try{
            $events = $bot->parseEventRequest($httpRequestBody, $signature);
            foreach ($events as $event) {
                if ($event instanceof LINE\LINEBot\Event\MessageEvent\TextMessage) {
                    $bot->replyText($event->getReplyToken(), 'こんにちは!');
                }
            }
        }catch(\Exception $e){
            Log::debug($e);
        }

parseEventRequest验证签名是否有效,并在请求有效时对其进行解析。

webhook↓发送各种事件。
Webhook事件对象
SDK

发送的事件是否为短信
检查它是否是$event instanceof LINE\LINEBot\Event\MessageEvent\TextMessage的实例。

响应令牌被发布到可以应答的请求,因此它由$event->getReplyToken()获取并由$bot->replyText与响应消息??

一起返回。

尝试根据事件

的类型更改响应

暂时实施对以下事件的响应。
?关注事件
?取消关注事件
邮票消息事件
短信事件
?位置消息事件

LineController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
        try{
            $events = $bot->parseEventRequest($httpRequestBody, $signature);
            foreach ($events as $event) {
                //フォローイベント
                if($event instanceof FollowEvent){
                    $followMessage = '友達登録ありがとう!何が食べたいか入力してね。近くのお店を見つけるよ!(例:餃子、ぎょうざ、ぎょーざ、焼肉、焼き肉、やきにく など)';
                    $bot->replyText($event->getReplyToken(), $followMessage);
                    continue;
                }
                //フォロー解除イベント
                else if ($event instanceof UnfollowEvent) {
                    continue;
                }
                //スタンプメッセージイベント
                else if($event instanceof StickerMessage){
                    $stampMessage = '可愛いスタンプだね?!何が食べたいか入力してくれたら近くのお店を見つけるよ!(例:餃子、ぎょうざ、ぎょーざ、焼肉、焼き肉、やきにく など)'
                    $bot->replyText($event->getReplyToken(), $stampMessage);
                    continue;
                }
                //テキストメッセージイベント
                else if($event instanceof TextMessage){
                    //TODO テキストメッセージ実装
                }
                //位置情報メッセージイベント
                else if($event instanceof LocationMessage){
                    //TODO 位置情報実装
                }
            }
        }catch(\Exception $e){
            Log::debug($e);
        }

参考别名????? ????

回复短信

此BOT要求您先输入要吃的食物,然后索取位置信息??
我想判断是饺子还是烤肉,但是
我想支持各种形式的饺子和烤肉,例如餃子、ぎょうざ、ぎょーざ、ギョーザ
只需将所有内容放入if语句中。也许不好??

LineService.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    /**
     * 入力された文字が餃子か判定
     *
     * @param $text
     * @return bool
     */
    public function isGyoza($text)
    {
        return ($text === '餃子' || $text === 'ぎょうざ' || $text === 'ぎょーざ' || $text === 'ギョーザ' || $text === '??????');
    }

    /**
     * 入力された文字が餃子か判定
     *
     * @param $text
     * @return bool
     */
    public function isYakiniku($text)
    {
        return ($text === '焼肉' || $text === '焼き肉' || $text === 'やきにく' || $text === 'ヤキニク' || $text === '????');
    }

您将在此处收到烤肉或饺子的结果。 ↓
如果输入了烤肉或饺子,请使用后面所述的requireLocation查找位置信息??
同样,putCategoryuser_idcategory保存在数据库中。
category对于饺子是1,对于烤肉是2。
如果输入其他单词,它将返回一条短信??

LineService.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
    /**
     * 送られてきたメッセージに対するレスポンス
     *
     * @param $bot
     * @param $event
     * @return void
     */
    public function getMessage($bot, $event)
    {
        try{
            $text = $event->getText();
            if ($this->isGyoza($text)) {
                //DBに保存
                $user_id = $event->getUserId();
                $category = Line::GYOZA;
                $this->putCategory($user_id,$category);
                //位置情報を求める
                $word = '餃子';
                $this->requireLocation($bot, $event, $word);
            } else if ($this->isYakiniku($text)) {
                //DBに保存
                $user_id = $event->getUserId();
                $category = Line::YAKINIKU;
                $this->putCategory($user_id,$category);
                //位置情報を求める
                $word = '焼肉';
                $this->requireLocation($bot, $event, $word);
            }else{
                $bot->replyText($event->getReplyToken(), '焼肉か餃子しか調べられないよ、、ごめんね。');
            }
        }catch(\Exception $e){
            Log::debug($e);
        }
    }

    /**
     * カテゴリーとユーザーIDをDBに保存
     *
     * @param $user_id
     * @param $category
     * @return void
     */
    public function putCategory($user_id, $category)
    {
        Line::create([
            'user_id' => $user_id,
            'category' => $category
        ]);
    }

    /**
     * 位置情報を求めるメッセージを送る
     *
     * @param $bot
     * @param $event
     * @param $word  //餃子か焼肉か
     * @return void
     */
    public function requireLocation($bot, $event, $word)
    {
        $uri = new UriTemplateActionBuilder('現在地を送る!', 'line://nv/location');
        $message = new ButtonTemplateBuilder(null, $word.'が食べたいんだね!今どこにいるか教えてほしいな!', null, [$uri]);

        $bot->replyMessage($event->getReplyToken(), new TemplateMessageBuilder('位置情報を送ってね', $message));

    }

?UriTemplateActionBuilder
轻击与此操作关联的控件时,将打开在第二个参数中注册的URL。
line://nv/location在线打开位置信息。

?ButtonTemplateBuilder
创建一个按钮模板。参数是标题,正文,图像URL和操作顺序。
注意,动作必须排列成阵列!

?TemplateMessageBuilder
创建模板消息。参数是替代文本ButtonTemplateBuilder。

我认为,如果到目前为止,它会像这样。 ??
Screenshot_20200701-181017.png

从发送的地址

获取经度和纬度

由于可以通过功能

getLatitude()getLongitude()获得纬度和经度,因此请使用此??? <? https://line.github.io/line-bot-sdk-php/source-class-LINE.LINEBot.Event.MessageEvent.LocationMessage.html#65-68

LineController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 try{
            $events = $bot->parseEventRequest($httpRequestBody, $signature);
            foreach ($events as $event) {
                //フォローイベント
                if($event instanceof FollowEvent){
                    $followMessage = '友達登録ありがとう!何が食べたいか入力してね。近くのお店を見つけるよ!(例:餃子、ぎょうざ、ぎょーざ、焼肉、焼き肉、やきにく など)';
                    $bot->replyText($event->getReplyToken(), $followMessage);
                    continue;
                }
                //フォロー解除イベント
                else if ($event instanceof UnfollowEvent) {
                    continue;
                }
                //スタンプメッセージイベント
                else if($event instanceof StickerMessage){
                    $stampMessage = '可愛いスタンプだね?!何が食べたいか入力してくれたら近くのお店を見つけるよ!(例:餃子、ぎょうざ、ぎょーざ、焼肉、焼き肉、やきにく など)';
                    $bot->replyText($event->getReplyToken(), $stampMessage);
                    continue;
                }
                //テキストメッセージイベント
                else if($event instanceof TextMessage){
                    (new LineService())->getMessage($bot,$event);
                    continue;
                }
                //位置情報メッセージイベント
                else if($event instanceof LocationMessage){
                    (new GurunaviService())->returnGurunaviList($bot, $event,$event->getLatitude(), $event->getLongitude()); //これを追加
                }
                else{
                    break;
                }
            }

从Gurunavi

获取信息

实现returnGurunaviList
使用餐厅搜索API。

您需要多少个参数? ??↓

?keyid(Gurunavi提供的访问密钥)

?category_s(类别代码)
https://api.gnavi.co.jp/api/manual/categorysmaster/
您可以通过点击此API获取类别代码列表。

?纬度

?经度

?范围(从纬度/经度(半径)开始的搜索范围)
就像1:300m,2:500m,3:1000m,4:2000m,5:3000m。高达3000m?

GurunabiService.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
     /**
     *
     * カテゴリーが存在すればぐるなびで検索をかける
     *
     * @param $bot
     * @param $event
     * @param $lat
     * @param $lng
     */
    public function returnGurunaviList($bot, $event, $lat, $lng)
    {
        //DBにカテゴリーがあるか検証
        $category = Line::where(['user_id' => $event->getUserId()])->latest()->first();
        if ($category) {
            $getGurunaviResult = $this->getGurunavi($lat, $lng, $category);
            if (property_exists($getGurunaviResult, 'error')) {
                $bot->replyText($event->getReplyToken(), 'お店が見つからなかったよ?、ごめんね。');
            } else {
                $category->delete();
                (new FlexMessage())->returnFlexMessage($event, $getGurunaviResult);
            }
        } else {
            $bot->replyText($event->getReplyToken(), '先に何が食べたいか教えてね!');
        }
    }

   /**
     *
     * ぐるなびで検索
     *
     * @param $lat
     * @param $lng
     * @param $category
     * @return mixed
     */
    public static function getGurunavi($lat, $lng, $category)
    {
        $endpoint = 'https://api.gnavi.co.jp/RestSearchAPI/v3/';
        //自分のアクセスキー
        $keyId = config('services.gurunavi.key');
        //検索範囲
        $range = 5;
        //カテゴリーコードを取得


        if ($category->category === 1) {
            $categoryCode = 'RSFST14008';
        } else {
            if ($category->category === 2) {
                $categoryCode = 'RSFST05001';
            } else {
                $categoryCode = 'RSFST05003';
            }
        }

        $url = $endpoint.'?keyid='.$keyId.'&category_s='.$categoryCode.'&latitude='.$lat.'&longitude='.$lng.'&range='.$range;
        $json = file_get_contents($url);

        return json_decode($json);
    }

验证是否存在

returnGurunaviList类别后,我使用getGurunavi执行Gurunavi搜索。将数据库中保存的类别与Gurunavi的类别代码进行比较。 (最好从头开始存储Gurunavi类别代码??)
顺便说一句,如果类别不是饺子和烤肉,则可以按章鱼烧的类别代码进行搜索。
并且由于搜索结果以json返回,因此将其解码并返回。

通过玩returnFlexMessage ??? returned?

返回搜索结果

将tabelog搜索结果转换为Flex Message并发送

Flex Message似乎是用json编写的,但我认为我永远做不到,所以
我添加了LINE Bot Designer。

使用LINE Bot Designer检查Flex Message的模板设计时创建,并将json转换为数组。

使用returnFlexMessage接收解码的搜索结果,使用generateFlexTemplateContent修补,最后使用curl进行POST。
这里有标题或正文吗?

flexMessage.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    /**
     * ぐるなびの情報を送信
     *
     * @param $event
     * @param $gurunavi
     * @return void
     */
    public function returnFlexMessage($event,$gurunavi)
    {
        $token = config('services.line.token');
        $postJson = $this->generateFlexTemplateContent($gurunavi);

        $result = json_encode(['replyToken' => $event->getReplyToken(), 'messages' => [$postJson]]);
        $curl = curl_init();
        //curl_exec() の返り値を文字列で返す
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        //POSTリクエスト
        curl_setopt($curl, CURLOPT_POST, true);
        //ヘッダを指定
        curl_setopt($curl, CURLOPT_HTTPHEADER, array('Authorization: Bearer '.$token, 'Content-type: application/json'));
        //リクエストURL
        curl_setopt($curl, CURLOPT_URL, 'https://api.line.me/v2/bot/message/reply');
        //送信するデータ
        curl_setopt($curl, CURLOPT_POSTFIELDS, $result);

        $curlResult = curl_exec($curl);

        curl_close($curl);

        return $curlResult;
    }

generateFlexTemplateContent中,将获取的Gurunavi信息逐个旋转并应用于Flex Message模板。
模板以json编写,但是使用LINE Bot Designer,您可以在检查设计时轻松创建json。

スクリーンショット 2020-07-07 17.41.53.png

之后,请参考此处调整json的形状。 ???♀?

flexMessage.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
    /**
     * ぐるなびの情報をflexMessageテンプレートにあてめる
     *
     * @param $gurunavis
     * @return array
     */
    private function generateFlexTemplateContent($gurunavis)
    {
        $lists = [];
        foreach ($gurunavis->rest as $rest) {
            $lists[] = $this->getFlexTemplate($rest);
        }

        $contents = ["type" => "carousel", "contents" => $lists];
        return ['type' => 'flex', 'altText' => 'searchResult', 'contents' => $contents];
    }

    /**
     * flexMessageテンプレート
     *
     * @param $gurunavi
     * @return array
     */
    private function getFlexTemplate($gurunavi)
    {
        return [
            "type" => "bubble",
            "hero" => [
                "type" => "image",
                "url" => $gurunavi->image_url->shop_image1 ? $gurunavi->image_url->shop_image1: "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_1_cafe.png",
                "size" => "full",
                "aspectRatio" => "20:13",
                "aspectMode" => "cover",
                "action" => [
                    "type" => "uri",
                    "label" => "Line",
                    "uri" => $gurunavi->url_mobile ? $gurunavi->url_mobile : '不明',
                ]
            ],
            "body" => [
                "type" => "box",
                "layout" => "vertical",
                "contents" => [
                    [
                        "type" => "text",
                        "text" => $gurunavi->name ? $gurunavi->name : '不明',
                        "size" => "xl",
                        "weight" => "bold",
                        "wrap" => true
                    ],
                    [
                        "type" => "box",
                        "layout" => "baseline",
                        "margin" => "md",
                        "contents" => [
                            [
                                "type" => "text",
                                "text" => $gurunavi->opentime ? $gurunavi->opentime : '不明',
                                "flex" => 0,
                                "margin" => "md",
                                "size" => "sm",
                                "color" => "#999999",
                                "wrap" => true
                            ]
                        ]
                    ],
                    [
                        "type" => "box",
                        "layout" => "vertical",
                        "spacing" => "sm",
                        "margin" => "lg",
                        "contents" => [
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "spacing" => "sm",
                                "contents" => [
                                    [
                                        "type" => "text",
                                        "text" => "種類",
                                        "flex" => 1,
                                        "size" => "sm",
                                        "color" => "#AAAAAA"
                                    ],
                                    [
                                        "type" => "text",
                                        "text" => $gurunavi->code->category_name_l[0] ? $gurunavi->code->category_name_l[0] : '不明',
                                        "flex" => 5,
                                        "size" => "sm",
                                        "color" => "#666666",
                                        "wrap" => true
                                    ]
                                ]
                            ],
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "spacing" => "sm",
                                "contents" => [
                                    [
                                        "type" => "text",
                                        "text" => "場所",
                                        "flex" => 1,
                                        "size" => "sm",
                                        "color" => "#AAAAAA"
                                    ],
                                    [
                                        "type" => "text",
                                        "text" => $gurunavi->access->station.'徒歩'.$gurunavi->access->walk.'分' ? $gurunavi->access->station.'徒歩'.$gurunavi->access->walk.'分' : '不明',
                                        "flex" => 5,
                                        "size" => "sm",
                                        "color" => "#666666",
                                        "wrap" => true
                                    ]
                                ]
                            ],
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "spacing" => "sm",
                                "contents" => [
                                    [
                                        "type" => "text",
                                        "text" => "駐車場",
                                        "flex" => 2,
                                        "size" => "sm",
                                        "color" => "#AAAAAA"
                                    ],
                                    [
                                        "type" => "text",
                                        "text" => $gurunavi->parking_lots ? $gurunavi->parking_lots : '不明',
                                        "flex" => 5,
                                        "size" => "sm",
                                        "color" => "#666666",
                                        "wrap" => true
                                    ]
                                ]
                            ]
                        ]
                    ]
                ]
            ],
            "footer" => [
                "type" => "box",
                "layout" => "vertical",
                "flex" => 0,
                "spacing" => "sm",
                "contents" => [
                    [
                        "type" => "button",
                        "action" => [
                            "type" => "uri",
                            "label" => "ぐるなびで見る",
                            "uri" => $gurunavi->url_mobile ? $gurunavi->url_mobile : '不明'
                        ],
                        "height" => "sm",
                        "style" => "link"
                    ],
                    [
                        "type" => "spacer",
                        "size" => "sm"
                    ]
                ]
            ]
        ];
    }
}

完成??

我成为了一名工程师,但在第一个月就尝试制作它感到沮丧,因为已经过去了四个月,所以当我再次尝试时感到惊讶。
我仍然无法编写干净的源代码,而且我确定服务类和方法都搞砸了,所以我将继续致力于它???

LINE参考非常容易理解!
有些不同!请让我知道是否有任何地方??