🐦

X (Twitter) API + Oauth 1.0a で画像付き投稿をする

2024/09/03に公開

キービジュアル

はじめに

画像付きツイートをするには現状(2024.09時点)では v1.1 API を用いて画像をアップロードし、その結果( media_id )を用いてツイートする必要があります。
そして Twitter の v1.1 API へのリクエストには OAuth 1.0a が必要なため、OAuth 1.0a を用いて認証し画像付きツイートをするまでを丁寧に解説していきます。

v2 API である https://api.twitter.com/2/tweets も OAuth 1.0a の認証で利用することができるため、OAuth 1.0a の認可を得られれば画像付きツイートをを行うことができます。

最初調べ始めたとき、すぐに実装できるだろうと思っていたのですが情報が分散しているのと、動く状態のサンプルコードがなく実装にかなり苦戦しました。また、OAuth 1.0a 自体の複雑さも相まって、実装にはかなりの時間を要しました。
意外と需要がありそうなのに情報が少ないので、しっかりとまとめていこうと思います。

今回の実装は Python で行いました。トークン周りさえ設定すれば実際に画像付き投稿が可能な状態のコードも GitHub にアップしておいたので合わせて参照してください。なお、理解を促進することを目的としているため、丁寧に構造化はしていません。シンプルに動くコードを目指しています。

https://github.com/edom18/TwitterAPI-Sample

ちなみにこの実装が必要になったのは、今開発している VR ゲーム内で生成した画像を X に投稿する機能を実装する必要があったためです。
短くサッと遊べて、おもしろ画像が生成できるミニゲームになっているので、ぜひプレイしてみてください!

https://x.com/sword_vri/status/1830504901182632071

全体の流れの把握

OAuth 1.0a を用いた認証・認可および API リクエストはかなり複雑になってくるので、全体の流れを概観しておきます。

  1. 各種 API へのリクエストに際して、シグネチャの生成およびヘッダの設定が必要
    1. ユーザのアクセストークンを取得する前の段階で利用するリクエストトークン取得 API などでも同様にシグネチャが必要
  2. OAuth 1.0a 認証方式を利用しユーザ認可を得てアクセストークンを取得する
    1. トークンの取得にはリクエストトークンなどのフローを用いてアクセストークンに変換する
  3. 取得したアクセストークンを用いてメディアアップロード API を用いて画像をアップロードする
  4. アップロードした画像の media_id を用いてツイート API にリクエストする

ものすごくざっくりまとめると、「各種リクエストには複雑なヘッダ設定処理を伴う。そして実際に投稿をするためのアクセストークンおよびアクセストークンシークレットを取得するために、リクエストトークンの取得から開始する」という感じです。

トークン取得の話の前に、まずは一番大事なヘッダの設定とシグネチャの生成について解説していきます。

シグネチャの生成とヘッダの設定

ぶっちゃけ、ここの部分が理解できればほぼすべての API のリクエストはむずかしくないと思います。ということで、しっかりとここの章は把握してください。

今回の実装にあたって、大まかな実装方針については以下の記事を参考にさせていただきました。

https://qiita.com/kerupani129/items/ee9d894cc67101f16c3f

上記記事から、シグネチャ生成についての流れの図を引用させていただきます。

シグネチャ生成のフロー図

この図が説明しているのは、署名に際して キーをどう生成値をどう生成 しているかを示しています。

ものすごくざっくり説明すると「API リクエストに必要なパラメータとメソッド名(POST や GET など)、URL をひとまとめにしたデータを、アプリのキー(Consumer Secret )とユーザのシークレット( Access Token Secret )を用いてハッシュ化し、これを署名とする」ということをやっています。

実装していく中で苦労したのは、リクエストヘッダに設定しなければならない値などが API ごとにちょっとずつ違っていたり、利用している署名のキー( Access Token Secret など)が間違っていたりしてもエラー内容が Unauthorized のみで返ってくるため、なにが問題なのかの原因特定が困難だったことです。

ヘッダに設定するデータについてはこのあと詳しく見ていきますが、署名のキーとなる部分については以下のことを頭に入れておいてください。

