GASでツイート自動収集ツールを作ってみた

# はじめに
本記事はサムザップ [Advent Calendar 2022](https://qiita.com/advent-calendar/2022/sumzap) の12/25の記事です。
昨日の記事は@K3nsukeさんの「【Unity】Rider上で非推奨関数/型を検知したい - お手軽に試すRoslynAnalyzer」でした。

この記事では、GASからTwitterAPI v2を使用して、指定したキーワードのツイートを自動収集するツールを作成したので、その作成の流れを紹介したいと思います。

# 作成したツール
スプレッドシートに①記載したキーワード ②除外したいアカウント ③除外キーワードの収集するツイートの条件を設定します。
15分ごとにスプレッドシートに設定された条件とマッチするツイートを検索し、マッチしたツイートがあれば、Slackに対象のツイートのリンクを投稿します。
![スクリーンショット 2022-12-24 20.58.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249963/636e9c71-67f8-aea4-dce4-fa0b354c56d2.png)
![スクリーンショット 2022-12-24 18.54.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249963/10a87292-2374-3452-4358-e61630d70b07.png)

# 作成の流れ
##  1. Twitter APIの使用申請
TwitterAPIを使用するために、Twitterの[Developer Platformページ](https://developer.twitter.com/en/portal/)から使用申請を行い、API Keyを取得する必要があります。  
申請時には基本情報、Twitter APIを使用する目的（英語）などを記入する必要があります。  
申請が完了したら、API Keyが発行されますので、控えておきます。  
発行からしばらく経ってしまうと、API Keyが確認できなくなってしまうので、注意しましょう。
発行されていると[こちらのページ](https://developer.twitter.com/en/portal/projects-and-apps)でAPI Keyを確認することができます。添付画像は発行してから時間がしばらく経過してしまったので、API KEYが既に確認できない状態になってしまっています。
![スクリーンショット 2022-12-24 18.42.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249963/8f8a63ce-33ce-222f-1158-1c39cb6233e0.png)

## 2. Googleスプレッドシートの用意
以下の入力できるスプレッドシートを作成をし、
1. 記載したキーワード
2. 除外したいアカウント
3. 除外キーワード
4. 最新のTweetID（検索する時に使用。普段は非表示にしている）
![スクリーンショット 2022-12-24 21.04.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249963/c4ec073a-12a0-457b-1152-451420b142f4.png)


## 3. GASでTwitterAPIを使用
GASでTwitterAPIを使用し、コーディングしていきます。以下、実装の流れになっています。
### 　シート情報を取得
```js
  //IDを指定して、作成したスプレッドシートを取得する  
  var spreadsheet = SpreadsheetApp.openById('XXXXXXX');

  //検索キーワードなどが記載されているシートを取得する
  const sheet = spreadsheet.getSheetByName('twitterデータ収集ツール');
```
### 検索キーワードの設定
```js
  //シートから検索キーワードを取得する
  let keywords = [];

  //シートの1列目（検索キーワードが記載されている列）の4行-最終行目までの情報を取得する
  sheet.getRange(4, 1, sheet.getLastRow() - 1).getValues().forEach((value) => {
    keywords.push(value[0]);
  });
  
  //Twitter API のクエリ用に整形する
  let keywordsForQuery = "";
  keywords.forEach((keyword, index) => {
    if(keyword === '') {
      return;
    }
      keywordsForQuery += (index === 0 ? `${keyword}` : ` OR ${keyword}`);
  });
```

### 除外したいユーザーの設定
```js
  //除外したいユーザーの一覧を取得する
  let skipUsers = [];
  //シートの2列目（除外ユーザーが記載されている列）の4行-最終行目までの情報を取得する
  sheet.getRange(4, 2, sheet.getLastRow() - 1).getValues().forEach((value) => {
    skipUsers.push(value[0]);
  });

  //Twitter API のクエリ用に整形する
  let skipUsersForQuery = "";
  skipUsers.forEach((userName) => {
    if(userName === '') {
      return;
    }
    skipUsersForQuery += ` -from:${userName}`;
  });
```

### twitter APIを使用し、条件にマッチしたツイートを取得する
```js
const fetchTopics = (keywords, skipUserNames, count, currentLatestTweetId) => {
    //クエリを生成
    // keywords = 検索キーワード　skipUserNames = 除外ユーザー　count = 取得するツイート数（今回は１０にしています）
    // -rt = リツイートを除外の設定
    let query = `query=((${keywords})  (${skipUserNames}) -rt)&expansions=author_id&user.fields=name,username&max_results=${count}`;

  　//シートから最後に検索したツイートのIDを取得する
  　var latestTweetIdCell = sheet.getRange('B1'); 
  　var currentLatestTweetId = latestTweetIdCell.getValue();

    //最後に検索ヒットしたキーワード以降にツイートされたものを取得する。
  　if(currentLatestTweetId　&& currentLatestTweetId !== undefined) {
    　query += `&since_id=${currentLatestTweetId}`;
  　}

  //urlを生成
  const targetUrl = `https://api.twitter.com/2/tweets/search/recent?${query}`;

　//発行されたAPIキーを設定
  const token = "API KEY";
  const options = {
    'method': 'get',
    'headers': {
      'Content-Type': 'application/json',
      'authorization': 'Bearer ' + token,
    },
  };
  return JSON.parse(UrlFetchApp.fetch(targetUrl, options));
}
```

### 検索キーワード、除外ユーザーの条件とマッチしたツイートから除外対象のキーワードを含むツイートを削除
```js
//除外対象のキーワード
let excludeKeywords = [];
  //シートの３列目（除外対象のキーワードが記載されている列）の4行-最終行目までの情報を取得する
  sheet.getRange(4, 3, sheet.getLastRow() - 1).getValues().forEach((value) => {
    excludeKeywords.push(value[0]);
  });

  let excludeData = [];
  excludeKeywords.forEach(excludeKeyword => {
    if(excludeKeyword === '') {
      return;
    }
    excludeData.push(excludeKeyword);
  });
```

### Slackに投稿するための文言を生成する
```js
　//ユーザー名とユーザーIDを紐づける
　let userData = {};
  response.includes.users.forEach((user) => {
    userData[user['id']] = user['username'];
  })

　let regexp = new RegExp(`${excludeData.join('|')}`);

  let tweetLinks = [];
  response.data.forEach((data) => {
    let userName = userData[data['author_id']];
    let tweetId = data['id'];
    
    //対象のツイートリンクを生成
    if(excludeData.length <= 0 || !data.text.match(regexp)) {
      tweetLinks.push(`https://twitter.com/${userName}/status/${tweetId}`);
    }
  })
```

### 一番最後のツイートのIDをシートに書き込んでおく
```js
  if(response.data.length > 0) {
    latest_tweet_id = response.meta['newest_id'];
    latestTweetIdCell.setValue(latest_tweet_id);
  }
```
### Slackへの送信
```js
　 //slackの指定チャンネルへ送信
  const channnelId = "XXXXX";

  tweetLinks.forEach((tweet) => {
    Slack.post(tweet, channnelId);
  });
```

<details>
<summary>コード全文も載せておきます</summary>

```js
function myFunction() {
  var spreadsheet = SpreadsheetApp.openById('XXXXXXX');//IDを指定して、作成したスプレッドシートを取得する
  const sheet = spreadsheet.getSheetByName('twitterデータ収集ツール');//検索キーワードなどが記載されているシートを取得する

  //シートから検索キーワードを取得する
  let keywords = [];
  //1列目（検索キーワードが記載されている列）の4行-最終行目までの情報を取得する
  sheet.getRange(4, 1, sheet.getLastRow() - 1).getValues().forEach((value) => {
    keywords.push(value[0]);
  });
  
  //Twitter API のクエリ用に整形する
  let keywordsForQuery = "";
  keywords.forEach((keyword, index) => {
    if(keyword === '') {
      return;
    }
      keywordsForQuery += (index === 0 ? `${keyword}` : ` OR ${keyword}`);
  });

  //除外したいユーザーの一覧を取得する
  let skipUsers = [];
  //2列目（除外ユーザーが記載されている列）の4行-最終行目までの情報を取得する
  sheet.getRange(4, 2, sheet.getLastRow() - 1).getValues().forEach((value) => {
    skipUsers.push(value[0]);
  });

  //Twitter API のクエリ用に整形する
  let skipUsersForQuery = "";
  skipUsers.forEach((userName) => {
    if(userName === '') {
      return;
    }
    skipUsersForQuery += ` -from:${userName}`;
  });
  
  
  var response = fetchTopics(keywordsForQuery, skipUsersForQuery, 10, currentLatestTweetId);

if(response.meta["result_count"] === 0) {
  return;
}

  let excludeKeywords = [];//除外対象のキーワード
  
  sheet.getRange(4, 3, sheet.getLastRow() - 1).getValues().forEach((value) => {
    excludeKeywords.push(value[0]);
  });

  let excludeData = [];
  excludeKeywords.forEach(excludeKeyword => {
    if(excludeKeyword === '') {
      return;
    }
    excludeData.push(excludeKeyword);
  });


　//ユーザー名とユーザーIDを紐づける
　let userData = {};
  response.includes.users.forEach((user) => {
    userData[user['id']] = user['username'];
  })

　let regexp = new RegExp(`${excludeData.join('|')}`);

  let tweetLinks = [];
  response.data.forEach((data) => {
    let userName = userData[data['author_id']];
    let tweetId = data['id'];
    
    if(excludeData.length <= 0 || !data.text.match(regexp)) {
      tweetLinks.push(`https://twitter.com/${userName}/status/${tweetId}`);
    }
  })

  if(response.data.length > 0) {
    latest_tweet_id = response.meta['newest_id'];
    latestTweetIdCell.setValue(latest_tweet_id);
  }

　 //slackの指定チャンネルへ送信
  const channnelId = "XXXXX";

  tweetLinks.forEach((tweet) => {
    Slack.post(tweet, channnelId);
  });
  
}

const fetchTopics = (keyword, skipUserNames, count, currentLatestTweetId) => {
    //クエリを生成
    // keyword = 検索キーワード　skipUserNames = 除外ユーザー　count = 取得するツイート数（今回は１０にしています）
    let query = `query=((${keyword})  (${skipUserNames}) -rt)&expansions=author_id&user.fields=name,username&max_results=${count}`;

      
  　//シートから最後に検索したツイートのIDを取得する
  　var latestTweetIdCell = sheet.getRange('B1'); 
  　var currentLatestTweetId = latestTweetIdCell.getValue();

    //最後に検索ヒットしたキーワード以降にツイートされたものを取得する。
  　if(currentLatestTweetId　&& currentLatestTweetId !== undefined) {
    　query += `&since_id=${currentLatestTweetId}`;
  　}

  const targetUrl = `https://api.twitter.com/2/tweets/search/recent?${query}`;

  const token = "API KEY";
  const options = {
    'method': 'get',
    'headers': {
      'Content-Type': 'application/json',
      'authorization': 'Bearer ' + token,
    },
  };
  return JSON.parse(UrlFetchApp.fetch(targetUrl, options));
}

class Slack {
  static post(message, channelId) {
    var payload = {
      "token": XXXX,
      "channel": channelId,
      "text": message,
      "username": "twitter収集ツール,
      "icon_emoji": ":robot_face:"
    };

    var params = {
      "method": "post",
      "payload": payload
    };

    var url = "https://slack.com/api/chat.postMessage";

    // Slackに投稿する
    UrlFetchApp.fetch(url, params);
  }
}

```
</details>

##  3. Apps Scriptのトリーガー機能を定期的に検索
15分ごとにApps Scriptのトリーガー機能を使って、条件に合ったツイートを検索
![スクリーンショット 2022-12-24 21.09.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249963/164a25ac-c348-6f2b-9160-001255ab6167.png)


# 最後に
今回は、GASでTwitter APIを利用して、条件を設定し、好みのツイートを自動取集するツールを紹介してきました。私はこのツールを使用して、自分が遊んでいるゲームの情報を収集するために使用しています。スプレッドシートで条件を設定できるようにしているため、簡単に条件を変えることができるのが気に入っています。

今回はTwitter APIの一部の機能しか紹介できませんでした。その他の機能にも興味がある方はぜひ公式のドキュメントを確認してみてください。

https://developer.twitter.com/en/support/twitter-api

最後まで読んでいただき、ありがとうございます。