Skip to content

Instantly share code, notes, and snippets.

@sogful
Last active July 23, 2026 18:52
Show Gist options
  • Select an option

  • Save sogful/8e343ce04da6895ed432e27a21776e91 to your computer and use it in GitHub Desktop.

Select an option

Save sogful/8e343ce04da6895ed432e27a21776e91 to your computer and use it in GitHub Desktop.
a little program i use to automate twitter banner updates (outdated, see https://github.com/sogful/banner)
import requests
from PIL import Image, ImageDraw, ImageFont
import tweepy
from datetime import datetime, timezone, timedelta
import time
import logging
import sys
import os
import threading
import subprocess
from pystray import Icon, Menu, MenuItem
#*//////////////////////////////////////////////////////////////////////*#
BANNER_SIZE = (900, 300)
def base_dir():
if getattr(sys, "frozen", False):
return os.path.dirname(sys.executable)
return os.path.dirname(os.path.abspath(__file__))
#*//////////////////////////////////////////////////////////////////////*#
def get_weather_summary(lat, lon):
url = (
f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}"
f"&current_weather=true"
)
r = requests.get(url, timeout=10)
r.raise_for_status()
data = r.json()
cw = data.get('current_weather', {})
temp = cw.get('temperature', 'N/A')
wind = cw.get('windspeed', 'N/A')
summary = f"{temp}°C, wind speed: {wind}km/hr"
return summary
def get_status_summary(user):
url = f"https://status.cafe/users/{user}/status.json"
r = requests.get(url, timeout=10)
r.raise_for_status()
data = r.json()
content = data.get("content", "N/A")
time_ago = data.get("timeAgo", "")
small = f"{time_ago} (from status.cafe/meow)"
return content, small
def draw_weather_on_banner(weather_text, status_main, status_small, input_image_path, output_image_path):
im = Image.open(input_image_path).convert("RGBA")
draw = ImageDraw.Draw(im)
fontsize = 25
font_paths = [ # once again, change this (also, the third path is for pythonanywhere, you can use it for deviceless daily updates)
"H:/Users/Admin/Downloads/ruji.ttf",
"ruji.ttf",
"/home/soggycat/banner/ruji.ttf",
]
font = None
for font_path in font_paths:
try:
font = ImageFont.truetype(font_path, fontsize)
break
except (OSError, IOError):
continue
if font is None:
system_fonts = [
"C:/Windows/Fonts/arial.ttf",
"C:/Windows/Fonts/calibri.ttf",
"C:/Windows/Fonts/tahoma.ttf"
]
for sys_font in system_fonts:
try:
font = ImageFont.truetype(sys_font, fontsize)
break
except (OSError, IOError):
continue
if font is None:
font = ImageFont.load_default()
timestamp_fontsize = fontsize // 2
timestamp_font = None
for font_path in font_paths:
try:
timestamp_font = ImageFont.truetype(font_path, timestamp_fontsize)
break
except (OSError, IOError):
continue
if timestamp_font is None:
for sys_font in system_fonts:
try:
timestamp_font = ImageFont.truetype(sys_font, timestamp_fontsize)
break
except (OSError, IOError):
continue
if timestamp_font is None:
timestamp_font = ImageFont.load_default()
gmt2 = timezone(timedelta(hours=2)) # change this to your timezone!!!!
now = datetime.now(gmt2)
time_str = now.strftime("%H:%M")
timestamp_text = f"last updated {time_str} GMT+2"
def measure(text, fnt):
try:
b = draw.textbbox((0, 0), text, font=fnt)
return b[2]-b[0], b[3]-b[1]
except ValueError:
return draw.textsize(text, font=fnt)
padding = 60
bg_padding = 10
corner_radius = 15
gap = 5 # spacing between main and small text
boxes = [
(weather_text, timestamp_text, "left"),
(status_main, status_small, "right"),
]
layouts = []
for main_text, small_text, side in boxes:
main_w, main_h = measure(main_text, font)
small_w, small_h = measure(small_text, timestamp_font)
box_w = max(main_w, small_w)
box_h = main_h + small_h + gap
y = padding
if side == "left":
x = padding
main_x = small_x = x # left align
else:
x = im.size[0] - padding - box_w
main_x = x + box_w - main_w # right align
small_x = x + box_w - small_w
box = [x-bg_padding, y-bg_padding, x+box_w+bg_padding, y+box_h+bg_padding]
layouts.append((box, main_x, y, main_text, small_x, y + main_h + gap, small_text))
overlay = Image.new("RGBA", im.size, (0, 0, 0, 0))
overlay_draw = ImageDraw.Draw(overlay)
for box, mx, my, main_text, sx, sy, small_text in layouts:
overlay_draw.rounded_rectangle(box, radius=corner_radius, fill=(0, 0, 0, 80))
im = Image.alpha_composite(im, overlay)
draw = ImageDraw.Draw(im)
for box, mx, my, main_text, sx, sy, small_text in layouts:
draw.text((mx, my), main_text, font=font, fill=(255,255,255,255))
draw.text((sx, sy), small_text, font=timestamp_font, fill=(255,255,255,200))
# scale down to the exact size twitter needs to keep the alpha channel
# so the fabled 900x300
if im.size != BANNER_SIZE:
im = im.resize(BANNER_SIZE, Image.LANCZOS)
im.save(output_image_path, format="PNG", optimize=False)
#*//////////////////////////////////////////////////////////////////////*#
def update_twitter_banner(banner_image_path):
consumer_key = "changethis"
consumer_secret = "andthis"
access_token = "andthis"
access_token_secret = "andthis"
auth = tweepy.OAuth1UserHandler(
consumer_key, consumer_secret, access_token, access_token_secret
)
api = tweepy.API(auth)
api.update_profile_banner(filename=banner_image_path)
def runupd():
try:
lat, lon = 6.7, 6.7 # change this location to yours
weather_text = get_weather_summary(lat, lon)
logging.info(f"weather: {weather_text}")
status_main, status_small = get_status_summary("user") # and change this to your status.cafe username. if you don't have one just remove the relevant code
logging.info(f"status: {status_main} ({status_small})")
script_dir = base_dir()
input_image = os.path.join(script_dir, "base.png")
output_image = os.path.join(script_dir, "currentbanner.png")
draw_weather_on_banner(weather_text, status_main, status_small, input_image, output_image)
logging.info("updating banner..")
update_twitter_banner(output_image)
return True
except Exception as e:
logging.error(f"{e}", exc_info=True)
return False
#*//////////////////////////////////////////////////////////////////////*#
def make_tray(script_dir, stop_event, update_event):
# builds the buggy tray
def on_update_now(icon, item):
logging.info("tray: update now")
update_event.set()
def on_exit(icon, item):
logging.info("tray: exit")
stop_event.set()
update_event.set()
icon.stop()
def on_recompile(icon, item):
logging.info("tray: recompile and reopen")
recompile = os.path.join(script_dir, ".claude", "recompile.ps1")
DETACHED = 0x00000008
subprocess.Popen(
["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass",
"-WindowStyle", "Hidden", "-File", recompile],
creationflags=DETACHED | subprocess.CREATE_NEW_PROCESS_GROUP,
close_fds=True,
)
on_exit(icon, item)
try:
tray_image = Image.open(os.path.join(script_dir, "icon.png"))
except (OSError, IOError):
tray_image = Image.new("RGBA", (64, 64), (0, 0, 0, 255))
menu = Menu(
MenuItem("recompile and reopen", on_recompile),
MenuItem("update now", on_update_now),
MenuItem("exit", on_exit),
)
return Icon("twitterbanner", tray_image, "twitter banner updater", menu)
def main():
script_dir = base_dir()
log_file = os.path.join(script_dir, "banner.log")
handlers = [logging.FileHandler(log_file)]
if sys.stdout is not None:
handlers.append(logging.StreamHandler(sys.stdout))
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=handlers
)
stop_event = threading.Event()
update_event = threading.Event()
def worker():
runupd()
while not stop_event.is_set():
logging.info("done, will try again in 1 hour..")
update_event.wait(timeout=3600) # wakes up early on manual update / exit
if stop_event.is_set():
break
update_event.clear()
runupd()
threading.Thread(target=worker, daemon=True).start()
make_tray(script_dir, stop_event, update_event).run()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment