Google Apps Scriptで数式を画像で返すTwitterBotを作ってみた

この記事は[SLP_KBIT Advent Calendar 2016](http://qiita.com/advent-calendar/2016/slp_kbit)の7日目の記事です。

# どんなものつくるの
- 「sqrt{x+2}」のようにリプライを送ると「![chart.png](https://chart.googleapis.com/chart?cht=tx&chl=%5csqrt%7bx%2b2%7d))」のように数式の画像にして返信を返してくれるTwitter bot

# 使用したもの
- 使用言語 : [Google Apps Script](https://developers.google.com/apps-script/ "Google Apps Script")
    - Googleが作ったサーバサイドで動くJavaScript、詳しくは後述記載。
- 画像変換部分 : [Google Charts Tools](https://developers.google.com/chart/ "Google Charts Tools")
    - これまたGoogleが作ったツール、SVGを使ったグラフや、画像化した数式を作れる。
- Twitter送受信 : [TwtterAPI](https://dev.twitter.com/)
    - Twitter公式のAPI。ツイートやＤＭの送受信、タイムラインの情報などを取得できる

# Goolge Apps Scriptとはなんぞや
>GASはJavaScript互換の本格的なサーバーサイドのスクリプト言語です。簡単なスクリプトを記述し、Googleのサーバーで実行することで、Googleが提供する各種のサービスを操り、独自のWebアプリを実現できます。
>(http://ascii.jp/elem/000/000/871/871501/ より引用)

　要するにサーバサイドで動いてくれるJavascriptです。サーバ自分で建てなくてもGoogleちゃんが動かしてくれる。さらにGoogleドライブのファイルへのアクセス、Gmailの送受信など至れり尽くせりである。

# 早速作ってみる
### 1.開発環境を整える
#### 1-1 下準備の下準備
   - まずはGoogleドライブに適当にフォルダを作りスプレッドシートを作成する。
       - 設定などをスプレッドシートで書くことでいじりやすくするため。  
    `ツール- > スクリプトエディタ`でエディタを起動する。

#### 1-2 TwitterAPIを使うための設定
   - Twitterでbotを作るにはアプリ認証が必要なので、OAuth1ライブラリを使って認証させる。
   - スクリプトエディタの```リソース->ライブラリ```を開き、ライブラリの検索にOAuth1ライブラリのプロジェクトキー
  `Mb2Vpd5nfD3Pz-_a-39Q4VfxhMjh3Sh48`
  を入力し、最新バージョンを選択後、保存する。
   - [Twitter Developers](https://apps.twitter.com/app/new)でアプリケーションを作成する。
   - Callback URLは```https://script.google.com/macros/d/[プロジェクト キー]/usercallback```を入力する。
   - プロジェクトキーはスクリプトエディタの```ファイル->プロジェクトのプロパティ```から確認できる。
   - 登録し終わると以下のような画面が出る。

<img width="400px" alt="2f4a05843b35c652fd56dd99ad3a09bf.png" src="https://qiita-image-store.s3.amazonaws.com/0/151975/173525f6-8a6f-5f44-0a5b-de638197da77.png">

 - 次にアクセストークンの発行を行う。```Keys and Access Tokensを```クリックし、ページ下部の```Create my Access token```をクリックして発行する。
 - 先人の知恵をお借りして、OAuth1認証を行う。 
   `https://gist.github.com/kijtra/f4cdd8775277372d42f7`

の、twitter.gsをありがたーーーく使わせてもらう。新規ファイルで「twitter.gs」の名前で保存し、「projectKey」「consumerKey」「consumerSecret」を登録画面やプロジェクトのプロパティを参考にそれぞれ書き換える。

   - ___早速実行して認証・・・と行きたいところだが、現在のバージョン(14)ではOAuth1ライブラリの「setProjectKey」が使えない。現在、プロジェクトキーは非推奨みたい。別にこいつはなくてもいいのでコメントアウトしときましょう。___
   - 最後に```実行->twitterAuthorizeUrl```で実行し、「Ctrl+Enter」で出てくるログに認証用のURLが出ているので開いて認証する。

#### 1-3 ツイートのテスト
 - 先人の知恵により、APIを簡単にたたくことができる。

```javascript
      var option = {
          "status":"山佐 ホットエリア"
      };
      var resp = Twitter.api("statuses/update",option);
```

これだけ。Twitter.apiは第一引数にAPIのURL([REST APIのリフェレンス](https://dev.twitter.com/rest/reference)を参考)、第二引数にパラメータを連想配列で記述する。
 あとはAPIに合わせてPOST/GETし、結果をオブジェクトとして返す。
### 2.リプライを取得して返信する
   - Twitterのリプライを取得するには「statuses/mentions_timeline」を使う。ただしこのままだと過去のすべてのリプライを取得してしまうので、一工夫
   #### 2-1 新しいリプライだけを取得
       - これには「since_id」パラメータを使う。ツイートにはそれぞれIDが振り分けられており、このパラメータにIDを指定することによって、そのIDより高い(新しい)ツイートだけを取得することができる。
       - 流れとしては  
since_idを指定してリプライを取得->処理->一番新しいツイートのIDを次回のsince_idに保存   
       これを繰り返す。

   - TwitterAPIには1分間に一回アクセスするようにする(アクセス制限に引っかかる)
   - 一定時間ごとのGASの実行にはトリガー機能を使用する。
   #### 2-2 返信する
       - 返信するには「statuses/update」の「in_reply_to_status_id」オプションに返信先のツイートIDを指定する。
       - 例：リプライが来たら決まった分を送り返す

```javascript
function Formular_bot(){
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("config");//configという名前のシートを取得
  var last_id = sheet.getRange("last_id").getValue();//最後に取得したリプライのIDをシートから取得
  var retobj = {};
  retobj = getNewReply(last_id);
  last_id = retobj.last_id;
  
  retobj['replies'].forEach(function(value,index,array){
    if(value['in_reply_to_screen_name']!=value['user']['screen_name']){//リプライの送信者と受信者が違うことを確かめる
      var option = {
        "status":"@"+value['user']['screen_name']+" アルゼ 4号機 プラズマアタック",
        "in_reply_to_status_id":value.id_str
      }
      Logger.log(option);
      Twitter.api("statuses/update",option);
    }
  });
  
  sheet.getRange("last_id").setValue(last_id);
}
function getNewReply(last_id){
  resp = {}
  resp['replies'] = Twitter.api('statuses/mentions_timeline',{'since_id':last_id})
  if(resp['replies'].length>0&&'id_str' in resp['replies'][0]){
    last_id = resp['replies'][0]['id_str']//最新のIDを保存しておく
  }
  resp['last_id'] = last_id;
  return resp;
}
```

   - ここまで来たらほぼ完成みたいなものですね

### 3.Google Charts Toolsとの連携
   - 受信したツイートをGoogle Charts Toolsで数式の画
   - これは「https://chart.googleapis.com/chart」 に「cht=tx」と「chl」にTeX形式で指定するだけ。Google有能すぎる
   - Twitterに画像をアップロードするには「[https://upload.twitter.com/1.1/media/upload.json](https://upload.twitter.com/1.1/media/upload.json)」にbase64エンコードしてpostすればいい(api.twitterじゃないので注意！)
   - んでこうなった

```js:formular_bot.gs
function Formular_bot(){
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("config");//configという名前のシートを取得
  var last_id = sheet.getRange("last_id").getValue();//最後に取得したリプライのIDをシートから取得
  var retobj = {};
  retobj = getNewReply(last_id); //リプライを取得する
  last_id = retobj.last_id;
  
  retobj['replies'].forEach(function(value,index,array){
    if(value['in_reply_to_screen_name']!=value['user']['screen_name']){//リプライの送信者と受信者が違うことを確かめる
 
      var intext = value.text;
      intext = intext.slice(intext.indexOf(value.in_reply_to_screen_name)+value.in_reply_to_screen_name.length+1);//文章部分だけを抜き出す
      var option = {
        'cht':'tx',
        'chl':''+intext+''
      };
      var URL = getEncordURL("https://chart.googleapis.com/chart",option);//Google Charts Tool用のURLにエンコード
      var resp = UrlFetchApp.fetch(URL,{'method':'get'});
      
      var resp_blob = resp.getBlob();
      var resp_64 = Utilities.base64Encode(resp_blob.getBytes());//TwitterAPI送信用にbase64にエンコード
      var form_data = {
         'media':resp_blob
      }
      var img_option = {
        'method':"POST",
        'payload':{'media_data':resp_64}
      };
      /*
      /  media/uploadに画像をbase64にエンコードしてPOSTし
      /  statusesのin_replay_to_status_idパラメータに戻ってきたJSONのmedia_id_stringを指定する
      /
      */
      var image_upload = JSON.parse(Twitter.oauth.service().fetch("https://upload.twitter.com/1.1/media/upload.json",img_option));
      var sendmsg = "@"+value['user']['screen_name'];
      var sendoption = {
        'status':sendmsg,
        'media_ids':image_upload['media_id_string'],
        'in_reply_to_status_id':value.id_str
      }
      Twitter.api('statuses/update',sendoption);
    }
  });
  
  sheet.getRange("last_id").setValue(last_id);
}

function getRateLimit(){
  Logger.log(JSON.stringify(Twitter.api('application/rate_limit_status'))); 
}
function getEncordURL(URL,param){
  URL += "?"
  Object.keys(param).forEach(function(key){
    URL+=key+"="+encodeURIComponent(param[key])+"&";
  }); 
  return URL.slice(0,URL.length-1);
}
function getNewReply(last_id){
  resp = {}
  resp['replies'] = Twitter.api('statuses/mentions_timeline',{'since_id':last_id})
  if(resp['replies'].length>0&&'id_str' in resp['replies'][0]){
    last_id = resp['replies'][0]['id_str']//最新のIDを保存しておく
  }
  resp['last_id'] = last_id;
  return resp;
}
```
 - 結果

<img width="400px" alt="CyuCynFUQAApVnp.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/151975/911751da-d0aa-f5d1-edcd-3cc3997fb8d1.jpeg">
やったぜ
# まとめ
 - 不安になるレベルの短さで実装できた。エラー処理とかまだ全然やってないので、本格的に稼働させるにはまだまだ
 - 長い数式だとTwitterの文字制限に引っかかるので、ある程度簡略化した記述方法を考える必要がある。
    それはまた次の機会ということで

# 参考文献・リンク
 - [Twitter APIの使い方まとめ](https://syncer.jp/twitter-api-matome)
 - [Google Apps ScriptでOAuthConfigのサポートが終了してTwitter botが危険そうだったので変更](https://kijtra.com/article/twitter-api-for-google-apps-script-without-oauthconfig/)
 - [GAS(Google Apps Script)でOAuthConfigのサポートが終了](http://yukiblg777.blog.fc2.com/blog-entry-47.html)