emma-hermes/gateway/notification_preferences.py

177 lines
5.1 KiB
Python

from __future__ import annotations
import json
import re
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional
from hermes_constants import get_hermes_home
MAILBOX_SUMMARY_CATEGORY = "mailbox_summary"
_STOP_PATTERNS = (
re.compile(r"\b(?:stop|unsubscribe|cancel)\b", re.IGNORECASE),
re.compile(r"不要再寄"),
re.compile(r"停止提供"),
re.compile(r"停止提醒"),
re.compile(r"不要摘要"),
re.compile(r"不要再給我"),
)
_SUMMARY_PATTERNS = (
re.compile(r"信件摘要"),
re.compile(r"昨天信件摘要"),
re.compile(r"summary", re.IGNORECASE),
re.compile(r"email\s*summary", re.IGNORECASE),
re.compile(r"mail\s*summary", re.IGNORECASE),
re.compile(r"digest", re.IGNORECASE),
re.compile(r"摘要"),
)
def _preferences_path() -> Path:
return get_hermes_home() / "state" / "notification_preferences.json"
def normalize_email(value: str | None) -> str:
return str(value or "").strip().lower()
def load_notification_preferences() -> Dict[str, Dict[str, Dict[str, Any]]]:
path = _preferences_path()
if not path.exists():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {}
return data if isinstance(data, dict) else {}
def save_notification_preferences(data: Dict[str, Dict[str, Dict[str, Any]]]) -> None:
path = _preferences_path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
def add_hard_stop(
recipient_email: str,
*,
category: str = MAILBOX_SUMMARY_CATEGORY,
source_platform: str = "",
note: str = "",
) -> bool:
email = normalize_email(recipient_email)
if not email:
return False
prefs = load_notification_preferences()
bucket = prefs.setdefault(category, {})
row = dict(bucket.get(email) or {})
row.update(
{
"enabled": True,
"updated_at": datetime.now().isoformat(),
"source_platform": str(source_platform or "").strip(),
"note": str(note or "").strip(),
}
)
bucket[email] = row
save_notification_preferences(prefs)
return True
def has_hard_stop(recipient_email: str, *, category: str = MAILBOX_SUMMARY_CATEGORY) -> bool:
email = normalize_email(recipient_email)
if not email:
return False
prefs = load_notification_preferences()
row = ((prefs.get(category) or {}).get(email) or {})
return bool(row.get("enabled"))
def text_requests_mailbox_summary_stop(text: str | None) -> bool:
raw = str(text or "").strip()
if not raw:
return False
has_stop = any(p.search(raw) for p in _STOP_PATTERNS)
has_summary = any(p.search(raw) for p in _SUMMARY_PATTERNS)
return has_stop and has_summary
def _script_path(job: Dict[str, Any]) -> Optional[Path]:
script = str(job.get("script") or "").strip()
if not script:
return None
path = Path(script).expanduser()
if path.is_absolute():
return path
return get_hermes_home() / "scripts" / script
def job_targets_email(job: Dict[str, Any], recipient_email: str) -> bool:
email = normalize_email(recipient_email)
if not email:
return False
path = _script_path(job)
if not path or not path.exists():
return False
try:
content = path.read_text(encoding="utf-8", errors="replace")
except Exception:
return False
patterns = (
re.compile(rf"RECIPIENT\s*=\s*['\"]{re.escape(email)}['\"]", re.IGNORECASE),
re.compile(rf"To:\s*{re.escape(email)}", re.IGNORECASE),
)
return any(p.search(content) for p in patterns)
def job_looks_like_mailbox_summary(job: Dict[str, Any]) -> bool:
haystacks = [
str(job.get("name") or ""),
str(job.get("script") or ""),
str(job.get("prompt_preview") or ""),
]
text = "\n".join(haystacks)
if not any(p.search(text) for p in _SUMMARY_PATTERNS):
return False
return bool(re.search(r"himalaya|mailbox|email", text, re.IGNORECASE))
def matching_mailbox_summary_jobs(
jobs: Iterable[Dict[str, Any]],
recipient_email: str,
) -> List[Dict[str, Any]]:
matches: List[Dict[str, Any]] = []
for job in jobs:
if not job_looks_like_mailbox_summary(job):
continue
if not job_targets_email(job, recipient_email):
continue
matches.append(job)
return matches
def stop_mailbox_summary_jobs_for_recipient(recipient_email: str) -> List[Dict[str, Any]]:
from cron.jobs import list_jobs, remove_job
jobs = list_jobs(include_disabled=True)
matches = matching_mailbox_summary_jobs(jobs, recipient_email)
removed: List[Dict[str, Any]] = []
for job in matches:
job_id = str(job.get("id") or "")
if not job_id:
continue
if remove_job(job_id):
removed.append(job)
return removed
def infer_source_email(source: Any) -> str | None:
user_id = normalize_email(getattr(source, "user_id", None))
if user_id and "@" in user_id:
return user_id
return None