ガッキー僕を励ましてくれ！！　〜 Node.jsでTwitter Botを作ってみた〜

#はじめに

##どんなもの
[こちら](https://twitter.com/gaki_home_bot)です。
テスト期間で病んでた時に作りました笑
[IMG_2752.JPG](https://qiita-image-store.s3.amazonaws.com/0/155642/6b3d55e0-2af9-a630-3ac4-25a0727f9f33.jpeg)

##流れ
1. ガッキーbotとフォロワー!
になる(ガッキーbotが誰かにフォローされたら自動でフォロバ)
2. フォロワーのマイナス発言にガッキーが反応
3. その人の過去ツイを参照
4. 名詞をgetする
5. それを元に褒めてあげる

##使った環境
サーバーOS➞Ubuntu16.04 LTS(さくらのVPS)
クライアントOS➞Ubuntu14.04 LTS
Node.js バージョン v7.2.1
npm バージョン 3.10.10
forever バージョン v0.15.3 ←スクリプトをサーバー側でデーモン化するときに必要
##npmで使ったパッケージ
twitter
mecab-aync ←形態素解析に利用

#実装方法
##ID登録
まずTwitterの新規IDを作りましょう。

##各種トークンなどを発行
botを作る時に必要なトークンを発行します。

1. [Twitterの公式サイト](https://apps.twitter.com/)を開く
2. Create an applicationを選択
3. もろもろ記入(サイトのURLを記入が必須ですがサイトとか持っていなかったら適当に書いておけばいいと思います)
4. Create your Twitter applicationを選択
5. Key and Access Tokensを選択
6. 下の方にあるCreate my access tokenを選択

これでbotを作る時に必要なものが揃いました。
ここで必要なのはそのページの中で

- Consumer Key(API Key)
- Consumer Secret(API Secret)
- Access Token 
- Access Token Secret

の４つです。
![IMG_2753[1].JPG](https://qiita-image-store.s3.amazonaws.com/0/155642/4f3a08ce-63f6-3b22-522c-ee11ff4e42a6.jpeg)
![IMG_2754[1].JPG](https://qiita-image-store.s3.amazonaws.com/0/155642/6bd28653-1862-2ae8-7e19-85e2bd98f7f2.jpeg)

##パッケージインストール
ここからは端末での作業になっていきます。
自分はサーバー側で実装してしまいましたが、クライアント側で実装してscpとかsftpでサーバーにアップロードとかでもいいと思います。

さて、パッケージをインストールしましょう。

```
npm install twitter -g
npm install mecab-async -g 
```

##最初の環境を実装していく。
名前はapp.jsにしました。
これからのプログラムはapp.jsの下に追加していく感じで行けばいいと思います。

```js:app.js
const Twitter = require('twitter');

// 形態素解析に使う
const MeCab = require('mecab-async');
const mecab = new Mecab()

// 上で取得した各種トークンを記入してください
const client = new Twitter({
    consumer_key:'hogehoge',
    consumer_secret:'hogehoge',
    access_token_key:'hogehoge',
    access_token_secret:'hogehoge'
});
```
## 基本的にやること
上でclientを定義したと思います。それに対して、

```
client.get 
```

または

```
client.post
```

をしていく形をとっていきます。そこでget/postするURLですが、[公式ドキュメント](https://dev.twitter.com/rest/reference)を見るといっぱい載っています。ただ今回使うのは、下だけです。

|使うところ| POST URL | POST 概要 | GET URL | GET 概要 |
|:-:|:-------:|:---:|:---:|:---:|
|1. 自動フォロバ|friendships/create|フォローしてくれる|followers/list|フォロワーを表示してくれる|
|2. マイナス発言チェック|statuses/update|ツイートしてくれる|statuses/home_timeline|自分のTLを見てくれる|
|3. 過去ツイ参照|なし|なし|statuses/user_timeline|指定したユーザーの投稿をみれる|
|4. 名詞をget|なし|なし|なし|なし|
|5. 褒める|statuses/update|ツイートしてくれる|なし|なし|

## 1 自動フォロバ
まずはフォローされたら自動的にフォロバしてくれる関数を作りましょう。
フォローする流れとしては

1. 何秒かに一回フォロワーを10人ほどあさる
2. そのフォロワーに対してガッキーがフォローしているか調べる
3. してなかったらフォロー

という流れです。

```js:app.js
// フォローしているか調べる関数 
const follow_check = (data)=>{

    // フォローしていたらここが条件文はtrue
    if(data["following"]){
        return false;    
    }
    else{

        // フォローしてなくて、フォロリクは送っている場合は除く
        if(data["follow_request_sent"]){
            return false;
        }

       // フォローしてない時のみtrueを返す
        return true;
    };
}

// 自動フォロバ関数
const follow = ()=>{
    
    // 自分のフォロワーを10人まであさってそれをdataに配列としていれてくれる
    client.get('followers/list',{count:10},(error,data,response)=>{
        
        // そのdataの中でもusersオブジェクトに絞る
        for(const key in data["users"]){
            if(follow_check(data["users"][key])){

               // user_idでフォローする人を指定、follow:trueでフォロー
               client.post('friendships/create',{user_id:data["users"][key]["id"],follow: true},(error,data2,response)=>{
                     if(error){
                         console.log(error);
                    }
             })
            }
        }
    })
}
```
ちなみに、getしてきたdataはこんな感じになっています。

```js:端末
{ users:
   [   // 最初の要素
       { id: 111111111111111,   // userのID数字 
       id_str: '111111111111111', 
       name: 'ガッキーbot',  
       screen_name: 'gaki_home_bot', 
// ... ... 
// 長いので途中省略
       following: true, // followしてるか
       live_following: false,
       follow_request_sent: false,　// フォロリク送ってるか
       notifications: true,
       muting: false,
       blocking: false,
       blocked_by: false,
       translator_type: 'none' } 
  // 次の要素
  ,{   id:.....
// 長いので途中省略
       translator_type:'none'}
  ],
  next_cursor: 1558285478716704000,
  next_cursor_str: '1558285478716704017',
  previous_cursor: 0,
  previous_cursor_str: '0' 
}
```

## 2. マイナス発言チェック
twitterから感情値を計算してそれが負なら...
などはしていません。正規表現で大雑把にやってしまいました(笑)
また、今回はstreamを使っていなく、TLをあさってマイナス発言を見つけていくので、重複リプライを防ぐためにそのtweetIDを保管しておく必要があります。

```js:app.js
// リプ送ったIDを保管する場所
let tweetdata = new Set(); 

// TLを検索する関数
const search_new= ()=>{

// 自分のTLをx件まであさってそれをdataにいれる
client.get('statuses/home_timeline',{},(error,data,response)=>{
    for(const key in data){
        if(data[key]["text"]){
            
            // 日本語の正規表現はこんな感じらしい 汚い言葉が連なっててごめんなさい
            if(data[key]["text"].match(/つ{1,1}ら{1,1}い{1,1}/)
               || data[key]["text"].match(/あ{1,1}あ{1,1}/)
               || data[key]["text"].match(/し{1,1}に{1,1}た{1,1}い{1,1}/)
               || data[key]["text"].match(/死{1,1}に{1,1}た{1,1}い{1,1}/)
               || data[key]["text"].match(/く{1,1}そ{1,1}/)
               || data[key]["text"].match(/ク{1,1}ソ{1,1}/)
                ){

                // tweetdataの中にあればリプしない
                if(tweetdata.has(data[key]["id_str"])) {
                }
　　　　　　　　
                // リプライをする関数を実行、tweetdataに追加
                else{
                    tweetdata.add(data[key]["id_str"]);
                    reply(data[key]);

                }
            }
            else{
            
                // おまけ
                if(data[key]["text"].match(/ガ{1,1}バ{1,1}ガ{1,1}バ{1,1}/)){
                    if(tweetdata.has(data[key]["id_str"])){

                    }
                    else{
                        tweetdata.add(data[key]["id_str"]);
                        client.post('statuses/update/',{status:'私ばがばがばじゃないです
！！'},(error1,tweet1,response1)=>{
                            if(!error1){
                            }
                        });
                    }
                }
            }
        }
    }
    return 0;
});
};
```
ここまでで検索してreply関数にdata[key]を渡しています。
ここでのdata[key]はこんな感じです。

```js:端末
{ created_at: 'Sat Apr 04 14:09:39 +0000 2017',
  id: 11111111111111111,  // tweetのID
  id_str: '11111111111111111',
  text: 'ツイート内容', 
  truncated: false,
  entities: { hashtags: [], symbols: [], user_mentions: [], urls: [] },
  source: '<a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>',
  in_reply_to_status_id: null,
  in_reply_to_status_id_str: null,
  in_reply_to_user_id: null,
  in_reply_to_user_id_str: null,
  in_reply_to_screen_name: null,
  user:
   { id: 111111111,
     id_str: '111111111',
     name: 'ほげほげ',
     screen_name: 'hogehoge',
// 途中省略
  lang: 'ja' 
}
```

## 3. 過去ツイ参照 4. 名詞をget 5. 褒める 
reply関数を実装していきます。
reply関数の記述は2でやったsearch_new関数よりも前に書いておきましょう。(前じゃなくても怒られないと思うけれど)
名詞をgetするときに使っているのはMeCabというもので形態素解析を行っています。

``` js:app.js
// 名詞フィルター関数 名詞として選びたくないものはfalseを返す。
const roma = (str)=>{
    if(str.match(/[a-z]/g) || str==='@' || str.match(/[1-9]/)){
        return false;
    }
    else{
        if(str.match(/-/) ){
            return false;
        }
        else{
                if(str.length===1){
                    return false;
                }
                else{
                    if(str==='こと' || str==='もの'){
                        return false;
                    }
                    else{
                        return true;
                    }
                }
        }
    }
}

// リプライ関数
const reply=(data)=>{

    // 過去の100ツイートを調べる
    client.get('statuses/user_timeline',{screen_name:data["user"]["screen_name"],count:100},(error,newdata,response)=>{

    // newdataに過去100ツイが入っているのでまずそれの内容を全部つなげる
    let str = '';
    let number=0;
    for(number;number<newdata.length;number++){
        str = str + newdata[number]["text"];
    }

    // 名詞を入れる配列を用意
    let meisi = [];

　　// つなげられたstrをMeCabを使って形態素解析する
    mecab.parse(str,function (err,result){
    for(let x=0;x<result.length;x++){

            // 名詞あるいは代名詞をgetする
            if(result[x][1]==='名詞' || result[x][1]==='代名詞'){
                
                // 名詞の中でもフィルターである程度は除外する roma関数
                if(roma(result[x][0])){
                    meisi.push(result[x][0]);
                }
            }
        }

        // meisiの中から乱数を用いて何かの名詞を獲得する
        str=meisi[Math.floor(Math.random()*(strrr.length))];
        const tweetid = data["id_str"];
        const tweetID=tl[0].user.screen_name;
        const tweetCo=`@${tweetID} 最近のあなたには${str}があるよ。自信もって!!`;

        // ツイートを送信
        client.post('statuses/update/',{in_reply_to_status_id:tweetid,status:tweetCo},(error,tweet,response)=>{
            if(!error){
            }
        });
    });
})};

```

## 関数をある一定時間ごとに実行する
先ほど定義した関数をsetIntervalを使ってサイクル化します。
またリプライしたIDを保管するtweetdataもたまる一方ですので、定期的に削除します。

```js:app.js
// 時間はミリ秒指定 1000 = 1秒
setInterval(search_new,60000);
setInterval(follow,300000);

// tweetdataを消す関数
const set_clear = () =>{
    tweetdata.clear();
}
setInterval(set_clear,600000000);
```
#最後に
## foreverでデーモン化
これで実装は終了です。最後はこのスクリプト(app.js)をデーモン化しましょう。
**forever**を使用します。
また先ほどまではクライアント側でもよかったのですが、ここからはサーバー側で行わなくてはいけない作業です。

```js:端末
sudo apt install forever  // foreverをインストールしていないなら
forever start app.js
```
## finish
これで終了です。
これは実装した本人が楽しむために作ったものです。もし気分を害された方などいらっしゃいましたらすぐに当Botを停止しようと思っています。その場合は[当アカウント](http://twitter.com/gaki_home_bot)にリプライしてください。

## おまけ
失敗例
がばがばに反応して「がばがばじゃないです」というリプライを送ることが無限ループしてしまった。
![IMG_2755[1].JPG](https://qiita-image-store.s3.amazonaws.com/0/155642/bd3f3ae3-2fbb-f56f-cb0e-c403b73e68f5.jpeg)

#参考
[twitter](https://www.npmjs.com/package/twitter) これだけで基本的にできます。
[mecab-async](https://www.npmjs.com/package/mecab-async) MeCabを使うものでこれが一番やりやすかった。
[Twitter Application Management](https://apps.twitter.com/) 各種トークンを取得するときに使います。
[Application-only authentication — Twitter Developers](https://dev.twitter.com/oauth/application-only) getとpostのURLはこちらで見ました。