アプリ開発者の Consumer Secret投稿するユーザの Access Token Secret が必要である」

では早速生成について見ていきましょう。

リクエストに含める必要のある必須のパラメータ

次に示すのは、どのリクエストにも必ず含めるパラメータのリストです。前述したように、API によっては以下のパラメータリストに追加のパラメータが必要なことがありますが、基本的にどのリクエストでも必ずこれらは必要となります。

【API リクエスト時に必ず含める必要のあるパラメータ】

Key Value (一部は例) 説明
oauth_consumer_key xxxxxxxxxxxx Twitter の Developer Portal で作成したアプリの Consumer Key
oauth_nonce yyyyyyyyyyyy リクエストごとに生成する一意の ID
oauth_signature_method HMAC-SHA1 シグネチャメソッドは「HMAC-SHA1」を使用
oauth_timestamp 1724938694 リクエスト時のタイムスタンプ
oauth_version 1.0 OAuth 1.0a を利用するため「1.0」
oauth_signature zzzzzzzzzzzz 諸々のデータを利用して作成したシグネチャ(base64 エンコーディング)(後述)

シグネチャおよびヘッダの生成のためのデータ

さて、ではこれらのデータを使ってどうやってシグネチャを生成するのかを見ていきます。

シグネチャの生成に含めるパラメータは利用する API ごとに異なるため、まずは投稿 API へリクエストすることを前提に解説していきます。(おそらく一番シンプルな API リクエストだと思います)

対象となる API の URL は https://api.twitter.com/2/tweets です。

この API にリクエストすることを前提に、まずは必要となるすべてのデータをリストアップします。

Key 説明
oauth_consumer_key Twitter の Developer Portal で作成したアプリの Consumer Key
oauth_consumer_secret Twitter の Developer Portal で作成したアプリの Consumer Secret
oauth_nonce リクエストごとに生成する一意の ID
oauth_signature_method シグネチャメソッドは「HMAC-SHA1」を使用
oauth_timestamp リクエスト時のタイムスタンプ
oauth_token 認可フローにて取得したユーザのアクセストークン
oauth_token_secret 認可フローにて取得したユーザのアクセストークンシークレット
oauth_version OAuth 1.0a を利用するため「1.0」
oauth_signature 諸々のデータを利用して作成したシグネチャ(base64 エンコーディング)
endpoint URL 実際に叩く API の URL
text 投稿するテキスト

長々と説明をする前に、まずは実際のコードを見たほうが理解が早いと思うのでコードを示します。いくつかのポイントについてはコメントに記載しています。

シグネチャの生成とヘッダの作成
# API リクエストに利用するヘッダを作成する
def create_oauth_header(
        endpoint_url,
        oauth_consumer_key,
        oauth_consumer_secret,
        oauth_token_secret,
        verbose=False,
        **additional_parameters):

    # ヘッダに含める必要があるパラメータの設定・生成
    method = "POST"
    oauth_signature_method = "HMAC-SHA1"
    oauth_version = "1.0"
    oauth_nonce = generate_nonce()
    oauth_timestamp = str(get_timestamp())

    oauth_parameters = {
        "oauth_consumer_key": oauth_consumer_key,
        "oauth_signature_method": oauth_signature_method,
        "oauth_timestamp": oauth_timestamp,
        "oauth_nonce": oauth_nonce,
        "oauth_version": oauth_version,
    }

    # 追加のパラメータがある場合はここで追加する
    all_parameters = oauth_parameters.copy()
    all_parameters.update(additional_parameters)

    # パラメータは、キーを昇順で並べる必要がある
    # パラメータの値は URL エンコーディングが必要
    sorted_parameters = "&".join(f"{encode_text(k)}={encode_text(v)}" for k, v in sorted(all_parameters.items()))

    # ベースストリングの作成
    base_string = f"{method}&{encode_text(endpoint_url)}&{encode_text(sorted_parameters)}"

    # Create a signing key
    signing_key = f"{encode_text(oauth_consumer_secret)}&{encode_text(oauth_token_secret)}"

    # シグネチャのハッシュを計算し、base64 エンコーディング
    signature = base64.b64encode(hmac.new(signing_key.encode(), base_string.encode(), hashlib.sha1).digest()).decode()

    if verbose:
        print(f"Base String: {base_string}")
        print("---------------------------------------------")
        print(f"Signing Key: {signing_key}")
        print("---------------------------------------------")
        print(f"Sorted Parameters: {sorted_parameters}")
        print("---------------------------------------------")
        print(f"Base String: {base_string}")
        print("---------------------------------------------")
        print(f"Signature: {signature}")

    # Authorization ヘッダの作成
    all_parameters["oauth_signature"] = signature

    # ヘッダは key="value", key="value", ... として並べる
    auth_header = "OAuth " + ", ".join([f'{encode_text(k)}="{encode_text(v)}"' for k, v in all_parameters.items()])

    return auth_header

