File size: 3,861 Bytes
4a182f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# app/notify.py
from __future__ import annotations
import os, re, hashlib, time, requests, smtplib, ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Optional

def _env(name: str, default: Optional[str] = None) -> Optional[str]:
    v = os.getenv(name)
    return v if v is not None and v != "" else default

# ---------- Slack ----------
def send_slack(message: str, webhook_url: Optional[str] = None) -> bool:
    url = webhook_url or _env("SLACK_WEBHOOK_URL")
    if not url:
        return False
    try:
        r = requests.post(url, json={"text": message}, timeout=10)
        return r.status_code // 100 == 2
    except Exception:
        return False

# ---------- Email (SMTP) ----------
def send_email(subject: str, text: str, html: Optional[str] = None,
               to: Optional[list[str]] = None) -> bool:
    host = _env("SMTP_HOST")
    user = _env("SMTP_USER")
    pwd  = _env("SMTP_PASS")
    port = int(_env("SMTP_PORT", "587"))
    sender = _env("SMTP_FROM", user)
    recipients = to or (_env("NOTIFY_TO","") or "").replace(";", ",").split(",")
    recipients = [x.strip() for x in recipients if x.strip()]
    if not (host and user and pwd and sender and recipients):
        return False

    msg = MIMEMultipart("alternative")
    msg["Subject"] = subject
    msg["From"] = sender
    msg["To"] = ", ".join(recipients)
    msg.attach(MIMEText(text, "plain", "utf-8"))
    if html:
        msg.attach(MIMEText(html, "html", "utf-8"))

    try:
        ctx = ssl.create_default_context()
        with smtplib.SMTP(host, port, timeout=15) as s:
            s.starttls(context=ctx)
            s.login(user, pwd)
            s.sendmail(sender, recipients, msg.as_string())
        return True
    except Exception:
        return False

def htmlify(title: str, url: str, synopsis: str, deadline_iso: str | None, deadline_text: str | None) -> str:
    dl = deadline_iso or "TBD"
    raw = f"<div style='color:#64748b'>Original: {deadline_text}</div>" if deadline_text else ""
    syn = (synopsis or "").replace("\n", "<br/>")[:1200]
    return f"""
    <div style="font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif">
      <h2 style="margin:0 0 8px 0">{title}</h2>
      <div>Deadline: <strong>{dl}</strong></div>
      {raw}
      <p style="margin-top:12px">{syn}</p>
      <p><a href="{url}">{url}</a></p>
    </div>
    """

# ---------- ICS (calendar) ----------
def _safe_name(s: str) -> str:
    s = re.sub(r"[^\w\-. ]+", "", s.strip()) or "event"
    return re.sub(r"\s+", "_", s)[:64]

def build_ics_all_day(summary: str, date_yyyy_mm_dd: Optional[str], description: str = "", url: str = "") -> str:
    # All-day event on deadline date; if None => today
    from datetime import datetime, timezone
    uid = hashlib.sha1(f"{summary}|{date_yyyy_mm_dd}|{url}".encode("utf-8")).hexdigest() + "@grants-rag"
    dtstamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
    if not date_yyyy_mm_dd:
        date_yyyy_mm_dd = datetime.utcnow().date().isoformat()
    y, m, d = date_yyyy_mm_dd.split("-")
    dt = f"{y}{m}{d}"  # VALUE=DATE

    desc = description or ""
    if url:
        desc = (desc + "\n" + url).strip()

    ics = [
        "BEGIN:VCALENDAR",
        "VERSION:2.0",
        "PRODID:-//grants-rag//EN",
        "CALSCALE:GREGORIAN",
        "METHOD:PUBLISH",
        "BEGIN:VEVENT",
        f"UID:{uid}",
        f"DTSTAMP:{dtstamp}",
        f"DTSTART;VALUE=DATE:{dt}",
        f"SUMMARY:{summary}",
        f"DESCRIPTION:{desc}",
        f"URL:{url}" if url else "",
        "END:VEVENT",
        "END:VCALENDAR",
        "",
    ]
    return "\n".join([x for x in ics if x])

def filename_for_ics(title: str, deadline_iso: Optional[str]) -> str:
    tag = (deadline_iso or "TBD").replace("-", "")
    return f"{_safe_name(title)}_{tag}.ics"