#!/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. """ import json import logging import os import random import smtplib import sqlite3 import ssl import sys import time 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(): """Read SMTP password from file.""" with open(PASS_FILE, "r") as f: return f.read().strip() def load_closings(): """Load random closing line from reference file.""" 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(): """Load random title from reference file.""" 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): """Build Sho'Nuff's HTML signature block.""" 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(): """Get SQLite connection.""" conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") return conn def load_tracking(): """Load sent-reminder tracking from JSON file.""" 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): """Persist sent-reminder tracking to JSON file.""" 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): """Generate a unique key for tracking sent reminders.""" return f"league_{league_id}_user_{user_id}_{reminder_type}" def already_sent(tracking, league_id, user_id, reminder_type): """Check if a reminder has already been sent.""" key = tracking_key(league_id, user_id, reminder_type) return key in tracking def mark_sent(tracking, league_id, user_id, reminder_type): """Record that a reminder was sent (with ISO timestamp).""" key = tracking_key(league_id, user_id, reminder_type) tracking[key] = datetime.now(timezone.utc).isoformat() def get_user_regions_preview(conn, league_id, user_id): """Get user's drafted regions for the 'your regions preview' in 24h email.""" 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): """Build the 24-hour reminder email body.""" 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") body = 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.""" return body def build_email_body_1h(league_name, display_name, league_id, draft_start_at_str): """Build the 1-hour reminder email body.""" 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") body = 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!""" return body def send_email(to_addr, subject, body, closing, title): """Send an email via SMTP with Sho'Nuff's signature.""" 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 part 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 part 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(): """Main logic: find leagues with upcoming drafts and send reminders.""" now = datetime.now(timezone.utc) tracking = load_tracking() conn = get_db() try: # Find leagues that are in draft status and have draft_start_at set 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 # Calculate time until draft time_until = (draft_start - now).total_seconds() # Get league members 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"] # Check 24-hour reminder window 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) # Check 1-hour reminder window 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() # ── Entry Point ───────────────────────────────────────────────────────────── 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