データの準備

順に見ていきましょう。

必須パラメータの設定
# ヘッダに含める必要があるパラメータの設定・生成
method = "POST"
oauth_signature_method = "HMAC-SHA1"
oauth_version = "1.0"
oauth_nonce = generate_nonce()
oauth_timestamp = str(get_timestamp())

前述の通り、リクエストに必須で含める必要のあるパラメータのうちの 4 つと、API へのメソッド名を定義しています。

上 3 つは固定値なので問題ないですね。
投稿用 API は POST になるので POST を指定します。oauth_version は、今回は 1.0 を用いて認証するため 1.0 を指定します。また、署名のハッシュアルゴリズムは HMAC-SHA1 を用いることが Twitter の API 仕様で決められているためこれを指定します。

続くふたつの値について説明します。

oauth_nonce は「Number used once」の略で、通信の際に利用される一度きりだけ使われる任意の文字列です。基本的に重複しない値なら問題ありません。
最後の oauth_timestamp は、API を叩くときのタイムスタンプです。これが、サーバの時間とあまりに大きく違う場合はエラーになるようです。

generate_nonce の実装
generate_nonce
def generate_nonce(length=32):
    return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
timestamp 計算の実装
timestamp
def get_timestamp():
    # 現在の時刻をUTCで取得
    now = datetime.datetime.utcnow()

    # UNIXタイムスタンプを取得(秒単位)
    timestamp = int(time.mktime(now.timetuple()))

    return timestamp

続く辞書定義はリクエストヘッダに含める内容をまとめたものです。

リクエストヘッダに含める内容の辞書化
oauth_parameters = {
    "oauth_consumer_key": oauth_consumer_key,
    "oauth_signature_method": oauth_signature_method,
    "oauth_timestamp": oauth_timestamp,
    "oauth_nonce": oauth_nonce,
    "oauth_version": oauth_version,
}

今回の実装では上記の辞書に追加辞書として以下のようにパラメータを追加するようにしています。

追加パラメータの追加
# 追加のパラメータがある場合はここで追加する
all_parameters = oauth_parameters.copy()
all_parameters.update(additional_parameters)

データの整形

パラメータの準備が整いました。ここから、このパラメータを用いてシグネチャおよびヘッダを生成していきます。

ここで準備するのは以下の形にパラメータを並べることです。

key=value&key=value&...
パラメータの整形
# パラメータは、キーを昇順で並べる必要がある
# パラメータの値は URL エンコーディングが必要
sorted_parameters = "&".join(f"{encode_text(k)}={encode_text(v)}" for k, v in sorted(all_parameters.items()))

このコードは辞書に格納されたパラメータを、キーを元に昇順でソートした上で key=value 形式に変換し、& で連結しています。また value は RFC 3986 に基づいた URL エンコードする必要があります。この処理は、シグネチャ生成のために必要なデータを整形するための処理です。

シグネチャの生成

情報が揃ったので、いよいよシグネチャを生成していきます。

シグネチャの生成は HMAC-SHA1 アルゴリズムを用います。そしてそのキーとなるのが Consumer SecretAccess Token Secret です。また、値の部分(ベースストリング)は上記のパラメータを用いて以下のように生成します。

シグネチャの生成(ベースストリング)
# ベースストリングの作成
base_string = f"{method}&{encode_text(endpoint_url)}&{encode_text(sorted_parameters)}"

