Lineで匿名チャットルームを作ってみた

## 開発のきっかけ
お腹が痛い時にどことない孤独を感じるのでどうにかしたい
↓
その時の気持ちを共有できる匿名チャットルームがあれば良いのでは？
↓
作るか

## 概要
匿名で会話が出来るLineBotを作っていきます。
なぜLineBotかと言うと、Lineの仕様上匿名のグループチャットは作れない為です。

## 使用技術
Node.js
Heroku

公式リファレンス
https://developers.line.biz/ja/reference/messaging-api/#common-properties

## 事前準備
すでにLineBotでメッセージの送受信ができている前提で話を進めさせていただきます。

こちらの記事を参考にしてとりあえず動くと状態まで開発を進めておいてください。

https://qiita.com/nkjm/items/38808bbc97d6927837cd

## コードの編集
今回主に編集するファイルはindex.jsです。
紹介した記事の通りに作成すると、このようになっているかと思います。

```javascript:index.js
// -----------------------------------------------------------------------------
// モジュールのインポート
const server = require("express")();
const line = require("@line/bot-sdk"); // Messaging APIのSDKをインポート

// -----------------------------------------------------------------------------
// パラメータ設定
const line_config = {
    channelAccessToken: process.env.LINE_ACCESS_TOKEN, // 環境変数からアクセストークンをセットしています
    channelSecret: process.env.LINE_CHANNEL_SECRET // 環境変数からChannel Secretをセットしています
};

// -----------------------------------------------------------------------------
// Webサーバー設定
server.listen(process.env.PORT || 3000);


// -----------------------------------------------------------------------------
// ルーター設定
server.post('/webhook', line.middleware(line_config), (req, res, next) => {
    res.sendStatus(200);
    console.log(req.body);
});

const bot = new line.Client(line_config);

// -----------------------------------------------------------------------------
// ルーター設定
server.post('/webhook', line.middleware(line_config), (req, res, next) => {
    // 先行してLINE側にステータスコード200でレスポンスする。
    res.sendStatus(200);

    // すべてのイベント処理のプロミスを格納する配列。
    let events_processed = [];

    // イベントオブジェクトを順次処理。
    req.body.events.forEach((event) => {
        // この処理の対象をイベントタイプがメッセージで、かつ、テキストタイプだった場合に限定。
        if (event.type == "message" && event.message.type == "text"){
            // ユーザーからのテキストメッセージが「こんにちは」だった場合のみ反応。
            if (event.message.text == "こんにちは"){
                // replyMessage()で返信し、そのプロミスをevents_processedに追加。
                events_processed.push(bot.replyMessage(event.replyToken, {
                    type: "text",
                    text: "これはこれは"
                }));
            }
        }
    });

    // すべてのイベント処理が終了したら何個のイベントが処理されたか出力。
    Promise.all(events_processed).then(
        (response) => {
            console.log(`${response.length} event(s) processed.`);
        }
    );
});
```

このコードに

・ユーザーidの追加機能
・ユーザーidの削除機能
・登録しているユーザー全員へメッセージを転送する機能

これらの機能を追加する為このように変更します。

```javascript:index.js
var fs = require('fs');

const server = require("express")();
const line = require("@line/bot-sdk");

const line_config = {
    channelAccessToken: process.env.LINE_ACCESS_TOKEN,
    channelSecret: process.env.LINE_CHANNEL_SECRET
};

server.listen(process.env.PORT || 3000);

const bot = new line.Client(line_config);

// サーバー設定

server.post('/webhook', line.middleware(line_config), (req, res, next) => {

    res.sendStatus(200);

    var user_ids = require('./user_ids.json');

    function unlink(path) {
      fs.unlink(path, function (err) {
        if (err) {
            throw err;
        }
      });
    }

    function writeFile(path, data) {
      fs.writeFile(path, data, function (err) {
        if (err) {
            throw err;
        }
      });
    }

    req.body.events.forEach((event) => {

        if (event.type == 'follow'){
            function getUniqueStr(myStrong){
                var strong = 1000;
                if (myStrong) strong = myStrong;
                    return new Date().getTime().toString(16)  + Math.floor(strong*Math.random()).toString(16)
            }

            let uuid         = getUniqueStr();
            let line_user_id = event.source.userId;

            user_ids[uuid] = line_user_id;

            unlink('./user_ids.json');
            writeFile('./user_ids.json', JSON.stringify(user_ids));

            console.log('------updated follow function------');
            console.log(user_ids);
            console.log('------updated follow function------');
        }

        if (event.type == 'unfollow'){
            for(key in user_ids){
                if(user_ids[key] == event.source.userId){
                    delete user_ids[key]
                    writeFile('./user_ids.json', JSON.stringify(user_ids));

                    console.log('------updated unfollow function------');
                    console.log(user_ids);
                    console.log('------updated unfollow function------');
                }
            }
        }

        if (event.type == "message" && event.message.type == "text"){
            let message = {
                type: 'text',
                text: event.message.text
            };

            for(key in user_ids){
                if(user_ids[key] != event.source.userId){
                    bot.pushMessage(user_ids[key], message);
                }
            }
        }

    });
});
```

