318 lines
12 KiB
Bash
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
|