友達が忙しくても友達とLINEする方法について考えてみた。

# 1. 概要
友達とのLINEって楽しいですよね。
でも、友達も忙しい時ってあります。

LINEなんて返してる暇ないよ！！･･･わかります。

そんな時、友達みたいなBOTがあったらな･･･って思いませんか？

･･･ということで、友達のLINEのトーク内容をごっそりかき集め、文章を自動生成してみよう！と！
ちょっと長いです･･･(・∀・)
暇人による暇人のための暇人だけが読んでくれるエントリです。

# 2. 開発環境
LINEトークファイル整形ツール
・WPF
・Visual Studio 2017 Community

Botアプリ
・Node.js
・Express

応答アプリ動作サーバー
・Heroku

# 3. データを用意
## 3-1. LINEトークファイルを出力
出力したいトークのトーク画面から右上の「V」マークを押して、その中の「設定」へ進みます。

![image01.JPG](https://qiita-image-store.s3.amazonaws.com/0/147118/2dac6d4e-9ff9-5994-1d19-802e45c1d631.jpeg)

その後、「トーク内容を送信」にてトーク内容をtxtファイルで送れます。メールとか。
![image02.JPG](https://qiita-image-store.s3.amazonaws.com/0/147118/7f2e407d-2b85-742e-3d34-c45c89ae8e57.jpeg)

## 3-2. LINEトークファイルを整形
さぁ、LINEから取り出したトーク内容ですが、日付や時間が入っているので、そのままではちょっと使えないです。
これを整形するツールをWPFで作成しました。
画面はこんな感じ超質素です。
![image03.png](https://qiita-image-store.s3.amazonaws.com/0/147118/f6d56bd6-8ae5-3b42-5256-4dee772a128c.png)
基本的にはファイルを読み込んで、本文だけ抜き出したいので日付とか取り除いて～ってだけですが、
今回はBOT作成ということで、特定のユーザーのみの本文をデータセットとしたかったので、
「メッセージ取得対象ユーザー」にユーザー名を入れるとその人のトーク内容だけ持ってこれるようにしました。

ちなみに出力したトークファイルの中身はこんな感じです。

> 08:38	あいて	いやいや！！！
08:38	あいて	ネットワークの仕事です笑
08:41	つまさっきー	俺の知ってるネットワーク系の人は
08:41	つまさっきー	みんなめっちゃオタク（良い意味で）
22:23	つまさっきー	"適度に距離保ちつつ楽しんでくれてるなぁ〜
って思った（笑）"

テキストファイルを一行ずつ読み取って、時間、名前、本文をマッピングしたかった。
データが「単数行」の場合がほとんどですがたま～に22:23のように「複数行」にわたる場合がありました。
これがまぁまぁめんどくさいです。やる気が失せました。（はやい）

```csharp:TextLine.cs
namespace LineTalkConverter
{
    class TextLine
    {
        public string Line { get; set; }
        public string Text { get; set; }
        public string Time { get; set; }
        public string Name { get; set; }
        public bool IsMultiple { get; private set; } = false;

        // 単数行
        public TextLine(string time, string name, string text)
        {
            this.Time = time;
            this.Name = name;
            this.Text = text;
        }

        // 複数行
        public TextLine(string line)
        {
            this.IsMultiple = true;
            this.Time = "";
            this.Name = "";
            this.Text = line;
        }
    }
}
```

そして、メインウィンドウのイベントハンドラたち。
整形処理をメインウィンドウに整形ボタンクリックイベントに全部ぶちこんでます。
良きに計らってください。

```csharp:MainWindow.xaml.cs

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;

namespace LineTalkConverter
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {

        string filePath = "";
        List<TextLine> textLines = new List<TextLine>();

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            // ダイアログのインスタンスを生成
            var dialog = new OpenFileDialog();

            // ファイルの種類を設定
            dialog.Filter = "テキストファイル (*.txt)|*.txt|全てのファイル (*.*)|*.*";

            // ダイアログを表示する
            if (dialog.ShowDialog() == true)
            {
                // 選択されたファイル名 (ファイルパス) をメッセージボックスに表示
                FilePath.Text = dialog.FileName;
                filePath = dialog.FileName;
            }
        }

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            StreamReader sr = new StreamReader(filePath, Encoding.GetEncoding("UTF-8"));

            var lastUserName = "";

            while (sr.EndOfStream == false)
            {
                var line = sr.ReadLine();

                // 空白行を無視
                if (line.Equals("")) continue;

                // ヘッダを無視
                if (line.Contains("[LINE]")) continue;
                if (line.Contains("保存日時")) continue;

                // スタンプを無視
                if (line.Contains("[スタンプ]")) continue;

                // URLを無視
                if (line.Contains("http")) continue;

                // 短すぎるものはもうそっこーぶちこむ
                if (line.Length < 4)
                {
                    var textLine = new TextLine(line);
                    textLines.Add(textLine);
                    continue;
                }

                // 日付を無視
                if (line.Length > 4)
                {
                    if (line.Substring(4, 1).Equals("/") && line.Substring(7, 1).Equals("/")) continue;
                }

                // メッセージが複数行に渡る場合がある
                if (line.Substring(2, 1).Equals(":"))
                {
                    // 単数行
                    var splitedLine = line.Split('\t');
                    var textLine = new TextLine(splitedLine[0], splitedLine[1], splitedLine[2].Replace("\"", ""));
                    lastUserName = textLine.Name;
                    textLines.Add(textLine);
                }
                else
                {
                    // 複数行
                    var textLine = new TextLine(line.Replace("\"", ""));
                    textLine.Name = lastUserName;
                    textLines.Add(textLine);
                }
            }

            var targetUser = TargetUser.Text;

            // @Test
            textLines.Where(x => x.Name.Equals(targetUser)).ToList().ForEach(x => Console.WriteLine("{0} : {1}", x.Name, x.Text));

            StreamWriter sw = new StreamWriter(
                @"result.txt",
                false,
                Encoding.GetEncoding("utf-8"));

            textLines.Where(x => x.Name.Equals(targetUser)).ToList().ForEach(x =>
            {
                sw.WriteLine(x.Text);
            });

            sw.Close();
            sr.Close();
        }
    }
}

```

簡単に解説します。

Button_Click()はファイル選択ダイアログの処理です。
ここは基本的な事しかやってません。

```csharp:
private void Button_Click(object sender, RoutedEventArgs e)
{
	// ダイアログのインスタンスを生成
	var dialog = new OpenFileDialog();

	// ファイルの種類を設定
	dialog.Filter = "テキストファイル (*.txt)|*.txt|全てのファイル (*.*)|*.*";

	// ダイアログを表示する
	if (dialog.ShowDialog() == true)
	{
		// 選択されたファイル名 (ファイルパス) をメッセージボックスに表示
		FilePath.Text = dialog.FileName;
		filePath = dialog.FileName;
	}
}

```

そして整形処理
条件をクリアしたものだけをリストに突っ込んでいきます。

＜条件＞

* 空白行でないこと
* "[LINE]"という文字を含まないこと←テキスト出力時のヘッダ
* "保存日時"という文字を含まないこと←テキスト出力時のヘッダ
* "[スタンプ]"という文字を含まないこと←スタンプ
* URLを含まないこと
* 日付だけの行もあるので、YYYY/MM/DDのフォーマットの日付が先頭に出現しないこと

ここがメインテーマじゃないしこんな感じでいっかって感じです(笑)

```csharp:

// 空白行を無視
if (line.Equals("")) continue;

// ヘッダを無視
if (line.Contains("[LINE]")) continue;
if (line.Contains("保存日時")) continue;

// スタンプを無視
if (line.Contains("[スタンプ]")) continue;

// URLを無視
if (line.Contains("http")) continue;

// 短すぎるものはもうそっこーぶちこむ
if (line.Length < 4)
{
	var textLine = new TextLine(line);
	textLines.Add(textLine);
	continue;
}

// 日付を無視
if (line.Length > 4)
{
	if (line.Substring(4, 1).Equals("/") && line.Substring(7, 1).Equals("/")) continue;
}

// メッセージが複数行に渡る場合がある
if (line.Substring(2, 1).Equals(":"))
{
	// 単数行
	var splitedLine = line.Split('\t');
	var textLine = new TextLine(splitedLine[0], splitedLine[1], splitedLine[2].Replace("\"", ""));
	lastUserName = textLine.Name;
	textLines.Add(textLine);
}
else
{
	// 複数行
	var textLine = new TextLine(line.Replace("\"", ""));
	textLine.Name = lastUserName;
	textLines.Add(textLine);
}

```

で、最後にユーザーで絞り込んでテキストファイルに出力します。

```csharp:

var targetUser = TargetUser.Text;

// ファイル出力設定
StreamWriter sw = new StreamWriter(
	@"result.txt",
	false,
	Encoding.GetEncoding("utf-8"));

// 画面から入力されたユーザー名でフィルタリング->リスト化->全データをテキストファイルに出力
textLines.Where(x => x.Name.Equals(targetUser)).ToList().ForEach(x =>
{
	sw.WriteLine(x.Text);
});

sw.Close();
sr.Close();

```

これでトークルーム内の特定のユーザーの本文だけが抽出できました！

**本文だけのresult.txtファイルが、プロジェクトのbin配下にできているはずです！あとで使います！**


# 4. LINEアカウントを作成
続いて、LINEBOTを作るためにアカウントを作成していきます。
## 4-1. LINE Developersにログイン
自分のLINEアカウントでログインできます。
## 4-2. プロバイダー作成（選択）
持ってない人は新規作成、持ってる人は作っても作らなくてもOKです。
![image04.png](https://qiita-image-store.s3.amazonaws.com/0/147118/c71af0ba-54ac-3387-1c5c-ecd8a2fc62ed.png)

## 4-3. チャネル作成
チャネルを作成します。
![image05.png](https://qiita-image-store.s3.amazonaws.com/0/147118/271de27c-aaca-197a-862f-0b5b67905b7a.png)

"Messaging API"を選択してください。
![image06.png](https://qiita-image-store.s3.amazonaws.com/0/147118/669c314a-43a7-8f4d-fcb7-8d3a934cc7fb.png)

次に、いくつか項目を埋めていきます。

アプリ名：そのままLINEアカウントの名前になります。
アプリ説明：審査とかはないはずなので、適当に。
プラン：Developer Trial
業種：「個人」もあります。
メールアドレス：メールアドレスです。

![image07.png](https://qiita-image-store.s3.amazonaws.com/0/147118/8d4e1966-a695-b90c-68fd-b8f0f9809d05.png)

チャネルが作成されたかと思います。
最終的にはいくつか設定が必要になるのですが、ここではまずチャネルシークレットとアクセストークンだけ取得します。
アクセストークンは"発行"が必要になります。特段何かなければ、失効までの時間は0時間で構いません。
失効後はアプリから認証が通らなくなってしまいますので。

# 5. Botアプリの開発
さぁ、やっとBotアプリの開発にこぎ着けました。
今回は「LINEトークから作成したデータセットと、Node.jsで作成したWEB APIで自動生成した文章をLINEに送る」
というテーマでした。前段が長くて忘れちゃいましたね。

あ、Node.jsインストールしておいてください。
https://nodejs.org/en/

WindowsならNodist使った方がいいかもです。
https://github.com/nullivex/nodist

## 5-1. Web APIの作成（Express）
まずは作業フォルダの作成からです。
好きな場所に作業フォルダを作ってください。

```terminal:DOS
C:\> mkdir bot-work
C:\> cd bot-work
```

Expressというnpmのパッケージをインストールします。
Node.jsのWebアプリのMVCフレームワークです。使いやすいです。

コマンドオプションの`-g`はglobal領域にnpmをインストールしますよ～の意です。

```terminal:DOS
C:\bot-work> npm install -g express
```



インストールが完了したら早速アプリを作っていきます。

```terminal:DOS
C:\bot-work> express -y line-bot
```

そして作ったアプリで`npm install`します。
依存関係にあるパッケージを自動的にダウンロードしてくれます。

```terminal:DOS
C:\bot-work> cd line-bot
C:\bot-work\line-bot> npm install
```

```terminal:DOS
C:\bot-work> npm start
```

試しに`npm start`でアプリを起動させて、ブラウザからhttp://localhost:3000とかにアクセスすると見れる。

## 5-2. データセットから文章自動生成
まずは
https://qiita.com/tsumasakky/items/10658611afdcdcad1daa#3-2-line%E3%83%88%E3%83%BC%E3%82%AF%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E6%95%B4%E5%BD%A2
でつくったデータセット（result.txt）をアプリのルートディレクトリに配置しましょう。

それではロジックを組んでいきましょう！

文章の生成には「マルコフ連鎖」を用います。
https://ja.wikipedia.org/wiki/%E3%83%9E%E3%83%AB%E3%82%B3%E3%83%95%E9%80%A3%E9%8E%96

マルコフ連鎖で単語を繋げていくには文章を形態素解析していく必要があります。
npmに"kuromoji"という形態素解析パッケージがあるので、それをインストールします。

```terminal:DOS
C:\bot-work\line-bot> npm install -s kuromoji
```

まずはマルコフ連鎖を行うモジュールを定義します。
アプリのルートディレクトリに"services"というフォルダを作成し、
その中に"markov.js"というファイルを作成します。

```javascript:markov.js
module.exports = class Markov {
  constructor(n) {
    this.data = {};
  }

  add(words) {
    for (var i = 0; i <= words.length; i++) {
      var now = words[i];
      if (now === undefined) {
        now = null
      };
      var prev = words[i - 1];
      if (prev === undefined) {
        prev = null
      };

      if (this.data[prev] === undefined) {
        this.data[prev] = [];
      }
      this.data[prev].push(now);
    }
  }

  sample(word) {
    var words = this.data[word];
    if (words === undefined) {
      words = [];
    }

    return words[Math.floor(Math.random() * words.length)];
  }

  make() {
    var sentence = [];
    var word = this.sample(null);
    while (word) {
      sentence.push(word);
      word = this.sample(word);
    }
    return sentence.join('');
  }
}

```

app.jsに次のメソッドを定義します。

```javascript:app.js

function getMarcovMessage() {
  return new Promise((resolve) => {
    let marcov = new Marcov();
    let message = "";
    builder.build(function (err, tokenizer) {
      if (err) {
        throw err;
      }

      fs.readFile('./result.txt', 'utf8', function (err, data) {
        if (err) {
          throw err;
        }

        var lines = data.split("\n");
        lines.forEach(function (line) {
          var tokens = tokenizer.tokenize(line);

          var words = tokens.map(function (token) {
            return token.surface_form;
          });

          marcov.add(words);
        });

        for (var n = 0; n < 3; n++) {
          message += marcov.make();
        }

        resolve(message);
      });
    });
  });
}

```

これでファイル読み込み->形態素解析->マルコフ連鎖で文章生成
という処理が完成です。

この時点でテストも可能です。
app.jsに下記のルーティングを追加して
http://localhost:3000/markov
にアクセスすれば生成された文章がみれるはず･･･！

```javascript:
app.get('/markov', (req, res) => {
  getMarcovMessage().then((msg) => {
    res.send(msg)
  })
})
```

## 5-3. LINEへ送信
では、生成された文章をLINEに送ってみましょう！

Node.jsからLINE Botを操作するのには、
line-bot-sdk-nodejsというモジュールを使用します。
https://line.github.io/line-bot-sdk-nodejs/getting-started/install.html

```terminal:DOS
C:\bot-work\line-bot> npm install -s @line/bot-sdk
```

まずは必要なモジュールのrequireと、接続情報の設定を`app.js`に追記します。

```javascript:app.js
// 必要なモジュールの読み込み
const line = require('@line/bot-sdk');

const middleware = require('@line/bot-sdk').middleware
const JSONParseError = require('@line/bot-sdk').JSONParseError
const SignatureValidationFailed = require('@line/bot-sdk').SignatureValidationFailed

// 接続情報
const config = {
  channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN,
  channelSecret: process.env.LINE_CHANNEL_SEACRET
};
```

さらにLINEBOTからきたリクエストに対する応答処理を追加していきます。

```javascript:app.js

app.use(middleware(config))

// リクエストを受け、文章生成を開始
app.post('/webhook', (req, res) => {
  Promise
    .all(req.body.events.map(handleEvent))
    .then((result) => res.json(result));
})

// エラーハンドラ
app.use((err, req, res, next) => {
  if (err instanceof SignatureValidationFailed) {
    res.status(401).send(err.signature)
    return
  } else if (err instanceof JSONParseError) {
    res.status(400).send(err.raw)
    return
  }
  next(err) // will throw default 500
})

const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  // 生成したメッセージを返す
  getMarcovMessage().then((msg) => {
    return client.replyMessage(event.replyToken, {
      type: 'text',
      text: msg
    });
  })
}

```

# 6. Botアプリをクラウドサーバーにデプロイ
SaaSでは有名でしょうか、Herokuにデプロイします！

別記事にしました！
https://qiita.com/tsumasakky/items/aa4134fe0866b0db5225

環境変数を設定するのを忘れないでください！
LINE Developersの「チャネル基本設定」にあります。

LINE_CHANNEL_SEACRET : Channel Secret
LINE_CHANNEL_ACCESS_TOKEN : アクセストークン（ロングターム）

# 7. LINE BOTアカウントの設定
いくつか設定を変更する必要があります。

* Webhook送信 : 利用しない -> 利用する
* Webhook URL : https://[アプリ名].herokuapp.com/webhook
* 自動応答メッセージ : 利用する -> 利用しない

ここまできたらLINEBOTとQRコードでお友達になっておいてくださいね！！

# 8. 動作テスト
![IMG_1537.jpg](https://qiita-image-store.s3.amazonaws.com/0/147118/ed9321e5-e82d-7b90-c4d0-2a3c35a27e0d.jpeg)

あるあるあるある（笑）

# 9. 感想
これ結構おもしろくて、一日中遊んでられますね☻

# 10. 参考情報
https://github.com/line/line-bot-sdk-nodejs
https://qiita.com/yoshizaki_kkgk/items/bd4277d3943200beab26
https://ja.wikipedia.org/wiki/%E3%83%9E%E3%83%AB%E3%82%B3%E3%83%95%E9%80%A3%E9%8E%96
https://www.npmjs.com/package/kuromoji