#!/bin/bash # shark-draft-reminder.sh — Send 24h and 1h draft reminder emails to league members # Runs every 15 minutes via cron set -euo pipefail exec python3 - "$@" << 'PYTHON_SCRIPT' #!/usr/bin/env python3 """ Shark Draft Reminder — sends 24h and 1h email reminders to league members before their draft starts. Runs every 15 minutes via cron; idempotent. DB: leagues table with draft_start_at (ISO 8601), status='draft' Tracking: /root/shark-game/data/draft-reminders.json """ import json import logging import os import random import smtplib import sqlite3 import sys from datetime import datetime, timedelta, timezone from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from importlib.util import spec_from_file_location, module_from_spec # ── Paths ────────────────────────────────────────────────────────────────── DB_PATH = "/root/shark-game/backend/game.db" TRACKING_FILE = "/root/shark-game/data/draft-reminders.json" LOG_FILE = "/var/log/shark-reminder.log" PASS_FILE = "/root/.config/himalaya/shonuff.pass" TITLES_FILE = "/root/.hermes/references/shonuff-titles.py" CLOSINGS_FILE = "/root/.hermes/references/shonuff-closings.py" SIG_IMAGE_FILE = "/root/.hermes/references/shonuff-image-b64.txt" SIG_HTML_FILE = "/root/.hermes/references/shonuff-email-signature.html" # ── SMTP Config ──────────────────────────────────────────────────────────── SMTP_HOST = "mail.germainebrown.com" SMTP_PORT = 2525 SMTP_USER = "shonuff@germainebrown.com" SMTP_FROM = "shonuff@germainebrown.com" SMTP_FROM_NAME = "Sho'Nuff Brown" BCC_ADDR = "g@germainebrown.com" # ── Base URL for draft room links ───────────────────────────────────────── BASE_URL = "https://shark.itpropartner.com" # ── Time windows (±5 minutes) ────────────────────────────────────────────── WINDOW_SECONDS = 5 * 60 # ±5 minutes # ── Logging ──────────────────────────────────────────────────────────────── logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler(LOG_FILE), logging.StreamHandler(sys.stdout), ], ) log = logging.getLogger("shark-draft-reminder") # ── Helpers ──────────────────────────────────────────────────────────────── def load_password(): with open(PASS_FILE, "r") as f: return f.read().strip() def load_closings(): spec = spec_from_file_location("closings", CLOSINGS_FILE) mod = module_from_spec(spec) spec.loader.exec_module(mod) return random.choice(mod.SHONUFF_CLOSINGS) def load_titles(): spec = spec_from_file_location("titles", TITLES_FILE) mod = module_from_spec(spec) spec.loader.exec_module(mod) return random.choice(mod.SHONUFF_TITLES) def load_signature_html(title): try: with open(SIG_IMAGE_FILE) as f: b64 = f.read().strip() with open(SIG_HTML_FILE) as f: sig = f.read() return sig.replace("%SHONUFF_IMAGE%", b64).replace("%SHONUFF_TITLE%", title) except FileNotFoundError: log.warning("Signature image/html files not found — using plain sig") return "" def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") return conn def load_tracking(): if not os.path.exists(TRACKING_FILE): return {} with open(TRACKING_FILE, "r") as f: try: return json.load(f) except json.JSONDecodeError: log.warning("Corrupt tracking file, resetting") return {} def save_tracking(tracking): os.makedirs(os.path.dirname(TRACKING_FILE), exist_ok=True) with open(TRACKING_FILE, "w") as f: json.dump(tracking, f, indent=2, sort_keys=True) def tracking_key(league_id, user_id, reminder_type): return f"league_{league_id}_user_{user_id}_{reminder_type}" def already_sent(tracking, league_id, user_id, reminder_type): return tracking_key(league_id, user_id, reminder_type) in tracking def mark_sent(tracking, league_id, user_id, reminder_type): tracking[tracking_key(league_id, user_id, reminder_type)] = datetime.now(timezone.utc).isoformat() def get_user_regions_preview(conn, league_id, user_id): rows = conn.execute( """SELECT r.emoji, r.name FROM draft_picks dp JOIN regions r ON r.id = dp.region_id WHERE dp.league_id = ? AND dp.user_id = ? ORDER BY dp.round, dp.pick_number""", (league_id, user_id), ).fetchall() if not rows: return " • No picks yet — get ready to draft!" return "\n".join(f" • {r['emoji']} {r['name']}" for r in rows) def build_email_body_24h(league_name, display_name, league_id, regions_preview, draft_start_at_str): draft_url = f"{BASE_URL}/draft-room.html?league_id={league_id}" dt = datetime.fromisoformat(draft_start_at_str) draft_time_str = dt.strftime("%A, %B %d at %I:%M %p %Z") return f"""Howdy {display_name}, 🦈 SHARK ALERT — Your draft for {league_name} starts in 24 hours! ⏰ Draft Time: {draft_time_str} Get ready to claim your regions and build the ultimate shark attack fantasy team. Here's your draft room link (bookmark it!): 🔗 {draft_url} 📋 Your Current Regions: {regions_preview} Make sure you're logged in and ready to go when the draft starts. Each pick is timed, so don't be late — or the sharks will swim right past you! Good luck, and may the biggest bites be yours.""" def build_email_body_1h(league_name, display_name, league_id, draft_start_at_str): draft_url = f"{BASE_URL}/draft-room.html?league_id={league_id}" dt = datetime.fromisoformat(draft_start_at_str) draft_time_str = dt.strftime("%I:%M %p %Z") return f"""Yo {display_name}, 🔥 The draft is T-1 HOUR away! {league_name} starts in just 60 minutes! ⏰ Draft Time: {draft_time_str} Head to the draft room NOW and make sure everything is ready: 🔗 {draft_url} 📌 Pro tips: • Make sure your browser notifications are enabled • Have your region research ready • Don't step away — your picks are on a timer! This is it — time to make your picks and show everyone who the real apex predator is. Let's go!""" def send_email(to_addr, subject, body, closing, title): sig_html = load_signature_html(title) msg = MIMEMultipart("alternative") msg["From"] = f"{SMTP_FROM_NAME} <{SMTP_FROM}>" msg["To"] = to_addr msg["Bcc"] = BCC_ADDR msg["Subject"] = subject plain_text = f"{body}\n\n{closing}\n\n-- \nSho'Nuff Brown\n{title}\nIT Pro Partner\n{SMTP_FROM}" msg.attach(MIMEText(plain_text, "plain")) html_body = body.replace("\n", "
\n") if sig_html: html_content = f"

