【画像投稿も可】Google Apps ScriptでTwitter API v2を使う方法

当サイトではアフィリエイト広告を利用して商品を紹介しています。

概要

この記事では、Google Apps Script (GAS) を使用して Twitter API v2 を操作する方法を説明します。具体的には、ツイートの投稿、削除、ユーザ情報の取得、画像付きツイートの投稿などを行うクラスTwitterV2の使用方法について解説します。

初期設定

プロパティストア

まず、TwitterのAPIキーとクライアントID、クライアントシークレットを取得し、GASのプロパティストアに設定します。以下のコードをスクリプトエディタで実行してください。

PropertiesService.getScriptProperties().setProperty('TW_ACCOUNT_NAME', 'あなたのTwitterアカウント名');
PropertiesService.getScriptProperties().setProperty('TW_API_KEY', 'あなたのAPIキー');
PropertiesService.getScriptProperties().setProperty('TW_API_SECRET', 'あなたのAPIシークレット');
PropertiesService.getScriptProperties().setProperty('TW_CLIENT_ID', 'あなたのクライアントID');
PropertiesService.getScriptProperties().setProperty('TW_CLIENT_SECRET', 'あなたのクライアントシークレット');

ライブラリのインストール

次に、OAuth1とOAuth2の認証を行うためのライブラリをインストールします。スクリプトエディタの「リソース」メニューから「ライブラリ」を選択し、以下のライブラリIDを追加してください。

  • OAuth1: 1CXDCY5sqT9ph64fFwSzVtXnbjpSfWdRymafDrtIZ7Z_hwysTY7IIhi7s
  • OAuth2: 1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF

認証

Twitter APIを使用するためには、Twitterの認証が必要です。以下のコードを実行して認証を行います。

function authorizeTwitterOAuth1() {
  const twitter = new TwitterV2();
  twitter.authorizeOAuth1();
}

function authorizeTwitterOAuth2() {
  const twitter = new TwitterV2();
  twitter.authorizeOAuth2();
}

これらの関数を実行すると、ログに表示されるURLを開き、Twitterで認証を行ってください。

Twitter側の設定

Twitterの開発者ページから、コールバックURLを設定する必要があります。以下の形式でコールバックURLを設定してください。

https://script.google.com/macros/d/***スクリプトID***/usercallback

サンプルコード

以下に、TwitterV2クラスの各メソッドの使用例を示します。

// ツイートを投稿する
function postTweet() {
  const twitter = new TwitterV2();
  const response = twitter.post('Hello, Twitter!');
  console.log(response);
}

// ツイートを削除する
function deleteTweet() {
  const twitter = new TwitterV2();
  const tweetId = '1234567890';  // 削除するツイートのID
  const response = twitter.destroy(tweetId);
  console.log(response);
}

// ユーザ情報を取得する
function getUserInfo() {
  const twitter = new TwitterV2();
  const response = twitter.getUser();
  console.log(response);
}

// 画像付きツイートを投稿する
function postImageTweet() {
  const twitter = new TwitterV2();
  const tweetText = 'Hello, Twitter with image!';
  const imageUrl = 'https://example.com/image.jpg';  // 画像のURL
  const response = twitter.postImg(tweetText, [imageUrl]);
  console.log(response);
}

コード一覧

最後に、全てのコードをまとめておきます。このコードをスクリプトエディタに貼り付け、適切に設定を行えば、Twitter API v2をGASから操作することができます。

/**
 * Twitterを扱うクラスです。
 * APIv2を使用しています。
 * 
 * ※事前にtwitterのAPIkeyの取得とパーミッションの設定が必要です。
 * 2.0のAPI_KEYは「STANDALONE APPS」から発行したKEYは使えません。
 * DEVELOPMENT APPからキーを発行してください。
 * 
 * ディベロップURL 
 *  https://developer.twitter.com/en/docs/twitter-api/data-dictionary/introduction
 * 依存するライブラリ
 * --- OAuth1 --- 
 * 1CXDCY5sqT9ph64fFwSzVtXnbjpSfWdRymafDrtIZ7Z_hwysTY7IIhi7s
 * --- OAuth2 --- 
 * 1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
 * 
 */
class TwitterV2 {
  /**
   * TwitterV2クラスのコンストラクタ
   * @param {string} accountName - Twitterアカウント名
   * @param {string} clientId - TwitterクライアントID
   * @param {string} clientSecret - Twitterクライアントシークレット
   */
  constructor(
    accountName = PropertiesService.getScriptProperties().getProperty('TW_ACCOUNT_NAME'),
    apiKey = PropertiesService.getScriptProperties().getProperty('TW_API_KEY'),
    apiSecret = PropertiesService.getScriptProperties().getProperty('TW_API_SECRET'),
    clientId = PropertiesService.getScriptProperties().getProperty('TW_CLIENT_ID'),
    clientSecret = PropertiesService.getScriptProperties().getProperty('TW_CLIENT_SECRET')
  ) {

    this.accountName = accountName;
    this.apiKey = apiKey
    this.apiSecret = apiSecret
    this.clientId = clientId;
    this.clientSecret = clientSecret;
  }

