GoogleAppsScriptでtwitterの発言をtelegramに転送するBOTを作る (複数人監視)

#はじめに
(追記)
リストでデータを取得する方法について投稿しました。[GoogleAppsScriptでtwitterの発言をtelegramに転送するBOTを作る\(リストで複数人監視版\) \- Qiita](https://qiita.com/k32ru/items/1356eff28ff73a7bda14)

前回の記事で<a href="https://qiita.com/k32ru/items/3e85cc1bdfbbd97a4dee">GoogleAppsScriptでtwitterの発言をtelegramに転送するBOTを作る </a>というものを作った。これは一人のひとしか対応してないので複数人の発言を監視するBOTを作れないかと思ったのが今回の始まりである。前回のを改造して、複数人に対応しても監視する対象が増えてくると、APIがリミットに達するのではないかと思い、今回は`statuses/home_timeline.json` を叩いて、ホームのタイムラインに流れる人の中で監視対象の人があれば、TGに発言を転送するようにした。そのため、今回のプログラムで発言を監視するためにはフォローしている必要がある。

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

// Twitter AppのConsumer Api Key
var CONSUMER_KEY = "xxxxxxxxxxxxxxxxxxx";
var CONSUMER_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

// 認証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');
  }
}


//post test message
function postMessage(){
  postRequest('statuses/update.json', { status: "test form gas" });
}
//データを永続化するために使用
function loadCount() {
  var count = properties.getProperty("Count");
  if (!count) return 0;
  return count;
}

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

function setLastTweetIDForce(){
  var twitterService = getTwitterService();
  if (twitterService.hasAccess()) {
      var url = 'https://api.twitter.com/1.1/statuses/home_timeline.json?count=3';
      Logger.log(url);
      var response = twitterService.fetch(url, {
      method: "get",
      muteHttpExceptions: true,
    });
  }else {
    Logger.log(service.getLastError());
  }
   var result = JSON.parse(response.getContentText());
   Logger.log(JSON.stringify(result, null, 2));
   saveCount(result[0]["id_str"])
   Logger.log("Seted lastID:" + result[0]["id_str"])
   var postIdstr = result[0]["id_str"]
   var twName = result[0]["user"]["name"]
   var twText = result[0]["text"]
   Logger.log(postIdstr + " " + twName + " " + twText)
  
}
function getHomeTimeLie(maxGetPostCount,sinceID){
  var twitterService = getTwitterService();
    if (twitterService.hasAccess()) {
      var url = 'https://api.twitter.com/1.1/statuses/home_timeline.json?count=' + maxGetPostCount + sinceID;
      var response = twitterService.fetch(url, {
      method: "get",
      muteHttpExceptions: true,
      });
      var getData = response.getContentText();
    } else {
      Logger.log(service.getLastError());
   } 
  return getData;
}
//タイムラインに流れてくる複数の特定のユーザIDのデータをTGに投稿する
function getMutiUserPostdata(){
  const maxGetPostCount = 10
  //監視する特定ユーザ。このユーザはフォローしている必要がある。
  const watchUsers = ['ingress','IngressJapan','NianticLabs','niantic_inc','NianticJP','NianticHelpJP','konotarogomame']
  var sinceID =''; 
  lastTweetID =  loadCount();
  if(lastTweetID == 0) 
  {
    sinceID ='';
  }else{
      sinceID = '&since_id=' + lastTweetID;
  }
   var getData = getHomeTimeLie(maxGetPostCount,sinceID)
   try {
         //例外が起きる処理
         var result = JSON.parse(getData);
         //次回このID以降のデータを取得するため、最新のツイートのIDを保存する。
         saveCount(result[0]["id_str"])
         Logger.log("lastID:" + result[0]["id_str"])
         //Logger.log(JSON.stringify(result, null, 2));
    }
    catch(e){
      //例外後の処理
      //何も読み込むデータがないと、「 エラーが発生。response:{"errors":[{"code":32,"message":"Could not authenticate you."}]}」と応答がくるみたい。
      Logger.log("エラーが発生。response:" + getData);
      return -1;
    }
    //取得したタイムライン　ディバッグ用
    for(const post of result){
        var postIdstr = post["id_str"]
        var twName = post["user"]["name"]
        var twScName = post["user"]["screen_name"]
        var twText = post["text"]
        Logger.log(postIdstr + " " + twName + twScName + " " + twText)
        
    }
    
    //取得したデータは配列の1番目が最新のデータとなっているので、逆順にして、古いデータから処理するようにする
    result.reverse();
    for(const post of result){
      for(const user of watchUsers){
        if(user == post["user"]["screen_name"]){
          var postIdstr = post["id_str"]
          var twName = post["user"]["name"]
          var twScName = post["user"]["screen_name"]
          var twText = post["text"]
          var tweetURL = 'https://twitter.com/' + twScName + '/status/' + postIdstr
          var tgPostBody =  twName + "(" + twScName + ")" +  "\n" + tweetURL + "\n" + twText;
          Logger.log(tgPostBody)
          sendMessage(tgPostBody)
          //Logger.log(JSON.stringify(result, null, 2));
        }
      }
    }
}

// send to telegram
function testSendMessage(){
  sendMessage('test message from gas')
}
function sendMessage(postMessage) {
  // 以下2行は自分の環境に合わせて設定してください
  var chatId="xxxxxxxxxxxxxxxxx";
  var token="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

  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)