このベースストリングは「リクエストメソッド、エンドポイント URL、パラメータを & で連結したもの」です。

前述のソート時にも指定していましたが、各データは RFC 3986 に基づくエンコードを行う必要があります。これは特殊文字を % でエンコードする処理です。( encode_text メソッドの実行部分)

encode_text の実装
encode_text
def encode_text(text):
    # RFC 3986 に基づく URL エンコード
    return urllib.parse.quote(text, safe='')

次に生成しているのはハッシュ化に用いるキーです。これは Consumer SecretAccess Token Secret& で連結したものです。

シグネチャの生成(ハッシュ化キー)
# Create a signing key
signing_key = f"{encode_text(oauth_consumer_secret)}&{encode_text(oauth_token_secret)}"

最後に、準備したキーとデータを元にハッシュ化し、さらにそれを base64 エンコードします。

シグネチャの生成(ハッシュ化)
# Create a signature
signature = base64.b64encode(hmac.new(signing_key.encode(), base_string.encode(), hashlib.sha1).digest()).decode()

ヘッダの生成

前段まででシグネチャの生成が終わりました。最後に、それを用いて最終的なヘッダを作成します。

シグネチャの生成(ヘッダの作成)
# Authorization ヘッダの作成
all_parameters["oauth_signature"] = signature

# ヘッダは key="value", key="value", ... として並べる
auth_header = "OAuth " + ", ".join([f'{encode_text(k)}="{encode_text(v)}"' for k, v in all_parameters.items()])

作成したシグネチャを oauth_signature というパラメータ名に設定し、それを含めたヘッダを作成します。ヘッダはすべてのパラメータを key="value" の形式にしたものを , 区切りで並べたものになります。

最終的に生成されるヘッダの見た目は以下のようになります。(見やすいように改行を含めていますが、実際には改行は含めません)

ヘッダの見た目
OAuth
    oauth_consumer_key="xvz1evF4PTGEFPog", 
    oauth_nonce="6eb24361e7250e5112288fa54dd8f634a732034c4301910c2dc8b3db", 
    oauth_signature_method="HMAC-SHA1", 
    oauth_timestamp="1581159389", 
    oauth_token="370773112-GmHMAgYyLbNIKZeRFsMPR9EMZ9weJAEb", 
    oauth_version="1.0", 
    oauth_signature="8TACi1tsshSi9dfiLa8Vm8SasTs%3D"

簡単にまとめると「ヘッダに含める情報を用いて署名を行った」というわけです。そのため、送信内容と署名に誤りがあると Unauthorized が返ってくることになります。

実際に投稿してみる

だいぶかかりましたが、リクエストに利用できるヘッダの作成が終わりました。これを用いて実際に投稿する API にリクエストする処理を見てみます。

投稿に必要な Consumer Key などは dotenv モジュールを用いて読み込んでいます。

実際に投稿する処理
def request(parameters):
    endpoint_url = "https://api.twitter.com/2/tweets"

    # ここでは、各データを dotenv で読み込んでいる
    oauth_consumer_key = os.environ.get("CONSUMER_KEY")
    oauth_consumer_secret = os.environ.get("CONSUMER_SECRET")
    oauth_token = os.environ.get("AUTH_TOKEN")
    oauth_token_secret = os.environ.get("AUTH_TOKEN_SECRET")

    # ツイートの API にはユーザの Access Token と Access Token Secret をヘッダに含める必要がある
    auth_header = create_oauth_header(
        endpoint_url=endpoint_url,
        oauth_consumer_key=oauth_consumer_key,
        oauth_consumer_secret=oauth_consumer_secret,
        oauth_token=oauth_token, # ここで指定しているアクセストークンは、後述する認可フローによって取得したユーザのアクセストークン
        oauth_token_secret=oauth_token_secret, # と同様に取得したアクセストークンシークレット
        verbose=False)

    # リクエストヘッダーのセット
    headers = {
        "Content-Type": "application/json",
        "Authorization": auth_header,
    }

    print(f"Access the api [{endpoint_url}] ...")

    response = requests.post(endpoint_url, headers=headers, json=parameters)
    return response

