Initial skills documentation — 25 categories, all SKILL.md + references + scripts
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user