[GoogleAppsScriptでtwitterの発言をtelegramに転送するBOTを作る \- Qiita](https://qiita.com/k32ru/items/3e85cc1bdfbbd97a4dee)
#使い方
`getMutiUserPostdata()`をGASで適当なタイミングで実行するようにしてください。
#コード解説
##監視するユーザ一覧
`const maxGetPostCount = 10`はタイムラインからの情報を取得する時にどくのらいの数を取得するかである。タイムラインが早い人が人は数を多くするとよいかもしれません。`const watchUsers = `は監視する人(ウオッチ対象)を配列で書きます。

```javascript
  const maxGetPostCount = 10
  //監視する特定ユーザ。このユーザはフォローしている必要がある。
  const watchUsers = ['ingress','IngressJapan','NianticLabs','niantic_inc','NianticJP','NianticHelpJP','konotarogomame']
```

##タイムラインの取得
タイムラインの取得は`getHomeTimeLie`でやっています。 中身はこのような感じになって`home_timeline.json`を叩いています。APIの使用については公式の[GET statuses/home\_timeline \| Docs \| Twitter Developer](https://developer.twitter.com/en/docs/twitter-api/v1/tweets/timelines/api-reference/get-statuses-home_timeline)を参照してください。

```javascript
function getHomeTimeLie(maxGetPostCount,sinceID){
  var twitterService = getTwitterService();
    if (twitterService.hasAccess()) {
      var url = 'https://api.twitter.com/1.1/statuses/home_timeline.json?count=' + maxGetPostCount + sinceID;
      var response = twitterService.fetch(url, {
      method: "get",
      muteHttpExceptions: true,
      });
      var getData = response.getContentText();
    } else {
      Logger.log(service.getLastError());
   } 
  return getData;
}
```

ここで一番重要なのは最新のツイートのみをどのように取得するかということです。便利なことにAPIにはその機能が`since_id`という形で用意されています。`home_timeline.json?count=10&since_id=1307952781752324096`とAPIを叩くと、それ以降(未来)のタイムラインを10個持ってくる事ができます(since_id=で指定したツイートは含まれません)。この関数内のsinceIDのは`'&since_id=' + lastTweetID;`となっていて、前回取得したタイムラインの最新のツイートIDをlastTweetIDに持っています(取得方法は後述)。なので、実行するたびに前回より新しく投稿(ツイート)された内容が取得できます。
##前回のタイムラインの最新ツイートの取得
前回のタイムラインの最新ツイートの取得は以下の部分で行われています。説明に不要な箇所は省いています。タイムラインをデータはJSONデータをパース後に配列で`result`に入っています。この配列では配列の一番最初にあるデータが最新のデータとなっていため、`result[0]["id_str"]`がタイムラインで取得した最新のツイートIDが入っています。その値を次回も使用するため保存します。

```javascript
 var getData = getHomeTimeLie(maxGetPostCount,sinceID)
 try {
	var result = JSON.parse(getData);
	//次回このID以降のデータを取得するため、最新のツイートのIDを保存する。
	 saveCount(result[0]["id_str"])　
}
catch(e){
	Logger.log("エラーが発生。response:" + getData);
}
```

なぜ`try`と`catch`をつかって、例外処理をしているのかというと、`home_timeline.json?count=10&since_id=xxxxxx`を叩いた時に、ツイートIDxxxxxx以降のデータが無い時に、`{"errors":[{"code":32,"message":"Could not authenticate you."}]}`という応答がくるみたいだからである。みたいというのは多分、そうなんだろうというで断言ができないからである。このエラーデータはJSONでパース処理をしよとすると出来ないので、例外処理として処理をするようにした。

##監視対象ユーザの発言をTGへ転送
タイムラインから取得したデータは配列の最初が一番新しいので、それを逆順`result.reverse();`にして監視対象かを判定する。
`for(const post of result){`でタイムラインの個々のツイート(投稿)ごとに`post`という形にバラす。そうすると名前は`post["user"]["screen_name"]`という形で取れるので、そこで`watchUsers`を個別にばらした`user`と比較して、同じ名前であればTGへ投稿するようにする。

```javascript
    result.reverse();
    for(const post of result){
      for(const user of watchUsers){
        if(user == post["user"]["screen_name"]){
```
##TGへの転送内容
TGへの発言は以下の部分で組み立てているので、お好きなように変更すればいいと思います。

```javascript
var postIdstr = post["id_str"]
var twName = post["user"]["name"]
var twScName = post["user"]["screen_name"]
var twText = post["text"]
var tweetURL = 'https://twitter.com/' + twScName + '/status/' + postIdstr
var tgPostBody =  twName + "(" + twScName + ")" +  "\n" + tweetURL + "\n" + twText;
Logger.log(tgPostBody)
sendMessage(tgPostBody)
```

#おわりに
タイムラインから流れてくるデータを処理することにより、APIの消費を少なくて特定のユーザの投稿を監視できるようになった。ただ、世の中にはフォローはしたくない(していることが相手にバレたくない)人のツイートを監視したいニーズもあるのかもしれないとこの記事を書いていて思いました。多分そういう人は普段リストに入れて監視していると思われるので、叩くAPIを変えてリストからデータを取ってくればいいのかなと思いました。