if __name__ == "__main__":
    parameters = {
        "text": "Hello, Twitter API!",
    }
    response = request(parameters)
    print(response.text)

create_oauth_header 関数は前段で解説したものです。この関数を通してリクエストヘッダを生成します。

ヘッダを設定している部分は以下の通りです。

ヘッダの設定
headers = {
    "Content-Type": "application/json",
    "Authorization": auth_header,
}

実際にリクエストを送っているのが以下の部分です。

リクエストの送信
response = requests.post(endpoint_url, headers=headers, json=parameters)

リクエストに付与されている parameters は以下のように設定した、ポストする内容です。

投稿パラメータ
parameters = {
    "text": "Hello, Twitter API!",
}

投稿が成功するとツイート ID などが返却され、もし失敗した場合は Unauthorized などのエラーが返ってきます。

シグネチャ生成、ヘッダ生成の部分が完成してしまえば、リクエストそのものはシンプルなのが分かってもらえたでしょうか。


さて、一番重要かつ複雑な署名についての説明が終わりました。これが理解できてしまえば他の API リクエストは攻略したも同然です。

改めて押さえておきたいポイントは以下です。

  • リクエストに含めるパラメータには必須の内容がある
  • 投稿などほとんどの API リクエストにはアクセストークンが必要
  • リクエストヘッダはパラメータを元にした署名(シグネチャ)が必要

そしてこれから説明するのは、API リクエストに必要な「ユーザのアクセストークンを取得する方法」となります。それが「認可フロー」です。

認可フローを理解する

ということで、まずは認可フローの全体の流れを概観します。シグネチャ生成とは少し違った複雑さがあるので全体の流れをまずは把握しておきましょう。

  1. https://api.twitter.com/oauth/request_token API へリクエストし、「アクセストークンの交換に必要なリクエストトークン」を取得する
    1. API から返される値が oauth_token となっているので混乱するが、これはまだアクセストークンではない
  2. 取得したリクエストトークンを用いて https://api.twitter.com/oauth/authenticate へ「ユーザにアクセス」してもらう
    1. ここはユーザ操作による認可になるためブラウザなどで X へアクセスしてもらい、許可ボタンを押してもらう必要がある
    2. API リクエストではない点に注意
  3. ユーザが認可してくれた場合(許可ボタンを押下した場合)、事前に設定したコールバック先にブラウザが遷移する
    1. このコールバック先は任意ではなく、Twitter Developer Portal で設置しておく必要がある。自由に設定できると意図しないページ飛ばされる、という懸念があるため
    2. 上記理由により Twitter Developer Portal のコールバック URL リストに設定していない URL をリクエストに含めるとエラーになる
  4. コールバック先の URL に、クエリの形で oauth_tokenoauth_verifier というパラメータが付与されて返ってくる
    1. e.g.) https://example.com/callback?oauth_token=xxxx&oauth_verifier=yyyy
    2. この verifier コードは生存期間が 30 秒ほどらしいので、時間が経つと使えなくなる点に注意
  5. この oauth_tokenoauth_verifier をリクエストに含めて https://api.twitter.com/oauth/access_token にリクエストを送る
    1. このリクエストが正常に返ってきたら、晴れてユーザのアクセストークンとアクセストークンシークレットが手に入る

文章だけで見てもだいぶ込み入っているのが分かるかと思います。

大雑把にまとめると、

  1. ユーザの認可に伴う下準備をする
  2. ユーザに認可してもらう
  3. 認可してもらったらアクセストークンを取得する

という流れになります。

認可フローの実装

では実際にこれを実装していきます。

ちなみに、ユーザがブラウザを介して許可をする以外の API リクエストすべてに、前述のシグネチャ生成のフローが必要となります。それを念頭に入れてこの先を読み進めてください。

今回実装した内容を以下に示します。

認可フローの実装
import os
import requests
from utility import create_oauth_header
import dotenv
dotenv.load_dotenv()

# Consumer Key と Consumer Secret は dotenv で読み込む
oauth_consumer_key = os.environ.get("CONSUMER_KEY")
oauth_consumer_secret = os.environ.get("CONSUMER_SECRET")

