177 lines
5.1 KiB
Python
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
|