🤖

🤖

:gijutsu_burogu:

Twitter Account Activity API を使ってリプライ自動返信する(Go)

概要

Twitter Account Activity API を使うことでリプライの自動返信をできます。 しかし、手順が複雑であり公式ドキュメントも分かりにくいと感じたため記事にまとめます。

リプライの自動返信には以下の二段階が必要です。

  1. 認証を行いアプリケーションを Webhook に登録する
  2. Webhook からリプライイベント通知を受け取り返信を行う

本記事では、図のアプリケーションを実装します。

f:id:kotaroooo0:20200626000132p:plain

実装

このリポジトリに実装例があります。

github.com

1. 認証を行いアプリケーションを Webhook に登録する

Twitter API の利用申請

API 利用するために、登録します。 Twitter 側が承認を行い、登録に時間がかかります。 詳しい説明は省略します。

developer.twitter.com

Twitter API の中でも、Account Activity API を利用します。 作成したアプリで Account Activity API を登録します。 Dev environment label は後から使います。 詳しい説明は省略します。

Challenge-Response Checks と Webhook の登録

Account Activity API から Webhook イベント通知を受け取るアプリケーションをデプロイする必要があります。 そのアプリケーションがあなたが作ったものか確認するために、Challenge-Response Checks を行います。

具体的には、GET /crc_check?crc_token=hoge のようなリクエストが飛んでくるので

{
  "response_token": "sha256=correct_response_token"
}

を正しく返す必要があります。

リクエスト先のエンドポイント(/crc_token)はこちらで指定できます。 response_tokenの生成には、CONSUMER_SECRETが必要で認証になります。 また、HTTPS 通信ができるサーバーでないとエラーになります。

アプリケーションの実装
func HandlerCrcCheck(c *gin.Context) {
    // Requestを受ける
    req := GetCrcCheckRequest{}
    if err := c.Bind(&req); err != nil {
        c.JSON(http.StatusBadRequest, err)
        return
    }
    // CrcTokenを生成し、Responseに詰める
    mac := hmac.New(sha256.New, []byte(os.Getenv("CONSUMER_SECRET")))
    mac.Write([]byte(req.CrcToken))
    res := GetCrcCheckResponse{
        Token: "sha256=" + base64.StdEncoding.EncodeToString(mac.Sum(nil)),
    }
    // Responseを返す
    c.JSON(http.StatusOK, res)
}

type GetCrcCheckRequest struct {
    CrcToken string `json:"crc_token" form:"crc_token" binding:"required"`
}

type GetCrcCheckResponse struct {
    Token string `json:"response_token"`
}
Webhook を登録する

Insomnia を使ってリクエストを送信し、Webhook に登録します。 https://api.twitter.com/1.1/account_activity/all/<your-dev-environment-label>/webhooks.json?url=<your-crccheck-endpoint> にリクエストを送信します。 <your-dev-environment-label>は、最初に Account Activity API を登録する際に入力したものです。 <your-crccheck-endpoint> は、Challenge-Response Checks を行うエンドポイントです。 なお、このエンドポイントは HTTPS でないと弾かれます。

f:id:kotaroooo0:20200801164056p:plain
Insomniaでリクエストを送信する

{
  "id": "1288888350132737027",
  "url": "https://98694ca5ac8e.ngrok.io/twitter_webhook",
  "valid": true,
  "created_timestamp": "2020-07-30 17:25:04 +0000"
}

のようにidがレスポンスとして帰ってくれば成功です。

次に、https://api.twitter.com/1.1/account_activity/all/<your-dev-environment-label>/subscriptions.jsonにリクエストを送ります。 先ほどのように OAuth のヘッダーをセットし、それに加えてリクエストボディとして先ほどのidを含む json を付与します。

f:id:kotaroooo0:20200731022805p:plain
subscription登録する

204 が帰ってくれば成功です。 これでWebhookの登録は完了です。

Tips1

僕は最初、Insomnia での登録がなぜかうまくいきませんでした。 公式で Webhook 管理用のアプリケーションが提供されています。 上で行ったことはこちらでも可能です。 Webhook の登録解除もできます。

README にしたがってセットアップすることで、ローカルサーバーを立ち上げることができます。

github.com

f:id:kotaroooo0:20200625233632p:plain

Tips2

AWS 等のサーバーにデプロイしながら CRC チェックの部分をデバッグしていくのはとてもめんどくさいので、開発中は ngrok を使うことを推奨します。

https://ngrok.com/

これを利用することで、ローカルホストのサーバーを外部公開することができるので、効率よく Webhook の登録の確認を行うことができます。

$ ngrok http 3000

$ curl http://98694ca5ac8e.ngrok.io/twitter_webhook\?crc_token\=hoge
{"response_token":"sha256=6kMzK/Hv5JCUiFwMzIJy8T+2N/uxhuSdn+GmTYb+Enc="}%

使い方は以下を参照してください。

qiita.com

2. Webhook からリクエストを受け取り、返信を行う

リプライイベント通知を受け取り、そのリクエストから情報を取得してリプライに対してリプライを返信します。 こちらは、Webhook からのリクエストを構造体にマッピングさえしてしまえば複雑ではないです。

func HandlerTwitterActivity(c *gin.Context) {
    // Requestを受ける
    req := PostTwitterActivityRequest{}
    if err := c.Bind(&req); err != nil {
        c.JSON(http.StatusBadRequest, err)
        return
    }

    // 略

    // リプライを返す
    params := url.Values{}
    params.Set("in_reply_to_status_id", req.TweetCreateEvents[0].TweetIDStr)
    twitterApiClient := NewTwitterApiClient()
    status, err := twitterApiClient.PostTweet(fmt.Sprintf("@%s %s", req.TweetCreateEvents[0].User.ScreenName, content), params)
    if err != nil {
        c.JSON(http.StatusBadRequest, err)
        return
    }
    c.JSON(http.StatusOK, status)
}

type PostTwitterActivityRequest struct {
    UserID            string             `json:"for_user_id" form:"for_user_id" binding:"required"`
    TweetCreateEvents []TweetCreateEvent `json:"tweet_create_events" form:"tweet_create_events" binding:"required"`
}

type TweetCreateEvent struct {
    TweetIDStr string `json:"id_str" form:"id_str" binding:"required"`
    Text       string `json:"text" form:"text" binding:"required"`
    User       struct {
        IDStr      string `json:"id_str" form:"id_str" binding:"required"`
        ScreenName string `json:"screen_name" form:"screen_name" binding:"required"`
    } `json:"user" form:"user" binding:"required"`
}

参照

developer.twitter.com

medium.com