# リクエストトークン取得のための API とコールバック先の指定
callback_url = "http://localhost:11230/oauth/redirect"
request_endpoint_url = "https://api.twitter.com/oauth/request_token"

# まずはリクエストトークンを取得するための API リクエストのヘッダを作成する
auth_header = create_oauth_header(
    endpoint_url=request_endpoint_url,
    oauth_consumer_key=oauth_consumer_key,
    oauth_consumer_secret=oauth_consumer_secret,
    oauth_token_secret="", # まだアクセストークンシークレット取得前なので空文字で OK
    oauth_callback=callback_url, # ヘッダにも oauth_callback を含めないとデフォルトのコールバック先に飛ばされてしまう
    verbose=False)

req_headers = {
    "Authorization": auth_header,
}

# リクエスト内容にコールバック先 URL を含める
request_token_params = {
    "oauth_callback": callback_url,
}
response_req = requests.post(request_endpoint_url, headers=req_headers, json=request_token_params)
response_req_text = response_req.text

# 返されたリクエストトークンを取り出す
oauth_token_kvstr = response_req_text.split("&")
token_dict = {x.split("=")[0]: x.split("=")[1] for x in oauth_token_kvstr}
req_oauth_token = token_dict["oauth_token"]

authenticate_url = "https://api.twitter.com/oauth/authenticate"

# コンソールに URL が表示されるのでそこにアクセスし許可ボタンを押す
print("Please access the following URL and get the OAuth Verifier.")
print(f"{authenticate_url}?oauth_token={req_oauth_token}")

# 許可後に遷移したコールバック URL に付与されている verifier を手入力する
oauth_verifier = input("OAuth Verifierを入力してください> ")

access_endpoint_url = "https://api.twitter.com/oauth/access_token"

auth_header = create_oauth_header(
    endpoint_url=access_endpoint_url,
    oauth_consumer_key=oauth_consumer_key,
    oauth_consumer_secret=oauth_consumer_secret,
    oauth_token_secret="", # まだアクセストークンシークレットは未取得なので空文字で OK
    oauth_token=req_oauth_token, # リクエストトークン
    oauth_verifier=oauth_verifier, # 手入力した verifier
    verbose=False)

acc_headers = {
    "Authorization": auth_header,
}

verifier_params = {
    "oauth_token": req_oauth_token,
    "oauth_verifier": oauth_verifier,
}
response_acc = requests.post(access_endpoint_url, headers=acc_headers, json=verifier_params)
response_acc_text = response_acc.text
print(response_acc_text)

上から順に説明していきます。

リクエストトークンの取得

まず最初に行うのはリクエストトークンの取得です。

リクエストトークンの取得
callback_url = "http://localhost:11230/"
request_endpoint_url = "https://api.twitter.com/oauth/request_token"

auth_header = create_oauth_header(
    endpoint_url=request_endpoint_url,
    oauth_consumer_key=oauth_consumer_key,
    oauth_consumer_secret=oauth_consumer_secret,
    oauth_token_secret="", # まだアクセストークンシークレット取得前なので空文字で OK
    oauth_callback=callback_url, # ヘッダにも oauth_callback を含めないとデフォルトのコールバック先に飛ばされてしまう
    verbose=False)

req_headers = {
    "Authorization": auth_header,
}

request_token_params = {
    "oauth_callback": callback_url,
}
response_req = requests.post(request_endpoint_url, headers=req_headers, json=request_token_params)

重要なポイントに絞って解説します。

リクエストトークンを取得する時点ではまだアクセストークンもアクセストークンシークレットも取得していません。そのため、oauth_token_secret には空文字を指定します。また、コールバック先の指定として oauth_callback を指定します。

そしてリクエストパラメータに oauth_callback を指定してリクエストを送ります。このコールバック先の URL は、前述した通り、Twitter Developer Portal で設定しておいたものを指定する必要があります。もし登録していない URL を指定するとエラーになるので注意してください。

リクエストが成功すると以下のようなフォーマットでレスポンスが返ってきます。

レスポンス
oauth_token=<TOKEN>&oauth_token_secret=<SECRET>&oauth_callback_confirmed=true

ユーザに認可してもらう

ここから oauth_token を取り出して次のステップに進みます。