{html_body}

{closing}

{sig_html}" else: html_content = f"

{html_body}

{closing}

--
Sho'Nuff Brown
{title}
IT Pro Partner

" msg.attach(MIMEText(html_content, "html")) password = load_password() try: with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as s: s.starttls() s.login(SMTP_USER, password) s.send_message(msg) log.info("Email sent to %s — subject: %s", to_addr, subject) return True except smtplib.SMTPException as e: log.error("SMTP error sending to %s: %s", to_addr, e) return False except Exception as e: log.error("Unexpected error sending to %s: %s", to_addr, e) return False def process_draft_reminders(): now = datetime.now(timezone.utc) tracking = load_tracking() conn = get_db() try: leagues = conn.execute( "SELECT id, name, draft_start_at, status FROM leagues WHERE status = 'draft' AND draft_start_at IS NOT NULL" ).fetchall() if not leagues: log.info("No leagues with upcoming drafts found") return log.info("Found %d league(s) with scheduled drafts", len(leagues)) for league in leagues: league_id = league["id"] league_name = league["name"] draft_start_str = league["draft_start_at"] try: draft_start = datetime.fromisoformat(draft_start_str) if draft_start.tzinfo is None: draft_start = draft_start.replace(tzinfo=timezone.utc) except (ValueError, TypeError) as e: log.warning("Invalid draft_start_at for league %d (%s): %s", league_id, league_name, e) continue time_until = (draft_start - now).total_seconds() members = conn.execute( """SELECT u.id, u.email, u.display_name FROM league_members lm JOIN users u ON u.id = lm.user_id WHERE lm.league_id = ? ORDER BY u.display_name""", (league_id,), ).fetchall() if not members: log.info("No members in league %d (%s)", league_id, league_name) continue for member in members: user_id = member["id"] email = member["email"] display_name = member["display_name"] if abs(time_until - 24 * 3600) <= WINDOW_SECONDS: if already_sent(tracking, league_id, user_id, "24h"): log.debug("24h reminder already sent for %s in league %d", display_name, league_id) else: regions = get_user_regions_preview(conn, league_id, user_id) body = build_email_body_24h(league_name, display_name, league_id, regions, draft_start_str) closing = load_closings() title = load_titles() subject = f"🦈 [{league_name}] — Draft Starts in 24 Hours!" if send_email(email, subject, body, closing, title): mark_sent(tracking, league_id, user_id, "24h") save_tracking(tracking) log.info("Sent 24h reminder to %s (%s) for league '%s'", display_name, email, league_name) elif abs(time_until - 3600) <= WINDOW_SECONDS: if already_sent(tracking, league_id, user_id, "1h"): log.debug("1h reminder already sent for %s in league %d", display_name, league_id) else: body = build_email_body_1h(league_name, display_name, league_id, draft_start_str) closing = load_closings() title = load_titles() subject = f"🦈 [{league_name}] — Draft Starts in 1 Hour!" if send_email(email, subject, body, closing, title): mark_sent(tracking, league_id, user_id, "1h") save_tracking(tracking) log.info("Sent 1h reminder to %s (%s) for league '%s'", display_name, email, league_name) finally: conn.close() if __name__ == "__main__": log.info("=== Shark Draft Reminder run started ===") try: process_draft_reminders() except Exception as e: log.exception("Unhandled error: %s", e) sys.exit(1) log.info("=== Shark Draft Reminder run complete ===") PYTHON_SCRIPT