LINE BOTを使って疑似彼女を錬成するレシピ

## はじめに

最近寒くなってきましたね〜、そしてもうすぐクリスマス！
世のカップルを爆撃するのを楽しみにしております ☆ wkwk

![depositphotos_320979774-stock-photo-closeup-of-red-baubles-hanging.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170923/842822db-96a8-a74c-b81c-809f5dc2f579.jpeg)

そんな寂しい僕のお供としてLINE BOTを作ってみましたので、今年のクリスマスは一緒に過ごそうと思います ♡

## 紹介すること・しないこと

**紹介すること**
- アプリケーションの構成
- BOTの仕様
- DB設計
- プログラム、及びその解説
- **採用PR** (ぜひ最後のページまで見ていって下さい！)

**紹介しないこと**
LINE DeveloperサイトでのLINEアカウントの作り方は省略させて下さい。すいません > <

## 構成
自前で用意したサーバがLINE BOTのサーバからHTTPでBodyを受け取ります。
その後、gRPC経由でGoにリクエストを投げテキストを加工しPHPに返しています。
![linebot (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170923/a3becec3-dd65-ef7e-82a4-d7e0ce622b3b.png)

## BOTの仕様
- 特定のキーワードを含むメッセージには、専用のメッセージで返信する
- それ以外のメッセージには、ランダムで用意したメッセージを返す

公式LINEアカウントあるあるですね。

## DB設計
ざっくりとしたER図ですが以下のように作ってます。
PHPがDBと接続することはなく、GoがCRUDを担います。
※ created_at等々の記述は省略しています。

![linebot_er.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170923/0165e5ef-5ed0-c31c-4e95-725b37e8ac93.png)


### 各テーブルの解説
**accounts**

- line_id ... LINE BOTサーバーから送られてきたUserIDの保存場所
- last_action_date ... LINEユーザの最終アクション日時をログっています

**message_logs**

- account_id ... accountsテーブルの外部キー
- text ... LINEから送られてきたメッセージの保存場所

**responses**

- match ... 特定のキーワードを保存  
LINEから送られてきたメッセージがmatchカラムと部分マッチした場合にtextカラムの値を返す
- text ... 特定のキーワードが含まれているケースでの返信に使用するテキスト

## 実装したコード

#### PHP

``` php:index.php
<?php
require('vendor/autoload.php');

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

$channel_access_token = getenv('CHANNEL_ACCESS_TOKEN');
$channel_secret = getenv('CHANNEL_SECRET');

class LinebotService
{
  private $client;

  function __construct()
  {
    $this->client = new Akame\LinebotClient(getenv('GRPC_HOST'), [
      'credentials' => Grpc\ChannelCredentials::createInsecure(),
    ]);
  }

  function reply_message_request(string $line_id, string $text): string
  {
    $request = new Akame\ReplyMessageRequest();
    $request->setLineId($line_id);
    $request->setText($text);
    list($response, $status) = $this->client->ReplyMessage($request)->wait();
    if ($status->code !== Grpc\STATUS_OK) {
      echo "ERROR: " . $status->code . ", " . $status->details . PHP_EOL;
      exit(1);
    }
    return $response->getMessage();
  }
}

$http_client = new CurlHTTPClient($channel_access_token);
$bot = new LINEBot($http_client, ['channelSecret' => $channel_secret]);

$line_service = new LinebotService();
$events = $bot->parseEventRequest(file_get_contents('php://input'), $_SERVER['HTTP_X_LINE_SIGNATURE']);
foreach ($events as $event) {
  $bot->replyText($event->getReplyToken(), $line_service->reply_message_request($event->getUserId(), $event->getText()));
}
```
### コードの解説
以下でgRPCにリクエストする値をセットしています。

``` php
$request->setLineId($line_id);
$request->setText($text);
```
```{line_id: "xxxx", "text": "Hello"}```ってリクエストをイメージしてもらうとわかりやすいかもです。


envはdirenvからセットしています。
https://github.com/direnv/direnv


---

#### Go

``` go:message.go
package services

import (
	m "akame/app/models"
	pb "akame/protobuf"
	"context"
)

type MessageService struct {
	pb.UnimplementedLinebotServer
}

func (s *MessageService) ReplyMessage(ctx context.Context, in *pb.ReplyMessageRequest) (*pb.ReplyMessageResponse, error) {
	account := &m.Account{}
	account_row := account.CreateAccount(in.GetLineId())

	messageLog := &m.MessageLog{}
	messageLog.Logger(int(account_row.ID), in.GetText())

	response := &m.Response{}

	return &pb.ReplyMessageResponse{Message: response.Reply(in.GetText())}, nil
}
```


### コードの解説
```go
// アカウントの作成をしています。
account.CreateAccount(in.GetLineId())
// メッセージをログとして残しています。
messageLog.Logger(int(account_row.ID), in.GetText())
// 返信用メッセージのテキストを返す。
response.Reply(in.GetText())
```

返信用のランダムテキストはDBじゃなくて、レスポンスモデルに配列で持っています。
(上のコードにないですが)

``` go:response.go
var replyPattern = []string{
	"今が1番幸せ", "いつもありがとう", "あなたと一緒にいると楽しい", "早く会いたいな",
	"2人だけの秘密ね",
}
```

## Seed
responsesテーブルのデータを事前に入れておきます。

``` sql:migrate/sql/000004_seed_respose.up.sql
INSERT INTO `responses`(`match`, `text`)
  VALUES ("愛してる", "私も愛してる"), ("かわいいね", "もうっ、恥ずかしいよ //");
```

``` sql:migrate/sql/000004_seed_respose.down.sql
TRUNCATE responses;
```

※ [go-gormigrate](https://github.com/golang-migrate/migrate)を使用しています。

## 彼女と会話してみる

![スクリーンショット 2021-11-12 11.48.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170923/5b24f238-2317-860b-ec80-d4c12da27432.png)

どうやら相思相愛のようです > <

## あとがき

BOTから返信を貰うためにLINEにテキスト打つ時が一番キツかったですねw

気づいたら、今年ももうあと少しですね〜。
世の中的に色々あって大変でしたけど、落ち着いて来ているので楽しいクリスマス・年末年始を過ごせそうです！

## 採用PR

大規模サイトので開発やマイクロサービス化・二次元コンテンツに興味がある、もしくはチャレンジしてみたいという方は是非一緒に働きましょう！

https://hrmos.co/pages/eisys/jobs/0000191

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307359/2000f749-3e82-7a47-b1c4-4444e237f79c.png" width=45%>