Files
avatar/plugins/calendario.py
T
lucianoandClaude Fable 5.1 ff79832c30 AvatarPy: assistente personale con volto 3D, voce, memoria e plugin
Riscrittura in Python dell'assistente Avatar con interfaccia HUD (derivata da Mark LIV, CC BY-NC 4.0, vedi NOTICE.md).
Tre motori (Claude API, server locale OpenAI-compatibile, Claude Code), voce Kokoro/macOS, Whisper MLX,
avatar 3D con sincronizzazione labiale, memoria per categorie, allegati con OCR, monitor con avvisi,
plugin per Calendario, Mail, Promemoria, Note, Musica, app, Mac, timer, meteo, contatti, Messaggi,
file, Comandi Rapidi, browser, Telegram, WhatsApp (archivio e tempo reale).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 16:21:39 +02:00

228 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Calendario di macOS: legge, crea, sposta e cancella eventi (EventKit)."""
from __future__ import annotations
import threading
from datetime import date, datetime, timedelta
_store = None
_lock = threading.Lock()
GIORNI = ["lun", "mar", "mer", "gio", "ven", "sab", "dom"]
MESI = ["gen", "feb", "mar", "apr", "mag", "giu", "lug", "ago", "set", "ott", "nov", "dic"]
def _get_store():
"""EKEventStore con accesso richiesto una sola volta."""
global _store
with _lock:
if _store is not None:
return _store
import EventKit
store = EventKit.EKEventStore.alloc().init()
done, res = threading.Event(), {}
def cb(granted, err):
res["granted"] = bool(granted)
done.set()
if hasattr(store, "requestFullAccessToEventsWithCompletion_"):
store.requestFullAccessToEventsWithCompletion_(cb)
else:
store.requestAccessToEntityType_completion_(EventKit.EKEntityTypeEvent, cb)
done.wait(60)
if not res.get("granted"):
raise RuntimeError("Accesso al Calendario negato: concedilo in Impostazioni di Sistema > Privacy e sicurezza > Calendari.")
_store = store
return store
def _nsdate(dt: datetime):
from Foundation import NSDate
return NSDate.dateWithTimeIntervalSince1970_(dt.timestamp())
def _pydate(nsdate) -> datetime:
return datetime.fromtimestamp(nsdate.timeIntervalSince1970())
def _parse_day(s: str) -> date:
s = (s or "").strip().lower()
today = date.today()
if s in ("", "oggi", "today"):
return today
if s in ("domani", "tomorrow"):
return today + timedelta(days=1)
if s in ("dopodomani",):
return today + timedelta(days=2)
if s in ("ieri",):
return today - timedelta(days=1)
return datetime.strptime(s[:10], "%Y-%m-%d").date()
def _parse_dt(s: str) -> datetime:
s = (s or "").strip().replace("T", " ")
for fmt in ("%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
try:
return datetime.strptime(s, fmt)
except ValueError:
continue
raise ValueError(f"Data e ora non valide: '{s}'. Usa il formato AAAA-MM-GG HH:MM.")
def _fmt(e) -> str:
start, end = _pydate(e.startDate()), _pydate(e.endDate())
day = f"{GIORNI[start.weekday()]} {start.day} {MESI[start.month - 1]}"
when = "tutto il giorno" if e.isAllDay() else f"{start:%H:%M}–{end:%H:%M}"
extra = f" @ {e.location()}" if e.location() else ""
return f"{day} {when}: {e.title()} ({e.calendar().title()}){extra}"
def _events_between(start: datetime, end: datetime):
store = _get_store()
pred = store.predicateForEventsWithStartDate_endDate_calendars_(_nsdate(start), _nsdate(end), None)
evs = list(store.eventsMatchingPredicate_(pred) or [])
evs.sort(key=lambda e: e.startDate().timeIntervalSince1970())
return evs
def eventi(params: dict, ctx: dict) -> str:
da = _parse_day(params.get("da", ""))
a = _parse_day(params.get("a", "")) if params.get("a") else da
if a < da:
a = da
evs = _events_between(datetime.combine(da, datetime.min.time()), datetime.combine(a + timedelta(days=1), datetime.min.time()))
label = f"{da:%d/%m}" if da == a else f"dal {da:%d/%m} al {a:%d/%m}"
if not evs:
return f"Nessun evento {label}."
return f"Eventi {label}:\n" + "\n".join(f"- {_fmt(e)}" for e in evs[:40])
def _find_calendar(store, name: str):
import EventKit
cals = [c for c in store.calendarsForEntityType_(EventKit.EKEntityTypeEvent) if c.allowsContentModifications()]
if name:
for c in cals:
if c.title().lower() == name.lower():
return c
for c in cals:
if name.lower() in c.title().lower():
return c
return store.defaultCalendarForNewEvents() or (cals[0] if cals else None)
def crea(params: dict, ctx: dict) -> str:
import EventKit
store = _get_store()
titolo = str(params.get("titolo", "")).strip()
if not titolo:
return "Errore: serve il titolo dell'evento."
start = _parse_dt(str(params.get("inizio", "")))
all_day = bool(params.get("tutto_il_giorno", False))
if all_day:
start = start.replace(hour=0, minute=0)
end = start + timedelta(days=1)
elif params.get("fine"):
end = _parse_dt(str(params["fine"]))
else:
end = start + timedelta(minutes=int(params.get("durata_minuti") or 60))
ev = EventKit.EKEvent.eventWithEventStore_(store)
ev.setTitle_(titolo)
ev.setStartDate_(_nsdate(start))
ev.setEndDate_(_nsdate(end))
ev.setAllDay_(all_day)
cal = _find_calendar(store, str(params.get("calendario", "")))
if cal is None:
return "Errore: nessun calendario modificabile disponibile."
ev.setCalendar_(cal)
if params.get("luogo"):
ev.setLocation_(str(params["luogo"]))
if params.get("note"):
ev.setNotes_(str(params["note"]))
ok, err = store.saveEvent_span_error_(ev, EventKit.EKSpanThisEvent, None)
if not ok:
return f"Errore nel salvataggio: {err}"
return f"Evento creato: {_fmt(ev)}"
def _match(params: dict) -> tuple[list, str]:
titolo = str(params.get("titolo", "")).strip().lower()
giorno = _parse_day(params.get("giorno", ""))
evs = _events_between(datetime.combine(giorno, datetime.min.time()), datetime.combine(giorno + timedelta(days=1), datetime.min.time()))
found = [e for e in evs if titolo and titolo in (e.title() or "").lower()] if titolo else evs
return found, f"{giorno:%d/%m}"
def elimina(params: dict, ctx: dict) -> str:
import EventKit
found, label = _match(params)
if not found:
return f"Nessun evento corrispondente il {label}."
if len(found) > 1:
return "Ho trovato più eventi, dimmi quale:\n" + "\n".join(f"- {_fmt(e)}" for e in found)
ev = found[0]
desc = _fmt(ev)
def do() -> str:
store = _get_store()
ok, err = store.removeEvent_span_error_(ev, EventKit.EKSpanThisEvent, None)
msg = f"Evento cancellato: {desc}" if ok else f"Errore nella cancellazione: {err}"
if ctx.get("say"):
ctx["say"](msg)
return msg
confirm = ctx.get("confirm")
if confirm:
return confirm("calendario_elimina", "Cancellare l'evento?", desc, do)
# Senza interfaccia (server MCP): serve la conferma esplicita nei parametri.
if str(params.get("confermato", "")).lower() in ("true", "1", "sì", "si", "yes"):
return do()
return f"Prima di cancellare chiedi conferma all'utente per: {desc}. Poi richiama con confermato=true."
def sposta(params: dict, ctx: dict) -> str:
import EventKit
found, label = _match(params)
if not found:
return f"Nessun evento corrispondente il {label}."
if len(found) > 1:
return "Ho trovato più eventi, dimmi quale:\n" + "\n".join(f"- {_fmt(e)}" for e in found)
ev = found[0]
new_start = _parse_dt(str(params.get("nuovo_inizio", "")))
duration = _pydate(ev.endDate()) - _pydate(ev.startDate())
ev.setStartDate_(_nsdate(new_start))
ev.setEndDate_(_nsdate(new_start + duration))
ok, err = _get_store().saveEvent_span_error_(ev, EventKit.EKSpanThisEvent, None)
return f"Evento spostato: {_fmt(ev)}" if ok else f"Errore: {err}"
DAY = {"type": "string", "description": "Giorno: 'oggi', 'domani', 'dopodomani' oppure AAAA-MM-GG."}
TOOLS = [
{"name": "calendario_eventi",
"description": "Elenca gli eventi del Calendario di macOS in un giorno o in un intervallo. Usalo per domande come 'che impegni ho', 'cosa ho domani', 'sono libero venerdì'.",
"parameters": {"type": "object", "properties": {"da": DAY, "a": {**DAY, "description": "Ultimo giorno dell'intervallo (facoltativo)."}}, "required": ["da"]},
"run": eventi},
{"name": "calendario_crea",
"description": "Crea un evento nel Calendario. Converti tu le espressioni come 'domani alle 15' in data e ora assolute usando la data odierna del prompt.",
"parameters": {"type": "object", "properties": {
"titolo": {"type": "string"},
"inizio": {"type": "string", "description": "Inizio, formato AAAA-MM-GG HH:MM."},
"fine": {"type": "string", "description": "Fine, formato AAAA-MM-GG HH:MM (facoltativo)."},
"durata_minuti": {"type": "integer", "description": "Durata in minuti se manca la fine (default 60)."},
"tutto_il_giorno": {"type": "boolean"},
"calendario": {"type": "string", "description": "Nome del calendario (facoltativo, altrimenti quello predefinito)."},
"luogo": {"type": "string"}, "note": {"type": "string"}},
"required": ["titolo", "inizio"]},
"run": crea},
{"name": "calendario_sposta",
"description": "Sposta un evento esistente a un nuovo orario, mantenendone la durata.",
"parameters": {"type": "object", "properties": {"titolo": {"type": "string", "description": "Parte del titolo."}, "giorno": DAY,
"nuovo_inizio": {"type": "string", "description": "Nuovo inizio, AAAA-MM-GG HH:MM."}},
"required": ["titolo", "giorno", "nuovo_inizio"]},
"run": sposta},
{"name": "calendario_elimina",
"description": "Cancella un evento. Azione irreversibile: viene chiesta conferma sullo schermo; se lo strumento risponde con [CONFIRMATION_PENDING], di' all'utente di confermare sul pannello e non dire che è fatto.",
"parameters": {"type": "object", "properties": {"titolo": {"type": "string", "description": "Parte del titolo."}, "giorno": DAY,
"confermato": {"type": "boolean", "description": "Solo senza interfaccia: true dopo la conferma esplicita dell'utente."}},
"required": ["titolo", "giorno"]},
"run": elimina},
]