【python】TwitterのAPI（v2）を使ってリプライに添付された画像に対して返信するbotを作ってみた(1/2 システム概要とTwitterAPIを用いた検索・ツイートまで)

# はじめに
Twitter API <b>v2</b>を取得したので，初心者ながら簡単なbotを作ってみました
Twitter API v1.1の情報とTwitter API v2の情報が混在しており，仕様も異なっていて結構苦戦したので自分用のメモも兼ねて情報共有します
雑だけど許してください

### botの機能
コーギーのbotです。犬の
コーギーは食いしん坊なので，食べ物の画像を与えると喜ぶし，食べ物以外の画像を与えると怒ります
それだけです
遊びで作ったものなので，存在意義はないです
URL貼っておきますので，好きにリプライ送ってみてください
https://twitter.com/firstCorgi

# システム概要
### ユースケース
1. ユーザーがbotに画像付きのリプライを送る
2. botが画像を判別し，画像が食べ物であるかどうか判断
3. 食べ物だったらそのツイートに歓喜のリプライ，食べ物以外だったら警戒のリプライ送る

### 仕様・構成図
![Untitled Diagram.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533399/41e2e36c-f4e8-f8f4-228a-48d9589e23cc.png)

①bot自身のアカウント名で検索をかけて，リプライされたツイートを取得する
②得られたツイートがDynamoDB上に存在しなかった場合処理を続行し，そのツイートをDBに投入(すでに返信済みのツイートか判断するため)
③画像のURLをFoodai(下で説明)に投げる
④画像が食べ物と判断された場合は歓喜，食べ物でないと判断された場合は警戒のツイートをする

### 実行環境
   - python3.9.2
   - TwitterAPI v2
   - foodAI v4.1

# Foodaiについて
画像が食べ物か判断するために，なんかないんかな～と探してたら，ちょうどAPIを発見

https://foodai.org/

画像のURLを投げると、その料理の名前を返してくれるらしく，APIも公開されてる
料理じゃない画像を投げると，料理じゃないよ～って言う結果も返してくれるらしいので，使えそう！
ただ，API取得するのに申請が必要で，許可得るまでに1週間くらいかかった

# TwitterAPIを使ってみる
### TwitterAPI取得

TwitterDeveloperPlatformにアクセス

https://developer.twitter.com/

bot用のTwitterアカウントを取得し，DeveloperPotalにアクセス。
API取得理由を聞かれるので，「I want to create a twitterbot」みたいな感じで書いた

アプリケーションを作成し，各種APIキーを取得

- BearerToken
- AccessToken
- AccessTokenSecret
- ClientID
- ClientSecret

### ツイートを検索する(GET /2/tweets/search/recent)
#### bearer_token認証
APIを用いてツイートを検索するには，bearer_token認証が必要らしい
requestsのauthパラメーターに渡す関数を示す

```python
def bearer_oauth(r):
    bearertoken = "取得したbearertoken"
    r.headers["Authorization"] = f"Bearer {bearertoken}"
    r.headers["User-Agent"] = "v2RecentSearchPython"
    return r
```

#### 過去1週間のツイートを検索
TwitterAPIv2では，検索できるﾂｲｰﾄは過去1週間のツイートだけらしい
過去1週間以降のツイートを取得したいなら，Academic版APIを取得しなきゃダメとのこと

上記のbearer_token認証用関数を用いて，requestsを用いてパラメータを渡す
引数queryに検索する文字列を与える

```python
def search(query):
    params = {}
    params["query"] = query
    url = "https://api.twitter.com/2/tweets/search/recent"
    response = requests.get(url, auth=bearer_oauth, params=params)
    #ステータスコードが200以外ならエラー処理
    if response.status_code != 200:
        raise Exception(response.status_code, response.text)
    #responseからJSONを取得
    return response.json()
```

queryに検索したい文字列を与えて実行する

```python
tweetlist = search("@firstCorgi")
```

以下のようなjsonが返ってくる

```
{
{'data': [
        {'id': '1486607935622971396', 'text': '@29KrDvVZd0bmh7T @firstCorgi ワンワンワンワンワンワン！！ワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワン！！！！！！！！！！！！！！！(歓喜)'},
        {'id': '1486604041136525316', 'text': '@firstCorgi https: //t.co/qnJltOpgaV'},
        {'id': '1486362546282307594', 'text': '@firstCorgi https://t.co/y1iV764BiA'},
        ...以下略,
    ]
'meta': {'newest_id': '1486607935622971396', 'oldest_id': '1486353876588843008', 'result_count': 10, 'next_token':'b26v89c19zqg8o3fpe47n239tzry5r1tyhz724b4vkby5'}
}
```

#### 画像のURL取得
画像のURLを取得するには，リクエストパラメーターに設定値を加える必要がある
"expansions"にattachments.media_keysを加えることで，ツイートに添えられた画像が存在する場合，media_key(画像を一意に判別するid)が取得できる。そして，"media.fields"にurlを加えることで，media_keyをキーとして画像のurlが取得できる配列が返ってくる

