Files

318 lines
12 KiB
Bash

#!/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", "<br>\n")
if sig_html:
html_content = f"<p>{html_body}</p><p><em>{closing}</em></p>{sig_html}"
else:
html_content = f"<p>{html_body}</p><p><em>{closing}</em></p><p>--<br>Sho'Nuff Brown<br>{title}<br>IT Pro Partner</p>"
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