  /**
   * ツイートを投稿する
   * 
   * @param {string} tweetText - ツイートのテキスト
   * @return {object} response - レスポンスオブジェクト
   */
  post(tweetText) {
    const service = this.getServiceOAuth2();
    if (service.hasAccess()) {
      const url = 'https://api.twitter.com/2/tweets';
      const payload = {
        'text': tweetText
      };
      const options = {
        method: 'post',
        headers: {
          Authorization: 'Bearer ' + service.getAccessToken(),
          'Content-Type': 'application/json'
        },
        payload: JSON.stringify(payload),
        muteHttpExceptions: true
      };
      const response = UrlFetchApp.fetch(url, options);
      return JSON.parse(response.getContentText());
    } else {
      throw new Error('Service not authorized.');
    }
  }

  /**
   * 画像をつけてツイートする
   * 
   * @param {string} postMsg - 文字列
   * @return {string.<URL>||blob} files 配列で指定 - 
   */
  postImg(postMsg, files) {
    const service = this.getServiceOAuth2();
    const url = 'https://api.twitter.com/2/tweets';

    if (files === undefined) return this.post(postMsg)
    if (files[0] === "") return this.post(postMsg)
    const mediaIds = files.map(file => {
      const fileBlob = (typeof (file) === "string") ? this._convertImg(file) : file
      const mediaId = this._uploadImgBlob(fileBlob)['media_id_string']
      return mediaId
    })
    const payload = {
      'text': postMsg,
      'media': {
        'media_ids': mediaIds
      }
    }
    const options = {
      method: 'post',
      headers: {
        Authorization: 'Bearer ' + service.getAccessToken(),
        'Content-Type': 'application/json'
      },
      payload: JSON.stringify(payload),
      muteHttpExceptions: true
    };

    const response = UrlFetchApp.fetch(url, options);
    const result = JSON.parse(response.getContentText());

    return result;
  }

  /**
   * URLから画像を取得する。
   * 
   * @param {string} imgUrl - 画像のURL
   * @return {blob} blob - blob
   */
  _convertImg(imgUrl) {
    const response = UrlFetchApp.fetch(imgUrl);
    const fileBlob = response.getBlob();
    return fileBlob
  }

  /**
   * blobをtwitterにアプロードする。
   * 
   * @param {Blob} blob - アップロードする画像のBlob
   * @return {object} media_id_string - アップロードした画像のID
   */
  _uploadImgBlob(blob) {
    const service = this.getServiceOAuth1();
    const url = 'https://upload.twitter.com/1.1/media/upload.json';
    const respBase64 = Utilities.base64Encode(blob.getBytes());//Blobを経由してBase64に変換

    const payload = {
      'media_data': respBase64
    }

    const options = {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      payload: payload,
      muteHttpExceptions: true
    };

    const response = service.fetch(url, options);
    const result = JSON.parse(response.getContentText());
    return result;
  }

  /**
   * ツイートを削除する
   * 
   * @param {string} tweetId - id
   * @return {object} data - data
   */
  destroy(tweetId) {

    const service = this.getServiceOAuth2();
    const url = `https://api.twitter.com/2/tweets/${tweetId}`;
    const options = {
      method: 'DELETE',
      headers: {
        Authorization: `Bearer ${service.getAccessToken()}`,
        'Content-Type': 'application/json'
      },
      muteHttpExceptions: true
    };

    const response = UrlFetchApp.fetch(url, options);
    const result = JSON.parse(response.getContentText());

    return result;
  }

  /**
   * GET /2/users/meを実行して自分のユーザ情報を取得する
   * 
   * @return {object} data - ユーザ情報
   * Note : https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me
   */
  getUser() {
    const service = this.getServiceOAuth2();
    const url = 'https://api.twitter.com/2/users/me?user.fields=created_at,description,entities,id,location,name,pinned_tweet_id,profile_image_url,protected,public_metrics,url,username,verified,verified_type,withheld';

    const options = {
      method: 'get',
      headers: {
        Authorization: `Bearer ${service.getAccessToken()}`,
        'Content-Type': 'application/json'
      },
      muteHttpExceptions: true
    };
    const response = UrlFetchApp.fetch(url, options);
    const result = JSON.parse(response.getContentText());
    return result;
  }


  /**
   * OAuth1サービスを取得する
   * @returns {Service} OAuth2サービス
   */
  getServiceOAuth1() {
    return OAuth1.createService(this.accountName)
      .setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
      .setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
      .setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
      .setConsumerKey(this.apiKey)
      .setConsumerSecret(this.apiSecret)
      .setCallbackFunction('TwitterV2.authCallbackOAuth1')
      .setPropertyStore(PropertiesService.getUserProperties())
  }
  /**
   * OAuth1のコールバック関数
   * @param {Object} request - リクエストオブジェクト
   * @returns {HtmlOutput} 認証結果のHTML出力
   */
  static authCallbackOAuth1(request) {
    const twitter = new TwitterV2()
    const service = twitter.getServiceOAuth1();
    const isAuthorized = service.handleCallback(request);
    const mimeType = ContentService.MimeType.TEXT;
    if (isAuthorized) {
      return ContentService.createTextOutput('認証完了です! authCallbackOAuth1').setMimeType(mimeType);
    } else {
      return ContentService.createTextOutput('Denied').setMimeType(mimeType);
    }
  }