リクエストトークンの取得(レスポンス)
# 返されたリクエストトークンを取り出す
oauth_token_kvstr = response_req_text.split("&")
token_dict = {x.split("=")[0]: x.split("=")[1] for x in oauth_token_kvstr}
req_oauth_token = token_dict["oauth_token"]

oauth_token を取り出したら、それを利用してユーザに認可をしてもらうための URL を生成します。

今回は生成した URL に自分でアクセスし、そこで認可後に取得された verifier を手入力する想定で実装しています。

リクエストトークンの取得(認可 URL)
authenticate_url = "https://api.twitter.com/oauth/authenticate"

# 自身で、生成された URL にアクセスし許可する
print("Please access the following URL and get the OAuth Verifier.")
print(f"{authenticate_url}?oauth_token={req_oauth_token}")

oauth_verifier = input("OAuth Verifierを入力してください> ")

verifier を取得したらそれを入力して次に進める想定です。(今回はあくまで全体の流れを把握するためなので自身でそれぞれの処理を実行していることを想定しています)

手作業で入力した verifier を用いてアクセストークンを取得するリクエストを送ります。
リクエスト処理は以下の通りです。

アクセストークンの取得
access_endpoint_url = "https://api.twitter.com/oauth/access_token"

auth_header = create_oauth_header(
    endpoint_url=access_endpoint_url,
    oauth_consumer_key=oauth_consumer_key,
    oauth_consumer_secret=oauth_consumer_secret,
    oauth_token_secret="", # まだアクセストークンシークレットは未取得なので空文字で OK
    oauth_token=req_oauth_token, # リクエストトークン
    oauth_verifier=oauth_verifier, # 手入力した verifier
    verbose=False)

acc_headers = {
    "Authorization": auth_header,
}

verifier_params = {
    "oauth_token": req_oauth_token,
    "oauth_verifier": oauth_verifier,
}
response_acc = requests.post(access_endpoint_url, headers=acc_headers, json=verifier_params)

ここもまた重要な点に絞って解説します。

今回のリクエストでは、前段で取得したトークンがあるため oauth_token にそれを指定しています。また、oauth_verifier には先ほど取得した verifier を指定します。どちらもヘッダに含める必要がある点に注意してください。

そしてリクエストのパラメータとしても oauth_tokenoauth_verifier を指定してリクエストを送ります。

無事にリクエストが成功すると以下のフォーマットでアクセストークンとアクセストークンシークレットが返却されます。

レスポンス
oauth_token=<TOKEN>&oauth_token_secret=<SECRET>&user_id=<USER_ID>&screen_name=<SCREEN_NAME>

これで無事、各 API リクエストに必要なアクセストークンとアクセストークンシークレットを取得することができました。

シグネチャ生成部分とアクセストークン取得部分が理解できれば、あとは各種 API リクエストの必要パラメータに沿って準備し、リクエストを投げることで投稿することができるようになります。

画像をアップロードする

ここからは実際に画像をアップロードしそれをツイートするまでを書いていきたいと思います。

しかしながら、シグネチャ生成、アクセストークン取得など一番大変なところは解説済みなので、以降はそこまで複雑なことはないと思います。

まず、画像アップロードができる API は現時点(2024.09)では v1.1 の API である https://upload.twitter.com/1.1/media/upload.json です。

そしてこの API を利用するために OAuth 1.0a での認証が必要であることは冒頭で説明しました。ここではこの API に画像をアップロードする方法について見ていきます。

画像のアップロードになるのでリクエストにはバイナリデータを送ることができる multipart/form-data 形式を利用します。ということで、まずはリクエストしているコードを見てみます。

