My Bluesky Mirror Explained
Introduction
Couple hours ago as of writing this, I released the first version of my Bluesky Mirror template and I wanted to explain how it works. Anyway, check out the Fabrizio Romano mirror and the Atlanta United mirror.
Code Review
NOTE: The Fabrizio Romano mirror does not use this code, it works completely different because I was stupid when it first came out. So it really only applies to the Atlanta United mirror and that too has been heavily modified to fit EXACTLY what the mirror needs.
setup.py
The user will start off with a nice user-friendly prompt just asking for a Twitter account login (dont worry I didn’t put anything that will steal it), API key, Bluesky user and password, twitter user they want to mirror and how often it wants to check for a new post (in seconds).
twitter_username = prompt_user_for_input("Enter your Twitter account username: ")
twitter_password = prompt_user_for_input("Enter your Twitter account password: ")
rapidapi_key = prompt_user_for_input("Enter your RAPIDAPI key: ")
bluesky_username = prompt_user_for_input("Enter your Bluesky handle/username (ie: user.bsky.social): ")
bluesky_password = prompt_user_for_input("Enter your Bluesky app-password: ")
target_user = prompt_user_for_input("Enter the target Twitter user (without the @, ie: 'Yopro20_): ")
check_interval = prompt_for_integer("Enter the interval (in seconds) to check for new posts: ")
Once you finish entering everything, you’ll see that a .env
file is created so that main.py
will utilize those presets you entered. Pretty simple.
main.py
Now the secret sauce. Also most of this was with Cursor and at night or in the morning so it isn’t that great. Now, lemme show you the actually important parts.
What we need to first do is actually log into Bluesky and Twitter.
def init_bluesky_client() -> Client:
client = Client()
bluesky_username = os.getenv("BLUESKY_USERNAME")
bluesky_password = os.getenv("BLUESKY_PASSWORD")
client.login(bluesky_username, bluesky_password)
return client
and
await app.sign_in(username, password)
You can probably guess which is Twitter and Bluesky even if you don’t know python. Both sign in, tweety is the more complicated (on the backend but good thing they handle it amazingly).
Moving on the tour, we move on with actually checking for the latest post!
last_tweet_id = None
# some code later
print("[PROCESS] Checking for new tweets...")
logging.info("Checking for new tweets...")
user = await app.get_user_info(target_username)
# some code later
if all_tweets:
latest_tweet = all_tweets[0]
tweet_id = latest_tweet.id
print(f"[INFO] Latest Tweet ID: {tweet_id}")
logging.info("Latest Tweet ID: %s", tweet_id)
if tweet_id != last_tweet_id:
last_tweet_id = tweet_id
print(f"[SUCCESS] New Tweet ID: {tweet_id}")
logging.info("New Tweet ID: %s", tweet_id)
Now I did cut out some snippets only to make this shorter and easier to read. Now most of this is readable and all_tweets
is just tweety saying “here is all of their tweets”, the [0]
, made it only scrap the latest post. Also the tweet id (like 1886784294204379549) is important for later. Next stop on the tour, getting the media and caption!
url = "https://twitter-video-and-image-downloader.p.rapidapi.com/twitter"
querystring = {"url": f"https://x.com/{target_username}/status/{tweet_id}"}
headers = {
"x-rapidapi-key": api_key,
"x-rapidapi-host": "twitter-video-and-image-downloader.p.rapidapi.com"
}
# some code later
response = requests.get(url, headers=headers, params=querystring)
data = response.json()
# Print the tweet message
if data.get("success"):
tweet_text = data.get("text", "No text available")
# some code later
# Download media files
images = []
videos = []
if "media" in data:
for index, media in enumerate(data["media"]):
media_url = media["url"]
media_type = media.get("type", "image")
print(f"[PROCESS] Downloading media from {media_url}...")
logging.info("Downloading media from %s...", media_url)
media_response = requests.get(media_url)
# some code later
if media_type == "video":
video_path = f"video{index}.mp4"
with open(video_path, "wb") as file:
file.write(media_response.content)
videos.append(video_path)
print(f"[SUCCESS] Downloaded {media_url} as {video_path}")
logging.info("Downloaded %s as %s", media_url, video_path)
A lot to take in but all it does is:
- Make a request (ask) the API for info on the tweet, let’s say this tweet, and it will respond with:
{"success":true,"version":"6","type":"tweet","text":"Working up to full πππΏπ²π»π΄ππ΅ πͺ","id":"1887664826236768353","url":"https://twitter.com/ATLUTD/status/1887664826236768353","media":[{"type":"video","url":"https://video.twimg.com/amplify_video/1887631691365093376/vid/avc1/1080x1080/QhE22KiF3PQBSEmI.mp4?tag=16","thumbnail":"https://pbs.twimg.com/media/GjI3MCuW0AEERbk.jpg","width":1080,"height":1080}],"parser":1}
The only important part is text
and url
for the caption and media url so we can download it. The code saves the caption for later and then downloads the media urls as a file (like image0.jpg
or video0.mp4
), pretty straight forward. Now for the fun part, actually posting to Bluesky.
Thanks atproto.blue.
response = bluesky_client.send_images(
text=post_text,
images=image_data,
image_alts=image_alts
)
print(f"[SUCCESS] Posted images to BlueSky. Response: {response}")
logging.info("Posted images to BlueSky. Response: %s", response)
Pretty simple, stripped it down to only the actual posting part, if you read the code, you would see there is also a video part, though videos aren’t as fun to handle as you think. But what if there is no media to a post?
response = bluesky_client.send_post(text=post_text)
Love it.
Links
Github Repo
Main Bluesky pagel
Fabrizio Romano mirror
Atlanta United mirror
Support me/Buy Me A Coffee