feat: support optional start/end time on events
event_start_date / event_end_date now accept either YYYY-MM-DD (date-only) or YYYY-MM-DDTHH:MM (ISO datetime). The NIP-52 publisher switches kind on the "T" delimiter: kind 31922 (date-based, YYYY-MM-DD start/end) when absent, kind 31923 (time-based, unix-timestamp start/end + day-granularity D tags) when present. Delete events match the original publish kind. Closing-date parsing accepts both formats. The LNbits admin form gains optional HH:MM inputs alongside each date picker; they fold into the wire-format string on submit and split back on edit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6aa280680e
commit
df4775126f
5 changed files with 138 additions and 32 deletions
|
|
@ -1,14 +1,17 @@
|
|||
"""
|
||||
NIP-52 calendar event publishing for the events extension.
|
||||
|
||||
Builds kind 31922 (date-based) calendar events from the Event model,
|
||||
signs them with the event creator's Account keypair, and publishes
|
||||
via the NostrClient to nostrclient relays.
|
||||
Builds NIP-52 calendar events from the Event model, signs them with the
|
||||
creator's Account keypair, and publishes via the NostrClient.
|
||||
|
||||
Kind 31922 is used for date-only events; kind 31923 (time-based) is used
|
||||
when event_start_date / event_end_date include a time component.
|
||||
|
||||
Reference: https://github.com/nostr-protocol/nips/blob/master/52.md
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import coincurve
|
||||
from loguru import logger
|
||||
|
|
@ -17,26 +20,60 @@ from .models import Event
|
|||
from .nostr.event import NostrEvent
|
||||
|
||||
|
||||
def _has_time(value: str | None) -> bool:
|
||||
"""ISO 8601 datetime strings contain a 'T' between date and time."""
|
||||
return value is not None and "T" in value
|
||||
|
||||
|
||||
def _to_unix(value: str) -> int:
|
||||
"""Parse ISO 8601 datetime (assume UTC if naive) to unix seconds."""
|
||||
dt = datetime.fromisoformat(value)
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return int(dt.timestamp())
|
||||
|
||||
|
||||
def build_nip52_event(event: Event, pubkey: str) -> NostrEvent:
|
||||
"""
|
||||
Convert an Event model to a NIP-52 kind 31922 (date-based) calendar event.
|
||||
Convert an Event model to a NIP-52 calendar event.
|
||||
|
||||
Tags:
|
||||
d - event.id (addressable identifier)
|
||||
Time-based (kind 31923) if event_start_date carries an HH:MM, otherwise
|
||||
date-based (kind 31922). Tags:
|
||||
d - event.id
|
||||
title - event.name
|
||||
start - event.event_start_date (ISO date string)
|
||||
end - event.event_end_date (optional)
|
||||
image - event.banner (optional)
|
||||
Content: event.info (description)
|
||||
start - unix timestamp (31923) or YYYY-MM-DD (31922)
|
||||
end - same encoding (optional)
|
||||
image, location, t (categories) - optional
|
||||
Content: event.info
|
||||
"""
|
||||
time_based = _has_time(event.event_start_date)
|
||||
kind = 31923 if time_based else 31922
|
||||
start_value = (
|
||||
str(_to_unix(event.event_start_date)) if time_based else event.event_start_date
|
||||
)
|
||||
|
||||
tags = [
|
||||
["d", event.id],
|
||||
["title", event.name],
|
||||
["start", event.event_start_date],
|
||||
["start", start_value],
|
||||
]
|
||||
|
||||
end_unix: int | None = None
|
||||
if event.event_end_date:
|
||||
tags.append(["end", event.event_end_date])
|
||||
end_value = (
|
||||
str(_to_unix(event.event_end_date)) if time_based else event.event_end_date
|
||||
)
|
||||
tags.append(["end", end_value])
|
||||
if time_based:
|
||||
end_unix = _to_unix(event.event_end_date)
|
||||
|
||||
if time_based:
|
||||
start_unix = _to_unix(event.event_start_date)
|
||||
start_day = start_unix // 86400
|
||||
end_day = (end_unix // 86400) if end_unix is not None else start_day
|
||||
for day in range(start_day, end_day + 1):
|
||||
tags.append(["D", str(day)])
|
||||
|
||||
if event.banner:
|
||||
tags.append(["image", event.banner])
|
||||
if event.location:
|
||||
|
|
@ -47,7 +84,7 @@ def build_nip52_event(event: Event, pubkey: str) -> NostrEvent:
|
|||
nostr_event = NostrEvent(
|
||||
pubkey=pubkey,
|
||||
created_at=int(time.time()),
|
||||
kind=31922,
|
||||
kind=kind,
|
||||
tags=tags,
|
||||
content=event.info or "",
|
||||
)
|
||||
|
|
@ -59,15 +96,17 @@ def build_nip52_delete_event(event: Event, pubkey: str) -> NostrEvent:
|
|||
"""
|
||||
Build a kind 5 delete event for a published NIP-52 calendar event.
|
||||
|
||||
Uses an 'a' tag to reference the parameterized replaceable event
|
||||
(kind 31922) per NIP-09.
|
||||
Uses an 'a' tag to reference the parameterized replaceable event per
|
||||
NIP-09. The referenced kind must match what we published — 31923 for
|
||||
time-based events, 31922 for date-only.
|
||||
"""
|
||||
referenced_kind = 31923 if _has_time(event.event_start_date) else 31922
|
||||
nostr_event = NostrEvent(
|
||||
pubkey=pubkey,
|
||||
created_at=int(time.time()),
|
||||
kind=5,
|
||||
tags=[
|
||||
["a", f"31922:{pubkey}:{event.id}"],
|
||||
["a", f"{referenced_kind}:{pubkey}:{event.id}"],
|
||||
],
|
||||
content="Event canceled",
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue