Help us understand the problem. What is going on with this article?

どうしても英語で投稿したい~TwitterのAccount Activity API + API Gateway + Lambdaでサーバーレス自動翻訳英語アカウントを作る~

More than 1 year has passed since last update.

この記事はLIFULL Advent Calendar 4日目の記事です。

今年も残すところ、あとひと月となりました。
月日の流れが非常に早く感じますが、最後まで気を引き締めていきたいと思います!

今回の記事の内容ですが、
タイトルを見た感じだと英語で投稿するのに重きを置いている様に見えますが、どちらかと言うと、Twitter社から公表された新APIであるAccount Activity APIを使ってみたの方が正しいと思います。

はじめに

なぜ今回英語で発信したいと思ったかと言うと、
私は学生時代に留学と称してアメリカの大学にお世話になっていたのですが、それはもうアメリカかぶれをしていた時期がございました。
SNSでの発言は英語も一緒に投稿、もしくは英語アカウントを作って投稿したりと、それはもう見事にかぶれておりました。

しかし、長くは続きませんでした。
なぜかというと、以下の課題があったからだと思います。
- 日本語で記載した文章をいちいち英語に直して書くのが大変。
- そこまでして英語で書いた文章をアメリカの友達が読むとは限らない(読むときは日本語でも読んでくれる)。
- そもそも英語力が乏しい。
etc...

と課題だらけで続けることはできませんでした。
しかしながら様々なエンジニアと触れ合うためには自分の意見を他国の人にも少しでも共有できる様に英語を話すべきだ!

そう考えたことによって、
もう普段の投稿を自動で翻訳して投稿し続ける様にすればよくない?
と本末転倒なことを思いつき実践することにしました。

ちなみに今回は自分がよく使うSNSで
実際に利用したいbotを作る予定ですので、
Twitterを題材にします。

Account Activity API

昨年からUserStreamAPIを廃止するぞ!と発表され、2018年の8月16日を持って終了することとなりました。

これによってあらゆるサードパーティクライアントが流れるタイムラインやダイレクトメッセージ、いいねやフォローやメンションを通知する機能を失う、もしくは代用しなければなくなりました。

なぜ廃止したのかの解説はこちらの記事を見ていただけるとわかりやすいです。

上記APIの廃止に伴い、新しいAPIが公表されました。それがこのAccount Activity APIです。
Account Activity APIを全ての開発者の皆さんに公開
このAPIでは昔の様にリアルタイムにタイムラインを取得することはできませんが、代わりにWebhookを利用したダイレクトメッセージ、リプライ、いいねやフォローのイベントを検知することができます。

webhookを利用できるようになるのは夢が広がりますね!
わーい:smiley:

やったこと

アーキテクチャ

今回の実装では、バックエンドにはAWS Lambdaを利用してサーバーレスで実装しました。
外部のサービスとしては、翻訳ができるかつ無料なものとしてMicrosoft AzureのTranslator Text APIを利用しています。
毎月200万文字までは無料のようでした。
また、先ほど紹介したTwitterのAccount Activity APIを利用していきます。
こちらもwebhookアドレス1つと購読するアカウント15個までは無料でした。

それらを利用して実際のアーキテクチャは以下の様な構成になります。
Screen Shot 2018-12-02 at 2.58.14.png
API GatewayでWebhookを受けてLambdaがAzure APIにアクセスし、翻訳したツイートをTwitterに投稿します。

まずは実現したもの

機能は単純でツイートをしたら翻訳されたものがほかのアカウントで呟かれるだけです。

以下のようにツイートをおこないます。
Screen Shot 2018-12-02 at 3.25.58.png

すると以下のように翻訳したものを英語アカウントが呟きます。
Screen Shot 2018-12-02 at 3.26.09.png

やった!!英語が喋れるようになったぞ!!(※なってません)

実装の前に準備するもの

  1. AWSのアカウントを取得
    • AWS LambdaとAPIGatewayを作るのに必要です。
  2. Twitterのデベロッパアカウントに登録
    • アカウント発行の際になんのために発行するのか英語300words以上で説明しなければならなくなっていました(英語を話さなくていいようにするために英語を書くことになるとは…)。
    • https://developer.twitter.com/en/apply で登録できます。
  3. Twitterアプリの設定やAccountActivityApiのセットアップ

    • twitterのアプリを作ります。今回はJapToEngTweetForRikushiruみたいな名前でアプリを作りました。
    • アプリケーションの詳細画面に行き、ConsumerKey,ConsumerSecret,AccessToken,AccessTokenSecretの発行とpermissionをRead,Write and Access direct messages権限で作成しました。
    • subscribeにはtwitterの通知イベントの全てが含まれるので、direct messageへのアクセス権限も必要となるみたいです。
    • ダッシュボードで一番下のAccountActivityApiの設定をしておきます。環境名を入力するのですが、こちらはなんでもいいようです。ただ後々必要となるので覚えておく必要があります。 Screen Shot 2018-12-02 at 17.50.03.png
  4. Microsoft Translator Text APIを有効化して、認証キーを取得する

    • https://azure.microsoft.com/ja-jp/ でAzureアカウントにサインアップ
    • Azure アカウントに Microsoft Translator のサブスクリプションを追加する
    • [すべてのリソース] メニューからサブスクリプションをクリックし、認証キーを取得する

環境構築

  • AWS(Lambda, ApiGateway)
  • node.js

今回使用した言語はnode.jsを利用しました。
(余談ですが、LambdaにRubyが対応しましたね!)

Lambdaでfunctionをつくる

こちらの画面から関数を一つ作ります。
下の入力フォームに適切に入力後、関数を作成します。
Screen Shot 2018-12-02 at 21.25.41.png
すると、以下のようにコードを入力することができる画面になります。
Screen Shot 2018-12-02 at 21.28.43.png

このブラウザ上で実装が可能になるのですが、
ここで実装をすると、npm installなどが行えないので、外部ライブラリをrequireすることができなません。
したがってローカルで実装していきます。

どこでもいいので、ディレクトリ内でindex.jsを作ります。
そのあとで、今回必要となる外部ライブラリをnpm installします。

$ mkdir sample 
$ cd sample
$ touch index.js
$ npm install twitter require-promise uuid

その後やりたいことをコードにしていきます。
その前に、Account Activity APIではどのようなことをWebhookで取得できるかを知る必要があります。
それはリファレンスに記載されていますので、ご参考にしてください。
これを見ていると今回やりたいことは
サブスクライブしているユーザーがツイートを作成した時に、その情報を受信したいので、
tweet_create_eventsを受け取った際に発火するようなコードにしていく必要がありそうです。

まずは、サブスクライブしているユーザーがツイートした場合に
どんなデータを受け取ることができるかを確認する必要があります。

受け取れるデータは以下のようなデータになります。

{
    // ツイートしたユーザーのid
    "for_user_id": "2423836924",
    // ツイートを作成したことを示すイベント 
    "tweet_create_events": [
        {
            "created_at": "Sat Dec 01 19:10:21 +0000 2018",
            "id": 1068945413384790000,
            "id_str": "1068945413384790016",
            "text": "うどん食べたくなってきた",
            "source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",
            "truncated": false,
            "in_reply_to_status_id": null,
            "in_reply_to_status_id_str": null,
            "in_reply_to_user_id": null,
            "in_reply_to_user_id_str": null,
            "in_reply_to_screen_name": null,
            "user": {
                ユーザー情報がつらつらと
            },
            "geo": null,
            "coordinates": null,
            "place": null,
            "contributors": null,
            "is_quote_status": false,
            "quote_count": 0,
            "reply_count": 0,
            "retweet_count": 0,
            "favorite_count": 0,
            "entities": {
                "hashtags": [],
                "urls": [],
                "user_mentions": [],
                "symbols": []
            },
            "favorited": false,
            "retweeted": false,
            "filter_level": "low",
            "lang": "ja",
            "timestamp_ms": "1543691421253"
        }
    ]
}

このデータを見る限り、tweet_create_eventsの中にツイート内容を示すtextがあるので、
これを取得して翻訳し、ツイートを投稿するコードを以下のように記載しました。

index.js
const twitter = require('twitter');
const request = require('request-promise');
const uuidv4 = require('uuid/v4');

//Twitter
//環境変数にしたほうがいいのですが、今回はわかりやすさのために、コード内に記載
const twitter_client = new twitter({
    consumer_key: YOUR CONSUMER KEY,
    consumer_secret: YOUR CONSUMER SECRET,
    access_token_key: YOUR ACCESS TOKEN, //翻訳後にツイートするユーザーのAccess Token
    access_token_secret: YOUR ACCESS TOKEN SECRET //翻訳後にツイートするユーザーのAccess Token Secret
    });

//Translator Text API
//環境変数にしたほうがいいのですが、今回はわかりやすさのために、コード内に記載
const translator = {
    key: YOUR KEY
}
//Translator Text API base url
const base_url = 'https://api.cognitive.microsofttranslator.com'

    exports.handler = (event, context, callback) => {
    var body = JSON.parse(event['body'])
    if(body.tweet_create_events){
        let tweet = body.tweet_create_events[0];
        //retweetとreplyは翻訳しない
        if(tweet.in_reply_to_user_id || tweet.in_reply_to_status_id || tweet.retweeted_status){
        return;
        }
        requestTranslateAndTweet(tweet.text, twitter_client);
        const response = {
            statusCode: 200,
            body: 'tweet created',
        };
        return response;
      }
    };


//Translator APIに翻訳リクエストを送信後、ツイートを行う。
function requestTranslateAndTweet(text, client){
    let options = {
      method: 'POST',
      baseUrl: base_url,
      url: 'translate',
      qs: {
          'api-version': '3.0', //api-versionをパラメータに付与する必要がある。
          'to': 'en' //英語に翻訳するのでen
      }, 
      headers: {
          'Ocp-Apim-Subscription-Key': translator.key,
          'Content-type': 'application/json',
          'X-ClientTraceId': uuidv4().toString()
      },
      body: [{
        'text': text
          }],
      json: true,
      simple: false,
      };
    // Translate Text APIにリクエストを行う。
    request(options)
      .then(function (body) {
          //リクエストが成功した場合、ツイートを行う。
          var translate_text = body[0].translations[0].text
          client.post('statuses/update', {status: translate_text},  function(error, tweet, response){
             if(error) return 'error';
             return 'OK';
          });
      })
      .catch(function (err) {
          console.log(err);
    });
}

これでtwitterからwebhookにPOSTされたデータを受け取り、翻訳してツイートすることができる関数を作成しました。
ここまでやったら、
ディレクトリの中身を以下のコマンドで圧縮し、Lambdaにzipファイルをアップロードします。

$ zip -r [圧縮した後のファイル名] *

圧縮したファイルを先ほど作成したLambda関数にて以下の画面からzipファイルをアップロードします。
Screen Shot 2018-12-02 at 22.19.00.png

アップロードして書いたコードが表示されたら関数の作成は完了です。

APIGatewayでPOSTメソッドを作り、Lambdaファンクションと紐づける

次にAPI GatewayでAPIを新規に作成します。
まずはAPI Gatewayの画面から新規APIを作成します。
Screen Shot 2018-12-02 at 21.24.12.png

作成したすると以下の様な画面になると思います。
Screen Shot 2018-12-02 at 22.23.39.png
上記の画面が開かれたら、[アクション]>[メソッドの作成]>[プルダウンからPOSTメソッドを選択]しPOSTメソッドを作成します
Screen Shot 2018-12-02 at 22.26.22.png
あとは、統合タイプをLambda関数とし、Lambda関数の欄に先ほど作成したLambda関数の名前を記入し、保存を選択します。
以下の様な画面になったら無事に作られたことを確認できます。
Screen Shot 2018-12-02 at 22.29.14.png

CRC用にAPIにGETメソッドをつくり、CRC用のLambdaファンクションを作成し紐づける

CRC(Challenge-Response Checks)とは

Webhookをしようするためには以下の要件を満たす必要があるそうです。

  • A base64 encoded HMAC SHA-256 hash created from the crc_token and your app Consumer Secret
  • Valid response_token and JSON format.
  • Latency less than 3 seconds.
  • 200 HTTP response code.

Twitterからwebhookで指定したurlに対しGETメソッドでリクエストが送信され、CRCのために以下のレスポンスを返答するようにします。

CRCで期待するレスポンス
{
    'response_token': 'sha256=' + base64.b64encode(sha256_hash_digest)
}

詳しいことはここで確認できます。
したがって、まずはCRC用のファンクションとAPIにGETメソッドを作成します。

CRC用のコード

node.jsで以下のコードをLambdaに作成しました。
Twitterから受け取ったqueryParameterからcrc_tokenを受け取ってconsumer secretでsha256でハッシュ化してresponseに返すという処理になります。

CRCtest
'use strict';
exports.handler = async(event, context, callback) => {
    // パラメータのcrc_token取得
    var response_token = null;
    if (event.queryStringParameters){
        var crc_token = event.queryStringParameters.crc_token;

        // ハッシュ化
        var crypto = require('crypto');
        var hmac = crypto.createHmac('sha256', TwitterのConsumerSecretKey).update(crc_token).digest('base64');
        var calculatedSignature = 'sha256=' + hmac;
        response_token = calculatedSignature;
    }
    const response = {
        statusCode: 200,
        body: JSON.stringify({'response_token': response_token}),
    };
    return response;
};

APIのGETメソッドを作る

