🐷

[Twitter API]ビジネスにも!?個人開発にも!?使い方無限大!無料で始めれる最強のAPI使ってみた!みんなも急げ!

2023/02/06に公開

初めに

無料で使えるのは2月の9日までらしいので9日超えたら有料です。
はーいお疲れ様でしたぁ~🤗課金課金チャリンチャリン~♪
https://twitter.com/TwitterDev/status/1621026986784337922?s=20&t=9u-W1yEcYESYqPnHKttpwA

ということでタイトルはちょっとネタっぽく書いてみましたが、
まだ無料だしせっかくなので今回はTwitterのAPIを使って触ってみたいと思い記事を書きました。

ではさっそく作って行く

  • 今回作るもの
    Gas(Google App Script)を使って、Twitterのトレンド情報、トレンドをキーワードにツイートを取得してDiscordのチャンネルに投稿する至って普通のBotを作って行こうと思います。

  • 使用するエンドポイント

    • Twitter API v1.1のAPIを使います。(v1系はもう古いです)
    • Discord Web hook URL

アカウント作成

まずTwitterのAPIを使うにはTwitterのアカウントが必要です。
お手持ちのアカウントを使ってDeveloper登録してください。
Twitter developer

開くとこんな感じの画面が出てくるので日本語を翻訳して適当にLet's do thisしてください。

アプリ作成

アカウントの確認メールが届き認証すると、アプリ作成画面が表示されるので適当に名前決めます(すでに使われてる名前だとエラーになります)

Gey keysを押して次の画面に行くとAPI Key、Secret、Bearer Tokenが表示されます。
今回はBearer Tokenだけあればいいので、これだけどっかにコピペしておきます。

APIリクエストしてみる

先ほどのBearer Tokenでツイートのトレンド情報は取得できるようになりました。

curlやpostmanなどでつながるか試してみてください。トレンド取得をするエンドポイント[1]https://api.twitter.com/1.1/trends/place.jsonにBearer Tokenを貼り付けGETすると値が返ってくるはずです。

:::detailsちなワイはpostmanでやってみました
こんな感じにresponseが返ってきてたらOKです

:::

Elevated Accessの申請していく

次にトレンドに含まれるキーワードを使ってツイートを検索したいので、ツイートを検索するエンドポイント[2]を叩きたいのですが、このエンドポイントは先のBearer Tokenだけだとエラーではじかれてしまいます。このエンドポイントには別途Elevated Accessに昇格する必要があります。

Elevated Accessの申請はもちろん無料でできるのでこのまま申請していきます。最終的に申請が通るとツイート検索APIがリクエストできるようになります。(検索も一定数まではもちろん無料!うーんリョウシンテキィ!!!)

