GoogleAppsScriptでtwitterの発言をtelegramに転送するBOTを作る

#はじめに
GoogleAppsScriptで特定のユーザの投稿があったらtelegramに投稿するBOTを作るの記事がなかったのでメモとしてまとめおく。
#コード

```javascript
//前回取得したID_strを永続化するため
var properties = PropertiesService.getScriptProperties();

// Twitter AppのConsumer Api Key
var CONSUMER_KEY = "xxxxxxxxxx";
var CONSUMER_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

// 認証URLを取得しログに出力する
function logAuthorizeUri() {
  var twitterService = getTwitterService();
  Logger.log(twitterService.authorize());
}

// OAuth認証をよしなにしてくれるサービスクラスのインスタンスを生成・取得する
function getTwitterService() {
  return OAuth1.createService('Twitter')
      .setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
      .setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
      .setAuthorizationUrl('https://api.twitter.com/oauth/authenticate')
      .setConsumerKey(CONSUMER_KEY)
      .setConsumerSecret(CONSUMER_SECRET)
      // リダイレクト時に実行されるコールバック関数を指定する
      .setCallbackFunction('authCallback')
      // アクセストークンを保存するPropertyStoreを指定する
      .setPropertyStore(PropertiesService.getUserProperties());
}

// リダイレクト時に実行されるコールバック関数
function authCallback(request) {
  var twitterService = getTwitterService();
  // ここで認証成功時にアクセストークンがPropertyStoreに保存される
  var isAuthorized = twitterService.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput('Success');
  } else {
    return HtmlService.createHtmlOutput('Denied');
  }
}
//データを永続化するために使用
function loadCount() {
  var count = properties.getProperty("Count");
  if (!count) return 0;
  return count;
}

function saveCount(count) {
  properties.setProperty("Count", count);
}

function isThisNewPost(newStr,oldStr){
  const pad0 = num => ('0'.repeat(21)+num).slice(-20)
  return pad0(newStr)>pad0(oldStr)
}

function getUserPostdata() {
  //監視したいユーザのIDを入れる
  const getTwitterName = 'hokusorailway'
  const maxGetPostCount = 10
  var twitterService = getTwitterService();
  if (twitterService.hasAccess()) {
    var url = 'https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=' + getTwitterName + '&count=' + maxGetPostCount;
    var response = twitterService.fetch(url, {
      method: "get",
      muteHttpExceptions: true,
      //payload: parameters
    });
    var result = JSON.parse(response.getContentText());
    //取得したデータは配列の1番目が最新のデータとなっているので、逆順にして、古いデータから処理するようにする
    result.reverse();
    for(const post of result){
      var nowIdstr = post["id_str"]
      var twText = post["text"]
      Logger.log(nowIdstr + " Text" + twText)
      //Logger.log(JSON.stringify(result, null, 2));
      
      //前回のpostIDをロードする
      var oldId_str =  loadCount();
      
      //Logger.log(oldId_str)
      if(isThisNewPost(nowIdstr,oldId_str)){
      //新しい投稿が出た場合には保存してTGへ投稿する
        saveCount(nowIdstr)
        Logger.log("新しい投稿があったのでTGに投稿します")
        var tweetURL = 'https://twitter.com/' + getTwitterName + '/status/' + nowIdstr
        var tgPostBody = twText;
        sendMessage(tgPostBody)
      }else{
        //新しい投稿がない場合には何もしない
        Logger.log("新しい投稿はありません")
        
      }
   }    
  } else {
    Logger.log(service.getLastError());
  }
}
//test message from gas
function testTwitterPost(){
  postRequest('statuses/update.json',{"status":"hello from gas"});
}
// TwitterにAPIリクエストを送る
function postRequest(api_url, parameters) {
  var twitterService = getTwitterService();
  if (twitterService.hasAccess()) {
    var url = 'https://api.twitter.com/1.1/' + api_url;
    var response = twitterService.fetch(url, {
      method: "post",
      muteHttpExceptions: true,
      payload: parameters
    });
    var result = JSON.parse(response.getContentText());
    Logger.log(JSON.stringify(result, null, 2));
  } else {
    Logger.log(service.getLastError());
  }
}


// send to telegram
function testSendMessage(){
  sendMessage('hello tg')
}

function sendMessage(postMessage) {
  // 以下2行は自分の環境に合わせて設定してください
  var chatId="xxxxxxxxxxxx";
  var token="xxxxxxx:xxxxxxxxxxxx";

  sendTelegram(chatId, postMessage, token);

}
function sendTelegram(chatId, text, token) {
  var payload = {
    'method': 'sendMessage',
    'chat_id': chatId,
    'text': text,
    'parse_mode': 'HTML'
  }

  var data = {
    'method': 'post',
    'payload': payload
  }

  UrlFetchApp.fetch('https://api.telegram.org/bot' + token + '/', data);

}
```
#使用前の準備
##ライブラリの登録とか
以下を参考にして、ライブラリの登録・コールバックの変更をする。
[\[GAS\] GoogleAppsScriptでTwitterbotを作る \- Qiita](https://qiita.com/k7a/items/e6a456bec26b4e667c47)
Twitterでアプリの申請をしてなければする。自分が過去に作成したものは[Twitter Developers](https://developer.twitter.com/en/apps)で見ることができる。
参考URLをもとに、logAuthorizeUri()を実行して、アカウントでアプリ認証させる。
##動作テスト
APIを叩けるかを確認するためにtestSendMessage()を実行する。アプリ認証したtwitterアカウントで「hello from gas」と発言が確認できればよい。

#作成したコードの解説
##特定ユーザの発言の取得
特定ユーザの発言の取得は以下のAPIを叩いている。直接ユーザのタイムラインを見に行っている。

```javascript
https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=' + getTwitterName + '&count=' + maxGetPostCount
```

APIには以下の公式のサイトを参照。
[GET statuses/user\_timeline \| Docs \| Twitter Developer](https://developer.twitter.com/en/docs/twitter-api/v1/tweets/timelines/api-reference/get-statuses-user_timeline)
##新しいものだけを取得
重要な部分だけを一部抜粋する。まず、取得してJSONでパース処理をした、resultという配列を逆順にする。なぜそうるのかというと、コメントにある通り、取得したデータは配列の1番目が最新のデータとなっているので、古い順番から処理するためである。

```javascript
//取得したデータは配列の1番目が最新のデータとなっているので、逆順にして、古いデータから処理するようにする
result.reverse();
for(const post of result){
	var nowIdstr = post["id_str"]
	var oldId_str =  loadCount();
	if(isThisNewPost(nowIdstr,oldId_str)){
	 	saveCount(nowIdstr)
		sendMessage(tgPostBody)
}
```
###データの永続化
その後、var nowIdstr = post["id_str"]で発言のIDを取り取り出す。loadCount();で古いデータを呼び出す。GASでデータの永続化するためにはいくつかある。詳細については[GASで永続化する方法まとめ（設定や処理結果を保存・読み込みしたい時） \- Qiita](https://qiita.com/tomboyboy/items/6a15ab1a4838bd6b7f9d)を参照してください。私は、今回については永続化したい値が一つだけであることからProperties Service を使用した。先程紹介したURLにもあるように、この方法だと何が保存されているのかがわからないのでディバッグするときなどはLogger.log(oldId_str)などをして、値が何が保存されているのかを確かめた方がいいとは思う。

###IDの新旧比較
古いIDと新しく取得したIDを比較して、そのIDが新しいのかを調査する必要がある。それが、isThisNewPost関数である。詳細については[JavaScriptで手っ取り早く64bit符号なし整数文字列の大小関係を比較 \- みちのいに\!\!](http://m77.hatenablog.com/entry/2018/06/03/033011)を参照して欲しい。簡単に説明すると、文字列のまま比較すると比較する桁数が異なる場合に大小関係が正しく判定されないということである。正直、今のtwitterの桁数が増えて次に桁上りするのはいつなのかよくわからないが、その時に頭を悩ますのが嫌なので桁数が増えても正しく判定できるようにした。

```javascript
function isThisNewPost(newStr,oldStr){
  const pad0 = num => ('0'.repeat(21)+num).slice(-20)
  return pad0(newStr)>pad0(oldStr)
}
```
#おわりに
GASの無料でここまで遊べるいい時代が来たなと思った。某位置ゲームの連絡手段としてtelegramを使用している。巷で流行っているチャットソフトよりも使いやすいと感じているが、あまり使っている人がいなくて残念だなと思っている。