DjangoでTwitter APIを利用してツイートする

# はじめに
この記事は「[Django Advent Calendar 2021](https://qiita.com/advent-calendar/2021/django)」の16日目の記事です。
3年連続3回目の参加です。毎年ありがとうございます。

# DRF + NuxtでWebサービスを作成しました。

2020年に作成したWebサービスを今年に入ってからDjango REST FrameworkとNuxt.jsで作り直しました。

https://npb-battingorder-maker.netlify.app/

https://note.com/shimayu22/n/nd65fbd7ebe03

前回作成した時は画像をツイートするために、一度作成した画像をローカルに保存してから自分で貼り付けてツイートしてもらう手順となっていましたが、今時そんな面倒なサービス使われないと思い、今回はTwitter APIを利用してボタン一発で画像付きツイートできるように改修しました。

その際に色々調べましたが、なかなかDjangoで認証画面を経由して画像付きツイートする一気通貫な記事がなかったので備忘録として記事を書こうと思い立ちました。
そもそもTwitter API自体がよくわからん・・・難しくない？

# Twitter APIでユーザー認証してからツイートする流れ
まずはどういう流れでDjangoとTwitter APIでユーザー認証して、ツイートするか確認します。

### 必要なもの
- API KEY
- API SECRET KEY

以上は、[Twitterデベロッパーサイト](https://developer.twitter.com/en)でアプリを申請した時に取得します。
Twitter APIを利用する場合はデベロッパーサイトで申請する必要がありますが、解説はここだけで長くなってしまうので割愛します。ググってｵﾅｼｬｽ。

### 認証手順

1. コンシューマーアプリケーション（今回はDjango）がリクエストトークンを取得するためのリクエストを作成する
1. ユーザーに認証してもらい、コンシューマーアプリケーションにリクエストトークンを送信する
1. リクエストトークンからアクセストークンを取得する
1. 3で受け取ったアクセストークンとシークレットトークンを使ってツイートする

### Djangoでの処理
手順１にて、API KEYとAPI SECRET KEYを`https://api.twitter.com/oauth/request_token`にPOSTで送ると、認証用URLが返ってきますのでリダイレクトさせます。
リダイレクト先はよくみるあの画面です。
![スクリーンショット 2021-12-14 23.54.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181775/de0fbe74-0881-8365-df42-6fb54a747530.png)
この画面で「連携アプリを認証」をクリックすると、デベロッパーサイトで指定したcallback用URLにGETパラメータ付きで返ってきますので、Django側でcallbackを受け取ってあげる必要があります。
このパラメータに`oauth_token`と`oauth_verifier`がついてくるので、`oauth_verifier`を使ってユーザーの`ACCESS_TOKEN_SECRET`を取得します。

最終的には以下のパラメータを使ってツイートします。
- API KEY（Twitterデベロッパーサイトで取得）
- API SECRET KRY（Twitterデベロッパーサイトで取得）
- oauth_verifier（ユーザー認証後のリダイレクト時に取得、ACCESS_TOKEN_SECRETを取得するために必要）
- ACCESS_TOKEN（oauth_tokenとしてユーザー認証後のリダイレクト時に取得）
- ACCESS_TOKEN_SECRET（oauth_verifierを利用して取得）

ユーザーとして使う時は認証画面うっとーしーなーと思っていましたが、実装するとなると結構面倒です。
これからは感謝しながら使います（ﾃﾉﾋﾗｸﾙｰ

### tweepy
PythonでTwitter APIを利用する際によく利用されるライブラリです。
今回はこれを使ったり使わなかったりしながら解説していきます。

https://www.tweepy.org/

# 実際のコード
サンプルプログラムの全容は以下からご確認ください。
お試し方法もREADMEに記載してあります。

https://github.com/shimayu22/turbo-enigma

## まずは自分のアカウントでツイートする
Twitterデベロッパーサイトで申請した時のアカウントでツイートする場合は比較的簡単です。

```views.py
from django.shortcuts import redirect
import tweepy

def tweet_myaccount(self):
    auth = tweepy.OAuthHandler('API_KEY', 'API_SECRET_KEY')
    auth.set_access_token('ACCESS_TOKEN', 'ACCESS_TOKEN_SECRET')
    api = tweepy.API(auth)
    api.update_status("これはテスト投稿です。tweepyを利用して投稿しています。")

    # Twitterにリダイレクトする
    return redirect('https://twitter.com/home')
```

:::note warn
'API_KEY'などの部分は実際のキーを入れてください。
ただし実際のプロジェクトでは環境変数に入れたりするなど、直接書き込まないでください。
:::

https://twitter.com/ShimayuSti/status/1470757901384187908?s=20

成功するとこんな感じでツイートできます。
ね？簡単でしょ？

## ユーザー認証をしてツイートする
続いてはこういうWebサービスでよくある、認証後にその人としてツイートするための処理です。

```views.py
from django.shortcuts import redirect
import tweepy

def tweet(self):
    # 認証準備
    auth = tweepy.OAuthHandler('API_KEY', 'API_SECRET_KEY')

    # Twitter認証画面URLを取得する
    try:
        redirect_url = auth.get_authorization_url()
    except tweepy.TweepyException:
        print("Error! Failed to get request token.")

    # ここで認証ページに遷移する
    return redirect(redirect_url)

def callback(request):
    # 認証画面でキャンセルした時の戻り先
    if 'denied' in request.GET.dict():
        return redirect('http://127.0.0.1:8000/')
    
    # 認証した場合の処理
    # ツイートするユーザーのトークンを取得する準備
    auth = tweepy.OAuthHandler('API_KEY', 'API_SECRET_KEY')
    auth.request_token['oauth_token'] = request.GET['oauth_token']
    auth.request_token['oauth_token_secret'] = oauth_verifier = request.GET['oauth_verifier']

    # ツイートするユーザーのシークレットトークンを取得する
    try:
        auth.get_access_token(oauth_verifier)
    except tweepy.TweepyException:
        print("Error! Failed to get request token.")
    
    # ツイートする
    auth.set_access_token(auth.access_token, auth.access_token_secret)
    api = tweepy.API(auth)
    api.update_status("認証画面を経由して投稿しています。これはテスト投稿です。")

    # Twitterにリダイレクトする
    return redirect('https://twitter.com/home')
```

まずは`tweet()`にて認証用URLを取得し、リダイレクトさせています。
![スクリーンショット 2021-12-15 0.39.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181775/b41ab37d-bc00-5849-321a-1ae31f01ad27.png)
「連携アプリを認証」をクリックすると、GETパラメータに`oauth_token `と　`oauth_verifier`をつけてTwitterデベロッパーサイトのCALLBACK URLSに指定したURLにアクセスします。（Djangoのログや、POSTMANを使うと`/callback?oauth_token=xxxx&oauth_verifier=xxxx`の形でアクセスしてくるのが分かります）
Django側の`callback(request)`で受けて、GETパラメータを取得します。
取得した`oauth_token `と　`oauth_verifier`を利用して`ACCESS_TOKEN_SECRET`を取得しています。
長い道のりでしたがこれで必要なパラメータが揃ったのでツイートします。

https://twitter.com/shimayu_sti/status/1470787407771738119?s=20

※私の猫アカウントです
たぶん動くと思いますが動かなかったらご連絡ください。

:::note warn
サンプルで何回も試そうとすると、同じ文言はツイートできないのでエラーになる場合があります。
その場合は文言を変更するとツイートできるはずです。
:::

## 任意の文言と画像でツイートする（ぼく将オーダーの場合）
フロントエンド側（Nuxt）で作った画像と入力した文言でツイートしたかったのですが、POSTでバックエンド側（DRF）に送っても認証のためリダイレクトしちゃうので、どこかに保存しておかないとせっかく作った画像も文言も消えてしまうんですね。
ちなみに画像はHTML2canvasで作っていますので、基本的にはbase64です。

本来ならCookieとかローカルストレージとかセッションに保存しておけばいいのでしょうが、試してみたところできなかった（自分の理解不足の）ため、データベースに保存しています。画像とはいえbase64形式であれば文字列として処理できますからね。
callback後の処理でデータベースから画像（base64）と文言を取り出していますが、tweepyではbase64そのままではアップロードできなかったので、生のTwitter APIを利用して画像アップロード→ツイートという方法を取っています。

:::note warn
以下のコードはDRFで作成しています。
実際にぼく将オーダーで使用しているコードのため、サンプルに含まれません。
:::

```views.py
class ImageTweetAPIView(views.APIView):
    """作成した画像をツイートするAPIクラス
    Args:
        img_base64 (String): imageのbase64文字列
        tweet_text (String): ツイートする文字列
    """

    def post(self, request):
        # img_base64とtweet_textをDBに保存する
        data = request.data
        # DBにデータを保存
        img_base64 = data['img_base64']
        tweet_text = data['tweet_text']

        # Twitter認証画面URLを取得する
        auth = tweepy.OAuthHandler(getenv('CONSUMER_KEY'), getenv('CONSUMER_SECRET'))

        try:
            redirect_url = auth.get_authorization_url()
        except tweepy.TweepyException:
            print("Error! Failed to get request token.")

        oauth_token = parse.parse_qs(parse.urlparse(redirect_url).query)['oauth_token'][0]
        # Tweetテーブルに保存する
        t = Tweet(oauth_token=oauth_token, img_base64=img_base64, tweet_text=tweet_text)
        t.save()

        # ここで認証ページに遷移する
        return redirect(redirect_url)

class TwitterCallbackAPIView(views.APIView):
    """Twitter認証のコールバック
    Args:
        oauth_token(String): ユーザートークン
        oauth_verifier(String): ユーザー認証コード
    """

    def get(self, request):
        # 認証画面でキャンセルした時の戻り先
        if 'denied' in request.query_params.dict():
            # denied=oauth_tokenをキーにTweetからデータを削除する
            t = Tweet.objects.get(oauth_token=request.query_params['denied'])
            t.delete()
            return redirect(getenv('BACK_URL'))

        # 認証した場合の処理
        # ツイートするユーザーのトークンを取得する準備
        auth = tweepy.OAuthHandler(getenv('CONSUMER_KEY'), getenv('CONSUMER_SECRET'))
        auth.request_token['oauth_token'] = oauth_token = request.query_params['oauth_token']
        auth.request_token['oauth_token_secret'] = oauth_verifier = request.query_params['oauth_verifier']

        # ツイートするユーザーのシークレットトークンを取得する
        try:
            auth.get_access_token(oauth_verifier)
        except tweepy.TweepyException:
            print("Error! Failed to get request token.")

        # oauth_tokenをキーにしてTweetからデータを取得する
        t = Tweet.objects.get(oauth_token=oauth_token)
        img_base64 = t.img_base64

        # 画像をUploadする
        oauth = OAuth1(getenv('CONSUMER_KEY'), getenv('CONSUMER_SECRET'), auth.access_token, auth.access_token_secret)
        headers = {'Content-Transfer-Encoding': 'base64'}
        upload_payload = {"media_data": img_base64}
        upload_response = requests.post(getenv('UPLOAD_API'), auth=oauth, headers=headers, data=upload_payload)

        upload_response_dict = json.loads(upload_response.text)
        media_id = upload_response_dict['media_id']

        # ツイートする
        tweet_text = t.tweet_text
        update_payload = {"status": tweet_text, "media_ids": media_id}
        update_response = requests.post(getenv('TWEET_API'), auth=oauth, headers=headers, data=update_payload)

        # Tweetからデータを削除する
        t.delete()

        # Twitterにリダイレクトする
        return redirect('https://twitter.com/home')
```

base64をデコードしてどこかに保存してからであればtweepyでも画像付きでツイートできますが、そのためにS3使ったりするのも面倒だったのでこうなりました。
もっといいやり方ないですかね・・・？

# おわりに
もしツイートできないなどバグがありましたらここのコメントか[Twitter](https://twitter.com/ShimayuSti)にてフィードバックいただけると幸いです。
サンプルのバグもありましたら同様にここのコメントかGitHubのissueでもいいですし、プルリクいただければ対応します。

https://npb-battingorder-maker.netlify.app/

https://github.com/shimayu22/turbo-enigma

# 参考
[Twitterデベロッパーサイト](https://developer.twitter.com/en)
[メディアのアップロード](https://developer.twitter.com/ja/docs/media/upload-media/api-reference/post-media-upload)
[Tweepy Documentation](https://docs.tweepy.org/en/v3.10.0/index.html)
[【Python3】tweepyで認証情報(AccessToken)の取得方法](https://sleepless-se.net/2018/11/17/hwo-to-get-tweepy-accesstoken-on-python3/)
[Python / TweepyでOAuth認証してツイートする](https://qiita.com/kaonashikun/items/f4ef80f68bcf5f8c6351)
[ユーザー認証を行ってAccess Token, Access Token Secretを取得する](https://www.wabiapp.com/PronamaStudy/php/php_twitter_auth.html)
[[Python]Tweepyでツイートを画像付きで投稿する](https://code-schools.com/tweepy-tweet/)
[TweepyでTwitterに投稿する](https://qiita.com/pontyo4/items/da8ba576c6281d67c0c7)
[Tweepy で Twitter に画像付きのツイートをする](https://qiita.com/ekzemplaro/items/f0c768215d7d1a7b90ac)
[Pythonで『tweepy』を使いこなす方法【テキスト・改行・複数画像添付】](https://karupoimou.hatenablog.com/entry/2019/05/17/080251)
[Tweepyを使って、PythonでTwitterのAPIを超簡単に操作する](https://blog.statsbeginner.net/entry/2015/10/21/131717)
[.env ファイルで環境変数を設定する (python-dotenv)](https://maku77.github.io/python/env/dotenv.html)
[Django URLパラメータをセッションに保存してテンプレートで利用する](https://btj0.com/blog/django/session/)
[[Python]Base64でエンコードされた画像データをデコードする](https://qiita.com/TsubasaSato/items/908d4f5c241091ecbf9b)