申請方法などはこちらが分かりやすいと思うのでこちらをみて申請してみてください(丸投げ
https://tensei-shinai.com/2022/04/27/twitter-api-elevated/

アプリの利用方法をある程度詳しく書けば、そこまで許可もらうこと自体は難しくなかったです。

:::details一応自分の時のTwitterとのやり取りを貼っておきます

  • とりあえず申請結果待ち
  • 朝方メール来てた。メール返信してくれとの事なので以下で返信送信
  • 無理やった
  • 再度返信 とりあえず分からんところは分からんで返したった
  • 審査通った

    審査申請して1日で承認もらった
    日本語でも英語でも多分どちらでも良かったかなと思う。
    :::

Gasでコードを作って行く

Google App Scriptページから新しいプロジェクトを適当な名前で作成します。

VSCodeなどエディタでやりたい場合は別途claspなどを入れる必要があります。やりたい方は調べてみてください。

今回はそのままGas上で処理を書いていきます。
そこまで複雑ではないのでコードをまとめて書いて、適当に説明だけしていきます。
めんどくさいわけではないです。はい。

:::details最終的なコード

/**
 * 定数
 */
// https://developer.twitter.com/en/docs/twitter-api/v1/trends/locations-with-trending-topics/api-reference/get-trends-available
// idは上記URLから取得可能なリストを取得できる
// 23424856はjapanのparent id
const twitterTrendEndpoint =
  "https://api.twitter.com/1.1/trends/place.json?id=23424856";

const twitterSearchEndpoint = "https://api.twitter.com/1.1/search/tweets.json";

const props = PropertiesService.getScriptProperties().getProperties();
// -- Twitter --
const ACCESS_TOKEN = props.ACCESS_TOKEN;
// -- Discord --
const DISCORD_WEBHOOK_URL = props.DISCORD_WEBHOOK_URL;

/**
 * Discordへのトレンド情報を通知処理
 */
function sendTrendToDiscord(trendName, trendUrl) {
  const embeds = [
    {
      title: `トレンドワード: ${trendName}`,
      description: `${trendName}についてのツイートだよ。もっと見たい場合は上のリンクをクリックしてね`,
      url: trendUrl,
      color: 5620992,
      timestamp: new Date(),
      author: {
        name: "Twitterトレンド紹介",
        url: trendUrl,
        icon_url: "https://avatars.githubusercontent.com/u/50278?v=4",
      },
    },
  ];

  const payload = {
    username: "TwitterTrendsBot",
    avatar_url: "https://avatars.githubusercontent.com/u/50278?v=4", // twitterのpng
    content: "", // MEMO: 上限2000文字
    embeds: embeds,
  };

  /**
   * embedとかも使える
   * https://qiita.com/Eai/items/1165d08dce9f183eac74#embed
   */

  const options = {
    method: "post",
    muteHttpExceptions: true,
    contentType: "application/json",
    payload: JSON.stringify(payload),
  };
  UrlFetchApp.fetch(DISCORD_WEBHOOK_URL, options);
}

/**
 * Discordへのツイート情報を通知処理
 */
function sendTweetToDiscord(tweet) {
  const payload = {
    username: "TwitterTrendBot",
    avatar_url: "https://avatars.githubusercontent.com/u/50278?v=4", // twitterのpng
    content: `https://twitter.com/${tweet.user.screen_name}/status/${tweet.id_str}`, // MEMO: 上限2000文字
  };

  const options = {
    method: "post",
    muteHttpExceptions: true,
    contentType: "application/json",
    payload: JSON.stringify(payload),
  };
  UrlFetchApp.fetch(DISCORD_WEBHOOK_URL, options);
}

/**
 * Twitter trends fetch
 */
function fetchTwitterTrends() {
  const url = twitterTrendEndpoint;
  const options = {
    method: "get",
    muteHttpExceptions: true,
    headers: {
      authorization: "Bearer " + ACCESS_TOKEN,
    },
  };
  const response = JSON.parse(UrlFetchApp.fetch(url, options));
  const trends = response[0]["trends"] ?? [];
  return trends;
}

/**
 * Twitter tweets fetch
 */
function fetchSearchTweet(query) {
  /**
   * twitterのsearch APIを使ってツイートを取得する
   * 現在設定してるパラメーター
   * q: 指定するキーワード
   * count: 取得するツイート数
   * result_type: 取得するツイートのタイプ(popular, mixed, recent)
   * locale: 言語
   * tweet_mode: 140字で省略される場合に全文表示するか(extendedで全文表示)
   * include_entities: entitiesを含めるかどうか
   * その他のクエリや詳細情報は以下リンク参照
   * https://developer.twitter.com/en/docs/twitter-api/v1/tweets/search/api-reference/get-search-tweets
   */
  const url = `${twitterSearchEndpoint}?q=${query}&count=1&result_type=mixed&locale=ja&tweet_mode=extended&include_entities=false`;
  const options = {
    method: "get",
    muteHttpExceptions: true,
    headers: {
      authorization: "Bearer " + ACCESS_TOKEN,
    },
  };

  const response = JSON.parse(UrlFetchApp.fetch(url, options));
  const statuses = response["statuses"] ?? []; // responseのキーがstatusesなのでstatusesで取得
  return statuses;
}

/**
 * メイン
 */
function main() {
  const trends = fetchTwitterTrends();
  /**
   *   response example
   * https://developer.twitter.com/en/docs/twitter-api/v1/trends/trends-for-location/api-reference/get-trends-place
   *{
   * "trends": [
   *  {
   *   "name": "#GiftAGamer",
   *  "url": "http://twitter.com/search?q=%23GiftAGamer",
   * "promoted_content": null,
   *       "query": "%23GiftAGamer",
   *      "tweet_volume": null
   *   },
   *  ...
   *  ]
   *}
   */
  let index = 0;
  for (const trend of trends) {
    // 4件以上のトピックは表示しない
    if (index > 3) break;
    index++;
    const trendName = trend.name ?? "";
    // MEMO: responseのurlはhttpなのでhttpsになるように変更
    const trendUrl = trend.url.replace("http", "https") ?? "";
    sendTrendToDiscord(trendName, trendUrl);
    // 遅延処理
    Utilities.sleep(500);

    const query = trend.query ?? "";
    tweets = fetchSearchTweet(query);
    for (const tweet of tweets) {
      sendTweetToDiscord(tweet);
    }
    // 遅延処理
    Utilities.sleep(3500);
  }
}

:::


環境変数にアクセスする

Gasでは環境変数にアクセスしてトークンなどのハードコーディングを避けることができます
::: details余談
自分ずっと環境変数ってこういう大切な情報を保持しておく所だと思ってたんですが、最近だとAWSやGCPなどでSecret Managerと呼ばれるものを使って管理するのがスタンダードのようです。ざっくりいうとデータを暗号化して置くことで読み取られないようにする技術です。
こういう大切な情報は秘匿情報、秘密情報、機密情報などと言われるそうです。個人的には秘匿情報がなんかシャレオツなんで好きです。
:::

// 環境変数にアクセスする
const props = PropertiesService.getScriptProperties().getProperties();
// -- Twitter --
const ACCESS_TOKEN = props.ACCESS_TOKEN;
// -- Discord --
const DISCORD_WEBHOOK_URL = props.DISCORD_WEBHOOK_URL;

このプロパティの値は設定タブの下にあるスクリプトプロパティから追加できます。

props.[変数名]で呼び出せるので、
今回だとACCESS_TOKENDISCORD_WEBHOOK_URLを登録していきます。
ACCESS_TOKENはコピペしてたBearer Tokenを貼り付けます。
DISCORD_WEBHOOK_URLはdiscordでwebhookURLを取得します。ここら辺はググって下さいまし。

個々の関数ざっくり説明

1. ツイッターのトレンド情報を取得
  const twitterEndpoint =
   'https://api.twitter.com/1.1/trends/place.json?id=23424856';
  ~~~~~
  function fetchTwitterTrends() {
    const url = twitterEndpoint;
    const options = {
      method: 'get',
      muteHttpExceptions: true,
      headers: {
        authorization: 'Bearer ' + ACCESS_TOKEN,
      },
    };
    const response = JSON.parse(UrlFetchApp.fetch(url, options));
    const data = response[0]['trends'] ?? [];
    return data;
  }
  ~~~~~
  const trends = fetchTwitterTrends();

idにしている23424856はjapanのidを指定しています。その他指定できる地域のidは下記から取得できます
https://developer.twitter.com/en/docs/twitter-api/v1/trends/locations-with-trending-topics/api-reference/get-trends-available

基本的には、Gasで使えるUrlFetchAppを使って指定のエンドポイントにアクセスしてfetchリクエストしているだけです。
特にひねりはないですが、唯一trendsの構造が最初配列になっているため[0]を指定してアクセスしてます。そうなってる理由は知りません。

2. キーワードからツイート取得

クエリにキーワードを入れてツイート情報を取得します。

const twitterSearchEndpoint = "https://api.twitter.com/1.1/search/tweets.json";
~~function fetchSearchTweet(query) {
  /**
   * twitterのsearch APIを使ってツイートを取得する
   * 現在設定してるパラメーター
   * q: 指定するキーワード
   * count: 取得するツイート数
   * result_type: 取得するツイートのタイプ(popular, mixed, recent)
   * locale: 言語
   * tweet_mode: 140字で省略される場合に全文表示するか(extendedで全文表示)
   * include_entities: entitiesを含めるかどうか
   * その他のクエリや詳細情報は以下リンク参照
   * https://developer.twitter.com/en/docs/twitter-api/v1/tweets/search/api-reference/get-search-tweets
   */
  const url = `${twitterSearchEndpoint}?q=${query}&count=1&result_type=mixed&locale=ja&tweet_mode=extended&include_entities=false`;
  const options = {
    method: "get",
    muteHttpExceptions: true,
    headers: {
      authorization: "Bearer " + ACCESS_TOKEN,
    },
  };

  const response = JSON.parse(UrlFetchApp.fetch(url, options));
  const statuses = response["statuses"] ?? []; // responseのキーがstatusesなのでstatusesで取得
  return statuses;
};

クエリの文字のエスケープとかはトレンドで返ってきたキーワードはエスケープされてるのでそのまま渡します。まぁ内部でやるだけなのでそこまで気にしないです。

それ以外は大体おんなじ感じです

3. discordに送信

Discordにトレンド情報とツイート情報を投稿する関数です
トレンド情報はembed(埋め込み投稿)、ツイート情報はそのまま(URL)のみ投稿という形でやっていきます。

function sendTrendToDiscord(trendName, trendUrl) {
  const embeds = [
    {
      title: `トレンドワード: ${trendName}`,
      description: `${trendName}についてのツイートだよ。もっと見たい場合は上のリンクをクリックしてね`,
      url: trendUrl,
      color: 5620992,
      timestamp: new Date(),
      author: {
        name: "Twitterトレンド紹介",
        url: trendUrl,
        icon_url: "https://avatars.githubusercontent.com/u/50278?v=4",
      },
    },
  ];

  const payload = {
    username: "TwitterTrendsBot",
    avatar_url: "https://avatars.githubusercontent.com/u/50278?v=4", // twitterのpng
    content: "", // MEMO: 上限2000文字
    embeds: embeds,
  };

  const options = {
    method: "post",
    muteHttpExceptions: true,
    contentType: "application/json",
    payload: JSON.stringify(payload),
  };
  UrlFetchApp.fetch(DISCORD_WEBHOOK_URL, options);
}

/**
 * Discordへのツイート情報を通知処理
 */
function sendTweetToDiscord(tweet) {
  const payload = {
    username: "TwitterTrendBot",
    avatar_url: "https://avatars.githubusercontent.com/u/50278?v=4", // twitterのpng
    content: `https://twitter.com/${tweet.user.screen_name}/status/${tweet.id_str}`, // MEMO: 上限2000文字
  };

  const options = {
    method: "post",
    muteHttpExceptions: true,
    contentType: "application/json",
    payload: JSON.stringify(payload),
  };
  UrlFetchApp.fetch(DISCORD_WEBHOOK_URL, options);
}

これも基本的にはトレンドの取得などとやってることは変わらずgetがpostになっただけです。
optionsをmethod: post, contentType: application/jsonpayloadに投稿したい内容を記載することでそのデータを投稿することができます。

embedでの送り方、使えるパラメータなどは下記などを見て調整してみると楽しいかもです
https://qiita.com/Eai/items/1165d08dce9f183eac74#embed

4. それら全部を合わせて実行

最後に上記のコードたちを実行するmain関数です。
trendを取得するとデータが返ってくるのですが、確か50件ぐらい返ってきます。その50件全てにツイート紐づけてdiscordに投稿していても邪魔なので今回は3件までを取得するようにしてます。

function main() {
  const trends = fetchTwitterTrends();
  let index = 0;
  for (const trend of trends) {
    // 4件以上のトピックは表示しない
    if (index > 3) break;
    index++;
    const trendName = trend.name ?? "";
    // MEMO: responseのurlはhttpなのでhttpsになるように変更
    const trendUrl = trend.url.replace("http", "https") ?? "";
    sendTrendToDiscord(trendName, trendUrl);
    // 遅延処理
    Utilities.sleep(500);

    const query = trend.query ?? "";
    tweets = fetchSearchTweet(query);
    for (const tweet of tweets) {
      sendTweetToDiscord(tweet);
    }
    // 遅延処理
    Utilities.sleep(3500);
  }
}

遅延処理はないとエラーになってしまってたので、適当に遅延処理を入れています。ウェーイ
なんでか知らんけど動かないんじゃ仕様がないですね。
所で大体こういうAPIにリクエストするときは非同期で通信するasync, awaitを使っているイメージがありますがそれらを使わなくても動くのはなんか不思議ですね。ワイにはよくわかりません。

はい、ということで後はGas上で関数をmain()にして実行するとwebhookが実行されdiscordに登録されるはずです。(されなければすみませんでした。原因不明です)

後はトリガータブから時間を指定することで定期的に実行してトレンドをdiscordのみんなに送ったりするのも良き哉(使い方は無限大ですね∞)

かーんせーい!🎉

わーい!ワオ

終わりに

「Twitter、API無料やめるってよ」って言われはじめ
ちょっと前にTwitter APIを触ったのを記事にできておらず、このまま無料期間終わって有料になると今のやり方も、もしかしたら変わるかもしれないなということでぎりぎりに滑り込んで記事にしてみました。

巷ではAPIを有料にしたのはbotを消すため何てのもあるみたいですね。わくわくしますね。
https://twitter.com/AsuNakima/status/1621719897054265344?s=20&t=7Wsf3J577URapIts7ekuCg

有料になったら料金は1カ月1万円位とイーロンマスクが言ってましたがどうなるんですかね?
今後のTwitterが楽しみです。

宣デーン!!!

今年は雑にでもアウトプットしていきたい年なので頑張って行きたいと思っているのです
良かったら最近Twitterつぶやいてるんでフォローしてくだっさいいいい!!
どなたでもフォローお待ちしてます。相互フォローしましょ、チェケ!!!
マイTwitter
https://twitter.com/kcash510

おわり

脚注
  1. https://developer.twitter.com/en/docs/twitter-api/v1/trends/trends-for-location/api-reference/get-trends-place
    パラメータの詳細はこちらで確認できます ↩︎

  2. https://developer.twitter.com/en/docs/twitter-api/v1/tweets/search/api-reference/get-search-tweets
    パラメータの詳細はこちらで確認できます ↩︎

Discussion