```python
def search(query):
    params = {}
    params["query"] = query
    params["expansions"] = "attachments.media_keys"     # 追加
    params["media.fields"] = "url"                      # 追加
    url = "https://api.twitter.com/2/tweets/search/recent"
    response = requests.get(url, auth=bearer_oauth, params=params)
    #ステータスコードが200以外ならエラー処理
    if response.status_code != 200:
        raise Exception(response.status_code, response.text)
    #responseからJSONを取得
    return response.json()
```

もう一度実行

```python
tweetlist = search("@firstCorgi")
```

これを実行すると，以下のようなjsonが返ってくる

```
{'data': [
        {'id': '1486607935622971396', 'text': '@29KrDvVZd0bmh7T @firstCorgi ワンワンワンワンワンワン！！ワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワンワン！！！！！！！！！！！！！！！(歓喜)'},
        {'attachments': {'media_keys': ['3_1486604035927187457']}, 'id': '1486604041136525316', 'text': '@firstCorgi https://t.co/qnJltOpgaV'},
        {'attachments': {'media_keys': ['3_1486362541429489668']}, 'id': '1486362546282307594', 'text': '@firstCorgi https://t.co/y1iV764BiA'},
        ...以下略,
    ]    
 'includes': {'media': [
     {'media_key': '3_1486604035927187457', 'type': 'photo', 'url': 'https://pbs.twimg.com/media/FKF6fosaIAEXyhd.jpg'},
     {'media_key': '3_1486362541429489668', 'type': 'photo', 'url': 'https://pbs.twimg.com/media/FKCe2zlaAAQt8ph.jpg'},
     {'media_key': '3_1486360414200156160', 'type': 'photo', 'url': 'https://pbs.twimg.com/media/FKCc6_CaUAAM0bv.jpg'},
     ...以下略,
    ]}
 'meta': {'newest_id': '1486607935622971396', 'oldest_id': '1486353876588843008', 'result_count': 10, 'next_token':'b26v89c19zqg8o3fpe47n239tzry5r1tyhz724b4vkby5'}
}
```

このように，画像が存在するツイートの場合，dataの中にattachmentが追加され，media_keyを取得できる
取得したmedia_keyをキーとして，includesの中のmediaの配列を探索することで，画像のURLを取得することが出来る

### ツイートする(POST /2/tweets)
#### OAuth1認証
APIを用いてツイートするには，OAuth1認証が必要らしい。
TwitterDevelopersからOAuth1の書き込み許可をする。
アプリの設定画面から，User authentication settingsのとこで設定できる
各種キーを取得したら，OAuth1認証用関数をrequestsライブラリでインポートし，各種キーを入力する

```python
from requests_oauthlib import OAuth1Session
oauth = OAuth1Session(
            "取得したClientID",
            client_secret="取得したClientSecret",
            resource_owner_key="取得したAccessToken",
            resource_owner_secret="取得したAccessTokenSecret"
        )
```

#### 通常ツイート

先ほど作成したoauthを用いてパラメータを送信する

```python
def tweet(text):
    url = "https://api.twitter.com/2/tweets"
    params = {
        "text": text,
        }
    response = oauth.post(url, json=params)
    #ステータスコードが201以外ならエラー処理
    if response.status_code != 201:
        raise Exception(response.status_code, response.text)
```

引数textにツイートしたい文字列を入力すればツイートできる

```python
tweet("ツイートしたい内容")
```

#### 任意のツイートにリプライを送る

先ほどのコードに"reply"のパラメータを追加する
返信したいツイートのツイートIDを入力する
ツイートIDとは，searchで返ってきた値の["data"]の中にある["id"]のことで，各ツイートに一意に割り当てられている
各ツイートのURLからも取得できる
例えば，下記のツイートでは1485994586510336002がツイートIDにあたる

https://twitter.com/firstCorgi/status/1485994586510336002

https://twitter.com/firstCorgi/status/1485994586510336002  

```python
def reply(to_replied_tweet_id, text):
    url = "https://api.twitter.com/2/tweets"
    params = {
        "text": text,
        "reply":{
            "in_reply_to_tweet_id": str(to_replied_tweet_id)    # 追加
            }
        }
    response = oauth.post(url, json=params)
    #ステータスコードが201以外ならエラー処理
    if response.status_code != 201:
        raise Exception(response.status_code, response.text)
```

例えば先ほどのツイートにリプライを送りたい場合，以下のように実行する

```python
reply("1485994586510336002", "リプライしたい内容")
```

# 手詰まった点

- APIv1.1とAPIv2が存在して，それぞれ使用が全く違うことを知らなかった。検索して出るのが大体v1.1の情報だったので，全然思い通りに動作しなかった

- ツイート検索とツイート送信の認証方法が異なっていて，めんどくさかった。後に気付いたけど，検索もOAuth1でいけるっぽい。試してない。

- ライブラリのTweepyを用いようとしたが，TwitterAPIv1.1の情報ばかりで，v2の情報が少なかったので使わなかった。TweepyのClientクラスを用いればv2を使えるらしいけど，結局使いにくくて普通にrequestsでやった方が早かった

- 画像のURL取得するのに，リクエストパラメータを追加しなければならない所に気付くのに時間がかかった。ツイートの文章本体に直接URLが張られてると思ってた。ツイートからmedia_keyを取得して，media_keyからURLを辿る作業が面倒だった

- 大まかな流れは適当なネットの記事を読んで，ある程度慣れたら公式ドキュメントに移行するのが一番近道