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

  • 5
    いいね
  • 0
    コメント

この記事はSLP_KBIT Advent Calendar 2016の7日目の記事です。

どんなものつくるの

  • 「sqrt{x+2}」のようにリプライを送ると「chart.png)」のように数式の画像にして返信を返してくれるTwitter bot

使用したもの

  • 使用言語 : Google Apps Script
    • Googleが作ったサーバサイドで動くJavaScript、詳しくは後述記載。
  • 画像変換部分 : Google Charts Tools
    • これまたGoogleが作ったツール、SVGを使ったグラフや、画像化した数式を作れる。
  • Twitter送受信 : TwtterAPI
    • Twitter公式のAPI。ツイートやDMの送受信、タイムラインの情報などを取得できる

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でアプリケーションを作成する。
  • Callback URLはhttps://script.google.com/macros/d/[プロジェクト キー]/usercallbackを入力する。
  • プロジェクトキーはスクリプトエディタのファイル->プロジェクトのプロパティから確認できる。
  • 登録し終わると以下のような画面が出る。

2f4a05843b35c652fd56dd99ad3a09bf.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を簡単にたたくことができる。
      var option = {
          "status":"山佐 ホットエリア"
      };
      var resp = Twitter.api("statuses/update",option);

これだけ。Twitter.apiは第一引数にAPIのURL(REST APIのリフェレンスを参考)、第二引数にパラメータを連想配列で記述する。
あとは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を指定する。
    • 例:リプライが来たら決まった分を送り返す
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との連携

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;
}
  • 結果

CyuCynFUQAApVnp.jpg
やったぜ

まとめ

  • 不安になるレベルの短さで実装できた。エラー処理とかまだ全然やってないので、本格的に稼働させるにはまだまだ
  • 長い数式だとTwitterの文字制限に引っかかるので、ある程度簡略化した記述方法を考える必要がある。 それはまた次の機会ということで

参考文献・リンク

この投稿は SLP_KBIT Advent Calendar 20167日目の記事です。