
前回、X API v2 で POST /2/tweets が 403「You are not permitted to perform this action」になる本当の原因 という記事を書きました。結論は「新規(低評価)アカウントがハッシュタグ付きの投稿を API から行うと、X のスパム判定で 403 になる」というもの。対策は単純で、X 投稿の本文からハッシュタグを外すことでした。
ただ、これには続きがあります。ハッシュタグを完全に捨てるのはもったいないので、実運用では「API ではハッシュタグ無しで投稿し、投稿後に X Premium の編集機能で手作業でハッシュタグを足す」という運用でしのいでいました。この記事は、その 残っていた手作業を X API の edit_options で完全自動化した、という後日談です。
結論だけ先に書くと、POST /2/tweets の edit_options.previous_post_id を使えば「投稿 → 編集」の2段階が API だけで完結します。ただし1か所だけ落とし穴(画像が消える)があるので、そこも含めて共有します。
環境
- X API v2、有料(Pay-Per-Use)プラン、対象アカウントは X Premium(編集機能は Premium 限定)
- 認証:OAuth 1.0a ユーザーコンテキスト(権限は Read and write)。OAuth 2.0 ユーザーコンテキストでも同様に動作
- 投稿処理:メディアを v1.1
media/uploadでアップロード →POST /2/tweetsにmedia_idsを添付 - 言語:Python(
requests+requests_oauthlib)
前回とまったく同じ構成です。変更点は「投稿後に編集を1回足す」だけです。
前回の到達点と、残っていた手作業
前回の対策で、X 投稿は次の形に落ち着いていました。
- 作成時:ハッシュタグを外した本文(+画像)で
POST /2/tweets→ 201 で安定して通る - その後:X アプリ/ブラウザで当該ポストを開き、手で
#測地系 #座標変換 …を書き足して保存(X Premium の編集)
作成の自動化はできていたのに、ハッシュタグ追加だけが手作業として残っていたわけです。1日4投稿もあると地味に面倒で、付け忘れも起きます。ここを潰したいというのが今回の動機です。
edit_options とは
X API v2 の POST /2/tweets には、公式ドキュメントに載っている edit_options というパラメータがあります。
Options for editing an existing Post. When provided, this request will edit the specified Post instead of creating a new one.
中身は previous_post_id(編集対象のポスト ID)だけ。つまり 同じ POST /2/tweets エンドポイントに edit_options.previous_post_id を付けて投げると、新規作成ではなく「既存ポストの編集」になる、という仕様です。X の画面でやっている「投稿を編集」を、そのまま API から叩けます(要 X Premium)。
これが使えるなら、フローはこうなります。
- STEP1:ハッシュタグ無しの本文で作成(=前回の 403 回避策のまま)→ 201・ポスト ID を取得
- STEP2:数秒待って、
edit_options.previous_post_idに STEP1 の ID を指定し、ハッシュタグ付きの本文で編集 → 201
スパム判定は「作成時のハッシュタグ」に反応していたので、作成をハッシュタグ無しで通し、後から編集で足せば 403 を踏まずにハッシュタグを載せられる、という理屈です。
検証:本当に通るのか
まず本番コードとは切り離した検証スクリプトで、テキストのみで試しました。前回と同じ .env・同じ OAuth1.0a です。
STEP1(ハッシュタグ無しで作成) → 201:
{
"data": {
"id": "20750xxxxxxxxxxxxx3356444",
"text": "【テスト投稿】edit_options検証用です。",
"edit_history_tweet_ids": ["20750xxxxxxxxxxxxx3356444"]
}
}
STEP2(edit_options で編集) → 201:
{
"data": {
"id": "20750xxxxxxxxxxxxx7709894070",
"text": "【テスト投稿】edit_options検証用です。\n\n#測地系 #座標変換",
"edit_history_tweet_ids": [
"20750xxxxxxxxxxxxx3356444",
"20750xxxxxxxxxxxxx7709894070"
]
}
}
ポイントは edit_history_tweet_ids が 2件になっている こと。これは「新しい投稿を作った」のではなく「既存投稿を編集して版が1つ増えた」ことを表します。本文にもハッシュタグがちゃんと入り、403 は出ませんでした。狙いどおりです。
送るペイロードはこれだけです。
payload = {
"text": "本文\n\n#測地系 #座標変換", # ハッシュタグ入りの完成形
"edit_options": {"previous_post_id": tweet_id}, # STEP1 で得た ID
}
requests.post("https://api.twitter.com/2/tweets", auth=auth, json=payload)
落とし穴:編集はメディア(画像)を引き継がない
ここで一度ハマりました。実運用の投稿は画像付きです。画像付きで STEP1 を投げ、STEP2 で テキストだけ(=ハッシュタグ入り本文だけ)を編集で送ったところ、編集自体は 201 で成功するのに、編集後のポストから画像が消えていました。
編集後のポストを GET /2/tweets/:id?expansions=attachments.media_keys で確認すると includes.media が空。作成時に本文末へ自動付与されていた画像の t.co リンクも、編集後の本文からは消えていました。
原因はシンプルで、edit_options の編集は「送った本文で丸ごと差し替える」挙動で、メディアは自動では引き継がれないからです。編集ペイロードに画像を含めなければ、画像無しのポストに上書きされてしまう。
対策も明快で、編集時に作成時と同じ media_ids を再送するだけです。
payload = {
"text": full_text_with_hashtags,
"edit_options": {"previous_post_id": tweet_id},
"media": {"media_ids": media_ids}, # ★作成時と同じ media_id を再送
}
これで再検証したところ、編集後のポストにも画像がちゃんと残りました。
{
"data": {
"attachments": { "media_keys": ["3_20750xxxxxxxxxxxxx3047468032"] },
"text": "本文\n\n#測地系 #座標変換 https://t.co/xxxxxxxx"
},
"includes": {
"media": [
{ "media_key": "3_20750xxxxxxxxxxxxx3047468032", "type": "photo",
"url": "https://pbs.twimg.com/media/xxxxxxxx.jpg" }
]
}
}
attachments.media_keys と includes.media に画像が入りました。しかも 同じ media_id を再利用できる(画像を再アップロードし直す必要はない)ので、余計なアップロード課金も発生しません。
実装:投稿関数の直後に編集を1回足す
自動化そのものは小さな関数1つで済みます。作成に成功したら、除去したハッシュタグがある場合だけ編集を1回投げます。
import time
X_EDIT_DELAY = 3 # 作成→編集の待機秒(反映遅延を考慮。実測3秒で201)
def x_edit_add_hashtags(full_text, tweet_id, media_ids, auth):
"""ハッシュタグ無しで作成した投稿を edit_options で編集し、
ハッシュタグ付き本文へ差し替える。画像は media_ids を再送して保持する。"""
time.sleep(X_EDIT_DELAY)
payload = {"text": full_text, "edit_options": {"previous_post_id": tweet_id}}
if media_ids:
payload["media"] = {"media_ids": media_ids} # ★画像を保持
r = requests.post("https://api.twitter.com/2/tweets", auth=auth, json=payload, timeout=60)
if r.status_code >= 300:
# 編集失敗は投稿自体の成功を無効化しない(ハッシュタグ無しのまま残るだけ)
print(f"[X][WARN] ハッシュタグ編集に失敗 {r.status_code}: {r.text}")
return
print("[X] ハッシュタグを編集追加 OK")
full_text は前回の limit_hashtags で外す前の「ハッシュタグ入り完成形」。既存の投稿処理で 201 を受け取った直後にこの関数を呼ぶだけで、「ハッシュタグ無しで作成 → 数秒後に編集で追加」が全自動になります。編集が失敗しても、作成自体は成功しているのでハッシュタグ無しのポストは残る(実害はハッシュタグが載らないだけ)——という安全側のフォールバックにしています。
実際に1日分(4投稿×画像付き)を本番投稿したところ、4件すべてで「作成 → 編集追加」が通り、画像もハッシュタグも揃った状態で公開できました。手作業はゼロになりました。
注意点
- X Premium 必須。編集機能は Premium 限定なので、非 Premium アカウントでは STEP2 が失敗します。
- 編集するとポスト ID が変わる(版が増える)。URL 共有やログ記録は、編集前後どちらの ID を正とするか決めておくと混乱しません。編集前の ID でも投稿ページには到達できます。
- 待機時間。作成直後は反映が追いつかないことがあるため、数秒(実測は3秒で十分)待ってから編集します。
- スパム判定は「作成時」に効く。ハッシュタグを載せるのが「編集」なら 403 を踏まない、というのが今回の肝です。とはいえ挙動が将来変わる可能性はあるので、編集失敗時に投稿を巻き戻さないフォールバックは入れておくのが無難です。
まとめ
- 前回の 403 対策(X ではハッシュタグを外す)で残っていた「後からハッシュタグを手で足す」手作業は、
POST /2/tweetsのedit_options.previous_post_idで自動化できる。 - フローは「ハッシュタグ無しで作成(201)→ 数秒待って編集でハッシュタグ追加(201)」。スパム判定は作成時に効くので、編集で足せば 403 を踏まない。
- 編集はメディアを引き継がない。画像付き投稿では、編集ペイロードに作成時と同じ
media_idsを再送して画像を保持する(再アップロード不要)。 edit_history_tweet_idsが増えていれば「新規作成ではなく編集」できている証拠。- ただし X Premium が前提で、編集するとポスト ID が変わる点には注意。
前回の記事で「ハッシュタグを諦める」ところまで書きましたが、edit_options を使えば ハッシュタグも画像も載せたまま、403 も踏まず、手作業もゼロにできます。同じく X の自動投稿でハッシュタグに悩んでいる方の参考になれば幸いです。
なお、この一連の検証は座標変換アプリ GeoConverterPro の SNS 自動投稿化の副産物です。iPhone だけで JGD2024〜東京測地系、平面直角座標、CSV 一括変換までオフラインで完結するアプリなので、測量・GIS 関係の方はよければ覗いてみてください。
開発者より: アプリ・Kindle本・オープンソースの一覧は GitHub: amru195704 にまとめています。
お願い
本記事の情報は参考目的で掲載しており、正確性・完全性を保証するものではありません。誤記・不正確な情報がございましたら、コメント欄よりご指摘いただければ、確認のうえ修正いたします。
アプリを入手(App Store):GeoConverterPro(座標変換) | GeoPrism JP(測地系の可視化・学習)
コメントを残す