これプラス
ユーザーデータを保存しておくファイルである、user_ids.jsonをindex.htmlと同じ階層に追加しておいてください

```json:user_ids.json
{}
```

編集が完了したら

```bash
$ ./deploy.sh
```

このコマンドでデプロイしてみましょう。
デプロイが完了したら、一度Botをブロックしてから友達追加してみましょう。
正しく編集出来ていたらメッセージの送受信が出来るようになっていると思います。
(一人では確認できないので、友達を招待してください)

ただし、お頭が少々残念な方々を招待しすぎると2chのようなカオスな空間が出来てしまうので注意が必要です。
<img width="599" alt="bot.png" src="https://qiita-image-store.s3.amazonaws.com/0/199666/a9581d27-8026-9015-3357-3c0bda0c43d8.png">

※ルームにそぐわない方は後でBotから排除させて頂きました☆

## コードの説明
追加した処理ごとに説明をしていきます。

### ユーザーの追加機能
```javascript:index.js
        if (event.type == 'follow'){
            function getUniqueStr(myStrong){
                var strong = 1000;
                if (myStrong) strong = myStrong;
                    return new Date().getTime().toString(16)  + Math.floor(strong*Math.random()).toString(16)
            }

            let uuid         = getUniqueStr();
            let line_user_id = event.source.userId;

            user_ids[uuid] = line_user_id;

            unlink('./user_ids.json');
            writeFile('./user_ids.json', JSON.stringify(user_ids));

            console.log('------updated follow function------');
            console.log(user_ids);
            console.log('------updated follow function------');
        }
```
イベントのタイプがfollow(友達追加)だった場合
ランダムに作成した番号とユーザーのLineIdをセットにしたHashを、パースしたuser_idsに追加し、その値を元にuser_ids.jsonを更新しています。

### ユーザーの削除機能
```javascript:index.js
        if (event.type == 'unfollow'){
            for(key in user_ids){
                if(user_ids[key] == event.source.userId){
                    delete user_ids[key]
                    writeFile('./user_ids.json', JSON.stringify(user_ids));

                    console.log('------updated unfollow function------');
                    console.log(user_ids);
                    console.log('------updated unfollow function------');
                }
            }
        }
```
イベントのタイプがunfollow(ブロック)だった場合
ブロックしてきたユーザーのLineIdをuser_ids.jsonから削除しています。

### 登録しているユーザー全員へメッセージを転送する機能
```javascript:index.js
        if (event.type == "message" && event.message.type == "text"){
            let message = {
                type: 'text',
                text: event.message.text
            };

            for(key in user_ids){
                if(user_ids[key] != event.source.userId){
                    bot.pushMessage(user_ids[key], message);
                }
            }
        }
```
イベントのタイプがmessageでしかもそのtypeがtextだった場合(メッセージを送信した場合)
user_ids.jsonに保存していた、メッセージを送信したユーザー以外のLineIdに向かいメッセージを送信しています。

## 動作確認方法
「どんな仕組みで動いてんの？」
「なぜか動かない」
そんな方は
Herokuのログをリアルタイムで見ながら、Lineを操作する事がおすすめです。
下記コマンドで吐き出されたログを絶えず見ることができます。

```bash
heroku logs --tail
```

## データ管理
このままだと、gitを更新するたびにuser_ids.jsonが初期化されてしまうので、user_ids.jsonをgitの対象外にしておきましょう

```bash
git rm --cached user_ids.json
```

## 最後に
もし興味があったら、お腹が痛い人たちBotにぜひ参加してみませんか？
QRコードはこれです。
<img width="160" alt="qr.png" src="https://qiita-image-store.s3.amazonaws.com/0/199666/9193ed29-e0a6-323b-6ac0-4776517b358f.png">


何か面白い匿名チャットを思いついたらぜひ作ってみてください！！