  /**
   * Twitterを認証します。
   * 事前にtwitter ディベロッパーページからコールバックURLを指定する必要があります。
   * https://developer.twitter.com/en/portal/projects/1352079192913920001/apps/20768938/settings
   * コールバックURLの形式:
   * https://script.google.com/macros/d/***スクリプトID***/usercallback
   */
  authorizeOAuth1() {
    const service = this.getServiceOAuth1();
    if (service.hasAccess()) {
      console.log('Already authorized');
    } else {
      const authorizationUrl = service.authorize();
      console.log('Open the following URL and re-run the script: %s', authorizationUrl);
    }
  }

  /**
   * Twitterの認証解除するメソッド
   */
  resetOAuth1() {
    const service = this.getServiceOAuth1();
    service.reset();
  }


  /**
   * OAuth2サービスを取得する
   * @returns {Service} OAuth2サービス
   */
  getServiceOAuth2() {
    this.pkceChallengeVerifier();
    const userProps = PropertiesService.getUserProperties();
    return OAuth2.createService(this.accountName)
      .setAuthorizationBaseUrl('https://twitter.com/i/oauth2/authorize')
      .setTokenUrl('https://api.twitter.com/2/oauth2/token?code_verifier=' + userProps.getProperty("code_verifier"))
      .setClientId(this.clientId)
      .setClientSecret(this.clientSecret)
      .setCallbackFunction('TwitterV2.authCallbackOAuth2')
      .setPropertyStore(userProps)
      .setScope('users.read tweet.read tweet.write offline.access')
      .setParam('response_type', 'code')
      .setParam('code_challenge_method', 'S256')
      .setParam('code_challenge', userProps.getProperty("code_challenge"))
      .setTokenHeaders({
        'Authorization': 'Basic ' + Utilities.base64Encode(this.clientId + ':' + this.clientSecret),
        'Content-Type': 'application/x-www-form-urlencoded'
      });
  }
  /**
   * OAuth2のコールバック関数
   * @param {Object} request - リクエストオブジェクト
   * @returns {HtmlOutput} 認証結果のHTML出力
   */
  static authCallbackOAuth2(request) {
    let twitter = new TwitterV2();
    const service = twitter.getServiceOAuth2();
    const authorized = service.handleCallback(request);
    if (authorized) {
      return HtmlService.createHtmlOutput('認証完了です!(authCallbackOAuth2)');
    } else {
      return HtmlService.createHtmlOutput('Denied.');
    }
  }
  /**
   * PKCE (Proof Key for Code Exchange) のチャレンジとバリデータを生成します。
   * すでに生成されている場合は何もしません。
   */
  pkceChallengeVerifier() {
    const userProps = PropertiesService.getUserProperties();
    if (!userProps.getProperty("code_verifier")) {
      let verifier = "";
      const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
      for (let i = 0; i < 128; i++) {
        verifier += possible.charAt(Math.floor(Math.random() * possible.length));
      }
      const sha256Hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, verifier);
      const challenge = Utilities.base64Encode(sha256Hash)
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');
      userProps.setProperty("code_verifier", verifier);
      userProps.setProperty("code_challenge", challenge);
    }
  }

  /**
   * Twitterを認証します。
   * 事前にtwitter ディベロッパーページからコールバックURLを指定する必要があります。
   * https://developer.twitter.com/en/portal/projects/1352079192913920001/apps/20768938/settings
   * コールバックURLの形式:
   * https://script.google.com/macros/d/***スクリプトID***/usercallback
   */
  authorizeOAuth2() {
    const service = this.getServiceOAuth2();
    if (service.hasAccess()) {
      console.log('Already authorized');
    } else {
      const authorizationUrl = service.getAuthorizationUrl();
      console.log('Open the following URL and re-run the script: %s', authorizationUrl);
    }
  }

  /**
   * Twitterの認証解除するメソッド
   */
  resetOAuth2() {
    const service = this.getServiceOAuth2();
    service.reset();
  }

}

補足: OAuth1を使用する理由

Twitter API v2では、基本的にOAuth2を使用して認証を行います。しかし、画像付きツイートの投稿については、現在のところTwitter API v2ではサポートされていません。そのため、画像付きツイートを投稿するためには、まだOAuth1を使用する必要があります。

以上で、GASを使用してTwitter API v2を操作する準備が整いました。ツイートの自動投稿や、特定のユーザのツイートの取得など、自由にカスタマイズしてご利用ください。