先ほど作ったAPIに今度はGETメソッドを作成していきます。
先ほどの要領でGETメソッドを作成し、先ほど作ったCRCtestのLambda関数を紐付けます。
そして以下のように作成されたら完了です。
Screen Shot 2018-12-02 at 22.32.53.png

APIデプロイ

ここまでやったら、APIをデプロイします。
[アクション]>[APIのデプロイ]を選択し、
以下の画面からデプロイするステージを選択してデプロイします。
Screen Shot 2018-12-02 at 22.35.01.png

すると、以下の様な画面からURLが取得できますので控えておきます。
Screen Shot 2018-12-02 at 22.37.13.png

これが今回のWebhook URLになります。

Twitterにwebhookのサーバを登録

webhookのサーバを登録するには、TwitterのAPIで登録する必要があります。
登録する際には、先ほど作成したCRCが実行されます。
ちなみに、登録は一度だけですので、Postmanなどでリクエストを送るのをお勧めします。

webhookを登録する手順ですが、以下のurlに対しPOSTリクエストを送ります。

method: POST
https://api.twitter.com/1.1/account_activity/all/[env_name]/webhooks.json?url=[webhookのURL]
header:
    Authorization: OAuth [OAuth parameters]

この時webhookのURLはパーセントエンコードをする必要がありますので、事前に行いましょう。
また、env_nameにはAccountActivityApiの設定にて設定したlabelの値が入ります。

またリクエストを送る際にOAuthの認証パラメータをheaderに含める必要があります。したがって発行したトークンの出番です。

以下の画面の入力項目を埋めて、POSTリクエストを送りましょう。
Screen Shot 2018-12-02 at 22.49.49.png
レスポンスコードが200で返ってくれば成功です。
ちなみに以下の様に返却されます。

{
 "id": "webhookで使用するID",
 "url": "https://{自分のサーバのドメイン名}/{crcチェックするエンドポイント}",
 "valid": true,
 "created_timestamp": "2018-11-30 00:13:41 +0000"
}

これでwebohookのurlを登録することができました。
わーい。
(僕はこれがうまくできなくて本当に苦労しました…)

Twitterのwebhookで購読するアカウントを登録

さいごに購読するアカウントを登録して終了です。
投稿するアカウントと購読するアカウントが同じ場合には同じアクセストークンでいいのですが、今回行うのは購読するアカウントと投稿するアカウントが別なので、購読するアカウントのアクセストークンを取得しておきます。

こちらの記事でアクセストークンを取得するための簡単なコードが紹介されていましたので参考になるかと思います。

購読するアカウントを追加するためには、以下のURLにPOSTでリクエストを送ることで、登録することができます(無料版は15アカウントまでしか登録できません)。

method: POST
https://api.twitter.com/1.1/account_activity/all/[env_name]/subscriptions.json
header:
    Authorization: OAuth [OAuth parameters]

こちらも同様に[env_name]にはAccountActivityApiの設定にて設定したlabelを記載しましょう。

今回もPostmanを使用すると簡単に登録可能でした。
Screen Shot 2018-12-02 at 23.14.04.png

こちらがうまくいけばwebhookの設定も完了です。
お疲れ様でした!

これによって最初にご紹介した様な購読対象が投稿したら自動で英語を呟くbotが完成しました。
わーい。

さいごに

今回は最近公開されたばかりのTwitterのAPIであるAccount Activity APIを利用してみました。
Webhookが利用できる様になって僕個人としてはかなり面白みが増したような気がします(評判を見るとタイムラインのストリーミングができないので、不評な面のほうが多そうですね)。
ただ、このAPIを利用するまでにかなり多くの問題が発生して導入するのが難しかったり、扱えるアカウントの数が無料枠だと少なかったりと実用するのは難しそうでした。
まだまだこちらのAPIの記事は多くない上に、リファレンスも完全に英語だったりするので、結構苦戦しましたが、いい経験になりました。
また、初めてLambdaでサーバレスを実践できたのもかなり大きな収穫でした。

そしてなにより、英語アカウントを自動化することができました!(やったね)

拙い文章な上、長ったらしい文章を記載してしまいました。
知識不足だったりする場面が多かったとは思いますが、温かい目で読んでいただければ幸いです。
また、ご指摘等ございましたら随時お伝えいただけるとありがたいです。

最後までお読みいただきましてありがとうございました。

riku-shiru
普段はキーボードをぽちぽちしています。バックエンドエンジニア👨‍💻
https://me.riku-shiru.com/
lifull
日本最大級の不動産・住宅情報サイト「LIFULL HOME'S」を始め、人々の生活に寄り添う様々な情報サービス事業を展開しています。
https://lifull.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away