I'm using a free-tier Twitter / X developer account.
Admittedly, the OAuth stuff confuses me. I have the following key-values in my .env (values removed), not knowing which are actually necessary and which aren't.
API_KEY=
API_SECRET=
ACCESS_TOKEN=
ACCESS_TOKEN_SECRET=
CLIENT_ID=
CLIENT_SECRET=
BEARER_TOKEN=
The following is a minimal and (semi-)complete example Twitter / X Flask client. It uses Tweepy and OAuth2.0 to authorize with Twitter / X via an auth url and callback flow, gets the authorized user's info, and gets the auth'd users last 10 tweets. These pieces work as expected.
The issue is when I try to use the delete flow (clicking the "Delete" button on the example), I get a 401 Unauthorized error.
How do I correct this so that I can delete my tweets?
from flask import Flask, jsonify, redirect, request, session, render_template_string
import tweepy
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey")
CLIENT_ID = os.getenv("CLIENT_ID")
CLIENT_SECRET = os.getenv("CLIENT_SECRET")
CALLBACK_URL = "http://127.0.0.1:5000/callback"
oauth2_handler = tweepy.OAuth2UserHandler(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
redirect_uri=CALLBACK_URL,
scope=["tweet.read", "tweet.write", "users.read", "offline.access"],
)
@app.route("/", methods=["GET"])
def auth_redirect():
try:
auth_url = oauth2_handler.get_authorization_url()
return redirect(auth_url)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/callback", methods=["GET"])
def callback():
try:
code = request.args.get("code")
if not code:
return jsonify({"error": "Missing authorization code"}), 400
access_token = oauth2_handler.fetch_token(request.url)
session["access_token"] = access_token
client = tweepy.Client(access_token["access_token"], wait_on_rate_limit=True)
user = client.get_me(user_auth=False)
tweets = client.get_users_tweets(id=user.data.id, max_results=10)
tweet_list = (
"".join(
[
f"""
<li>{tweet.text}
<form action="/delete_tweet/{tweet.id}" method="POST">
<button type="submit">Delete</button>
</form>
</li>
"""
for tweet in (tweets.data or [])
]
)
or "<p>No tweets found.</p>"
)
return render_template_string(
f"""
<h1>Authorization Successful</h1>
<h2>User Information</h2>
<ul>
<li><strong>ID:</strong> {user.data.id}</li>
<li><strong>Username:</strong> {user.data.username}</li>
<li><strong>Name:</strong> {user.data.name}</li>
</ul>
<h2>Last 10 Tweets</h2>
<ul>{tweet_list}</ul>
"""
)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/delete_tweet/<string:tweet_id>", methods=["POST"])
def delete_tweet(tweet_id):
try:
access_token = session.get("access_token")
if not access_token:
return jsonify({"error": "Access token not found"}), 400
client = tweepy.Client(
consumer_key=CLIENT_ID,
consumer_secret=CLIENT_SECRET,
access_token=access_token["access_token"],
wait_on_rate_limit=True,
)
response = client.delete_tweet(tweet_id)
if response.data:
return jsonify({"message": f"Tweet {tweet_id} deleted successfully"})
return jsonify({"error": "Failed to delete tweet"}), 500
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == "__main__":
app.run(debug=True)
This was a simple fix. As I noted in my question, Twitter's OAuth paths confuse me, especially which version (OAuth1.0a and OAuth2.0) is allowed / prohibited to be used with which version of the API (v1.1 vs v2.0).
Given this, I don't actually know why this fix works. If someone could add those details, including when the particulars of which keys / secrets / tokens and user_auth param should be used, that'd be helpful.
The fix was two-fold:
tweepy.Client
initialization, replace the keys / secrets / tokens with the singular bearer_token
anddelete_tweet
should include user_auth=False
.This makes the method
@app.route("/delete_tweet/<string:tweet_id>", methods=["POST"])
def delete_tweet(tweet_id):
try:
access_token = session.get("access_token")
if not access_token:
return jsonify({"error": "Access token not found"}), 400
client = tweepy.Client(
bearer_token=access_token["access_token"],
wait_on_rate_limit=True,
)
response = client.delete_tweet(tweet_id, user_auth=False)
if response.data:
return jsonify({"message": f"Tweet {tweet_id} deleted successfully"})
return jsonify({"error": "Failed to delete tweet"}), 500
except Exception as e:
return jsonify({"error": str(e)}), 500