画像をアップロードする
def post(files):
    endpoint_url = "https://upload.twitter.com/1.1/media/upload.json"

    oauth_consumer_key = os.environ.get("CONSUMER_KEY")
    oauth_consumer_secret = os.environ.get("CONSUMER_SECRET")
    oauth_token = os.environ.get("AUTH_TOKEN")
    oauth_token_secret = os.environ.get("AUTH_TOKEN_SECRET")

    auth_header = create_oauth_header(
        endpoint_url=endpoint_url,
        oauth_consumer_key=oauth_consumer_key,
        oauth_consumer_secret=oauth_consumer_secret,
        oauth_token=oauth_token, # リクエストトークンの交換で取得したアクセストークン
        oauth_token_secret=oauth_token_secret, # とアクセストークンシークレット
        verbose=False)

    # リクエストヘッダーのセット
    headers = {
        "Authorization": auth_header,
    }

    print(f"Access the api [{endpoint_url}] ...")

    response = requests.post(endpoint_url, headers=headers, files=files)
    return response

ツイート投稿の API 呼び出しとほぼ同じ見た目になっているのが分かるかと思います。違いは files をリクエストに含めている点です。

filesdict 型になっており、パラメータ名は media になっています。そしてその内容はファイルを読み込んだファイルオブジェクトになっています。

ファイルの指定
file = open(image_file_path, "rb")
files = {
    "media": file,
}

上記理由により、実際にリクエストが送られる際のログを見てみると multipart/form-data のフィールドとして media が追加され、内容がファイルのバイナリとして送られているのが分かります。

send: b'--b37c71c90dca02b483a864b6a7b6e7b6\r\nContent-Disposition: form-data; name="media"; filename="logo.jpg"\r\n\r\n\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xe2\x02\xa0ICC_PROFILE\x00\xn...略'

画像のアップロードに成功するとアップロードした画像の media_ia がレスポンスとして返されます。この media_id を利用してツイート投稿をすることで画像付きポストができるようになります。

レスポンスは以下のような形で返ってきます。

レスポンス
{
    'media_id': 1829075979844874240,
    'media_id_string': '1829075979844874240',
    'size': 41809,
    'expires_after_secs': 86400,
    'image': {
        'image_type': 'image/jpeg',
        'w': 400,
        'h': 400
    }
}

この media_id を使って画像付きツイートを投稿していきます。

画像付きツイートを投稿する

いよいよ最後のパートです。画像アップロードまで完了しました。あとはアップロードした画像付きツイートを投稿することができれば当初の目的が達成されます。

ただ、ツイート投稿そのものはシグネチャ生成の時点で説明してあります。実は投稿内容に少し手を加えるだけで達成することができます。

具体的には以下のように、ツイート文に加えて media_id を含めるだけです。言い換えると、 media_id を含めれば画像付きツイートを、含めなければテキストツイートを投稿することができるということです。

画像付きツイートの投稿
media_id = response.json()["media_id_string"]

parameters = {
    "text": tweet_text,
    # Media ID を配列で指定する
    "media": {
        "media_ids": [media_id],
    },
}

tweet_response = post_tweet_request(parameters)

post_tweet_request はシグネチャ生成のときに解説したものと同様です。以下のようにユーティリティを読み込むときに rename しているだけです。

post_tweet_request の import
from post_tweet import request as post_tweet_request

これで画像付きツイートの投稿が完了です。

まとめ

画像のアップロードおよびツイート投稿そのものは単に API にリクエストを投げるだけなのでむずかしいことはありません。
むずかしさを増しているのは、OAuth 1.0a による認可フローとシグネチャ生成です。

冒頭でも書きましたが「画像付き投稿をプログラムから行う」という比較的需要がありそうなことが、一気通貫で分かりやすく書かれている記事がない印象です。

それはひとえに、認証フローの複雑さなどもあると思っています。

この記事が、画像付きツイートをしたい方の助けになれば幸いです。

告知

冒頭でも書きましたが、この機能は開発中の VR ゲームで画像付きツイートをさせたかったのがきっかけでした。Meta Quest 向けのゲームで、実際に剣を振ることで斬撃が出て食材を切る、タイミングゲーとなっています。

また、最近話題の生成 AI を利用し、カットした食材に応じて毎回様変わりする料理イラストを楽しむことができるゲームになっています。

4Gamers さんと Yahoo! ニュースにリリースが掲載されました!

https://www.4gamer.net/games/833/G083385/20240902004/

https://news.yahoo.co.jp/articles/e6cd82a63024a4a0d8b5e241c9e45799dd162c20

https://x.com/sword_vri/status/1830504901182632071

Discussion