OAuth2、API v2でTwitter🐦にツイートできるようにしてみた（GAS）

# はじめに

Twitterの投稿をAPI v2とOAuth2をGoogle Apps Scriptで利用したときの対応をまとめています。
各々の記事は見つかったのですが両方使ってツイートする記事は見つからなかったので実施時の対応を共有します。

# 参考サイト

https://zenn.dev/mamushi/articles/twitter_api_v2_setup

https://zenn.dev/rikei_ocojo/articles/gas-twitter-non-public-metrics

https://officeforest.org/wp/2021/05/22/gas_twitter_v2/

# 準備

## アクセスキーの発行
Twitterのアクセスキーを発行します。[このサイト](https://zenn.dev/mamushi/articles/twitter_api_v2_setup)を参考に「2. Developer アカウント登録（Essential利用）」を実施します。

## スクリプト プロパティへアクセスキーを登録

[アクセスキー発行](#アクセスキーの発行)で発行された`CLIENT_ID` `CLIENT_SECRET`をPropaties Serviceへ登録する。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1985649/f5ae2e49-7371-5e6d-03f8-5bd0432d33a6.png)

## GAS(Google Apps Script)のTwitter側の設定

### GAS(Google Apps Script)のIDの確認

GASでプロジェクトの設定から`スクリプトID`をメモしておく。


### Twitter側へCallback URLを入力
[GAS(Google Apps Script)のIDの確認](#GAS(Google Apps Script)のIDの確認)でメモしたスクリプトIDを[ここ](https://developer.twitter.com/en/portal/products)へアクセスし、認証設定にGAS(Google Apps Script)のURLを設定する

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1985649/22b00524-a8ac-aa25-6569-d5c97bf06090.png)


# 実装

## GASでOAuthを利用するためのライブラリ追加

GASプロジェクトでライブラリの追加でスクリプトIDに「`1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF`」入力して検索
IDが「OAuth2」であることを確認して追加しておく

## Twitter 認証部分

[こちら](https://zenn.dev/rikei_ocojo/articles/gas-twitter-non-public-metrics)を参考に認証部分を実装
※ツイートするには **.setScope** に`tweet.write`が必要です。

```javascript
function getService() {
  pkceChallengeVerifier();
  const userProps = PropertiesService.getUserProperties();
  const scriptProps = PropertiesService.getScriptProperties();
  const clientId = scriptProps.getProperty('CLIENT_ID');
  const clientSecret = scriptProps.getProperty('CLIENT_SECRET');

  return OAuth2.createService('twitter')
    .setAuthorizationBaseUrl('https://twitter.com/i/oauth2/authorize')
    .setTokenUrl('https://api.twitter.com/2/oauth2/token?code_verifier=' + userProps.getProperty("code_verifier"))
    .setClientId(clientId)
    .setClientSecret(clientSecret)
    .setCallbackFunction('authCallback')
    .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(clientId + ':' + clientSecret),
      'Content-Type': 'application/x-www-form-urlencoded'
    })
}

function authCallback(request) {
  const service = getService();
  const authorized = service.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('Success!');
  } else {
    return HtmlService.createHtmlOutput('Denied.');
  }
}

function pkceChallengeVerifier() {
  var userProps = PropertiesService.getUserProperties();
  if (!userProps.getProperty("code_verifier")) {
    var verifier = "";
    var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";

    for (var i = 0; i < 128; i++) {
      verifier += possible.charAt(Math.floor(Math.random() * possible.length));
    }

    var sha256Hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, verifier)

    var challenge = Utilities.base64Encode(sha256Hash)
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '')
    userProps.setProperty("code_verifier", verifier)
    userProps.setProperty("code_challenge", challenge)
  }
}

function logRedirectUri() {
  var service = getService();
  Logger.log(service.getRedirectUri());
}
```

## 初回認証用のメソッド

初回認証用のメソッドで実行して`Open the following URL and re-run the script:`につづくURLにアクセスし認証を実施する。
`Success`が表示されればOK！

```javascript
function main() {
  const service = getService();
  if (service.hasAccess()) {
    Logger.log("Already authorized");
  } else {
    const authorizationUrl = service.getAuthorizationUrl();
    Logger.log('Open the following URL and re-run the script: %s', authorizationUrl);
  }
}
```

## ツイート用メソッド

[こちら]()を参考にツイート部分の実装をする。


```javascript
const endpoint2 = "https://api.twitter.com/2/tweets";

function postTwitter(){
  let twitterText = "Test tweet!!!"; // ツイートしたいメッセージを入力
  let service = getService();
  if (service.hasAccess()) {   
    let message = {
      text: twitterText
    }

    const headers = {
      Authorization: 'Bearer ' + service.getAccessToken()
    }
    const response = UrlFetchApp.fetch(endpoint2, {
      method: "post",
      headers,
      muteHttpExceptions: true,
      payload: JSON.stringify(message),
      contentType: "application/json"
    });

    const result = JSON.parse(response.getContentText());

    Logger.log(JSON.stringify(result, null, 2));
    
  } else {
    Logger.log("Not Authorized");
    return null;
  }
}
```
# 実行
GASで`postTwitter`を指定して実行するとツイートできます！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1985649/d567f1cb-7ef0-8058-0f0e-6594c273a11c.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1985649/579443c3-1ae9-5bc2-ef8d-5f2217f2b71d.png)

# まとめ
OAuth2、API v2を利用してTwitterにツイートを実施することができました。

# 注意事項
- Twitter APIの有償化アナウンスがされていますがこの記事の時点では1500回までの利用であれば無償で利用出来そうです。
- ですが今後どうなるかわからないです。

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">A new form of free access will be introduced as this is extremely important to our ecosystem – limited to Tweet creation of up to 1,500 Tweets per month for a single authenticated user token, including Login with Twitter.</p>&mdash; Twitter Dev (@TwitterDev) <a href="https://twitter.com/TwitterDev/status/1623467618400374784?ref_src=twsrc%5Etfw">February 8, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


- APIの申請から発行までが時間が掛かりそう。
‐ Twitter APIの審査は厳しくなっているようです。

https://zenn.dev/toshihide2000/articles/26298dd4b0dfad

https://ikesala.com/twitter_api-3/