Salesforce の Apex で Twitter API を叩いてみた

この記事は、 Salesforce の Apex から Twitter Developer Platform 上に作成した Twitter Developer App を介して自身の Twitter アカウントにツイートを投稿する方法を紹介します。

## Twitter Developer Platform とは
[Twitter Developer Platform](https://developer.twitter.com/ja) に登録(審査あり)すると、自身の Twitter アカウントに紐づく Twitter Developer App を作成できるようになります。App を作成すると様々な API にアクセスできるので、BOT を作成したり、マーケティングツールなどと連携しツイートやエンゲージメントの分析、投稿やキュレーションが可能になります。

<img width="529" alt="スクリーンショット 2020-04-06 23.05.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26378/006e789d-8e2d-c33b-6a21-0f0685193e62.png">

## Twitter Developer API のプラン
Twitter Developer の API には、Standard/Premium/Enterprise などの API が存在し、それぞれ目的やできる事、
お値段が異なります。今回は、ツイートを投稿したいだけなので無料の Standard APIs を使わせて頂きます。

<img width="986" alt="スクリーンショット 2020-04-06 21.54.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26378/c0de5d90-e1e7-8b2f-2327-7fd7c6d280fb.png">

## 認証
Standard APIs を利用するには、 HTTP リクエストの Authorization ヘッダーに OAuth 1.0a HMAC-SHA1 signature を作成し設定してあげる必要があります。そのため、 Twitter Developer Platform で App を作成した際に UI 上から以下の情報を発行しておきます。

- OAuth Consumer Key
- OAuth Consumer Secret
- OAuth Token
- OAuth Token Secret



# Apex での OAuth1.0a 認証
通常、Salesforce から Apex で外部サービスの API をコールする際には、指定ログイン情報による認証を行います。
しかし、現在の指定ログイン情報は OAuth1.0a での認証方式をサポートしていません。
そのため、今回は [Twitter の公式ドキュメント](https://developer.twitter.com/en/docs/basics/authentication/oauth-1-0a/creating-a-signature)に従って Apex で記述する事にしました。


```java
public class TwitterUtil {
    static final String ENCODING_SCHEME = 'UTF-8';
    
    public static HttpRequest signRequest(HttpRequest req, String consumerKey, String consumerSecret, String oauthToken, String oauthTokenSecret) {
        
        Boolean isMultipartPost = req.getMethod().toUpperCase() == 'POST' && req.getHeader('Content-Type').startsWith('multipart/form-data;');
        
        // Collecting parameters
        String nonce     = String.valueOf(Crypto.getRandomLong());
        String timestamp = String.valueOf(DateTime.now().getTime() / 1000);
        Map<String,String> parameters = new Map<String,String>();
        parameters.put('oauth_signature_method','HMAC-SHA1');
        parameters.put('oauth_version','1.0');
        parameters.put('oauth_consumer_key', consumerKey);
        parameters.put('oauth_timestamp', timestamp);
        parameters.put('oauth_nonce', nonce);
        parameters.put('oauth_token', oauthToken);
        if (!isMultipartPost) {
            parameters.putAll(getParameterMap(req.getBody()));
            parameters.putAll(getParameterMap(req.getEndpoint().substringAfter('?')));
        }
        
        // Creating the parameter string
        List<String> keyValues = new List<String>();
        for (String key : parameters.keySet()) {
            keyValues.add(percentEncode(key) + '=' + percentEncode(parameters.get(key)));
        }
        keyValues.sort();
        String parameterString = String.join(keyValues, '&');
        
        // Creating the signature base string
        String endpoint = req.getEndpoint();
        String baseURL = endpoint.contains('?') ? endpoint.substringBefore('?') : endpoint; 
        String signatureBaseString = req.getMethod().toUpperCase() 
            + '&' + percentEncode(baseURL) 
            + '&' + percentEncode(parameterString);
        
        // Getting a signing key
        String signingKey = percentEncode(consumerSecret) + '&' + percentEncode(oauthTokenSecret);
 
        // Calculating the OAuth signature
        Blob sig = Crypto.generateMac('HmacSHA1', Blob.valueOf(signatureBaseString), Blob.valueOf(signingKey));
        String oauthSignature = percentEncode(EncodingUtil.base64encode(sig));
        
        // Building the header string
        String header = 'OAuth' + 
            + ' oauth_consumer_key="' + consumerKey + '"'
            + ', oauth_nonce="' + nonce + '"'
            + ', oauth_signature="' + oauthSignature + '"'
            + ', oauth_signature_method="HMAC-SHA1"' 
            + ', oauth_timestamp="' + timestamp + '"'
            + ', oauth_token="' + oauthToken + '"'
            + ', oauth_version="1.0"';
       
        req.setHeader('Authorization', header);
        return req;
    }
    private static Map<String,String> getParameterMap(String parameterString) {
        Map<String,String> parameters = new Map<String,String>();
        if (String.isNotBlank(parameterString)) {
            for (String pair : parameterString.split('&')) {
                List<String> kv = pair.split('=');
                if (kv.size() == 2) parameters.put(kv[0], EncodingUtil.urlDecode(kv[1], ENCODING_SCHEME));
            }
        }
        return parameters;
    }
    private static String percentEncode(String s) {
        return EncodingUtil.urlEncode(s, ENCODING_SCHEME)
            .replace('+', '%20')
            .replace('*', '%2A')    
            .replace('%7E', '~');
    }
}
```
※ % エンコードは手抜きです。だれか正しく実装する方法を教えて下さい。

## ツイートする
ツイートを投稿するには、 [POST statuses/update](https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update) の API を利用します。
一般に「ツイート」と呼ばれるものは、API 上では「status」と呼ばれるので注意が必要ですね。
リクエストが成功すると、自身の Twitter タイムラインにツイート本文が表示されます。

```java:
// ツイート本文
String tweetText = 'Hello Twitter!';

// Twitter Developer App で発行した情報
String clientKey = 'XXXXXXXX';
String clientSecret = 'XXXXXXXX';
String oauthToken = 'XXXXXXXX';
String oauthTokenSecret = 'XXXXXXXX';

// HTTP リクエストを作成
HttpRequest req = new HttpRequest();
req.setMethod('POST');
req.setEndpoint('https://api.twitter.com/1.1/statuses/update.json'); 
req.setHeader('Content-Type','application/x-www-form-urlencoded');
req.setBody('status='+ EncodingUtil.urlEncode(tweetText, 'UTF-8'));

// HTTP リクエストから Authorization ヘッダーを作成し設定する
req = TwitterUtil.signRequest(req, clientKey, clientSecret, oauthToken, oauthTokenSecret);

// リクエストを送信
Http http = new Http();
HttpResponse res = http.send(req);

// リクエストが成功したら結果をログに出力
if (res.getStatusCode() == 200) {
  System.debug(res.getBody())
}
```