1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-08-01 18:18:31 +00:00

- Compare previous-current events to determine if non-invitee changes were made (notify non-added/removed attendees of event update)

This commit is contained in:
Nate Harris 2025-07-19 23:00:25 -06:00
parent 3dbe68705b
commit d541719c4c
2 changed files with 71 additions and 16 deletions

View file

@ -55,16 +55,17 @@ def _cleanup(path):
class HookNotificationItem:
def __init__(self, notification_item_type, path, uid=None, new_content=None, old_content=None):
def __init__(self, notification_item_type, path, content=None, uid=None, new_content=None, old_content=None):
self.type = notification_item_type.value
self.point = _cleanup(path)
self._content_legacy = content
self.uid = uid
self.new_content = new_content
self.old_content = old_content
@property
def content(self): # For backward compatibility
return self.uid or self.new_content or self.old_content
return self._content_legacy or self.uid or self.new_content or self.old_content
@property
def replaces_existing_item(self) -> bool:
@ -73,8 +74,7 @@ class HookNotificationItem:
def to_json(self):
return json.dumps(
self,
default=lambda o: o.__dict__,
{**self.__dict__, "content": self.content},
sort_keys=True,
indent=4
)

View file

@ -2,6 +2,8 @@
# for email notifications
# Copyright © 2025-2025 Nate Harris
import enum
import hashlib
import json
import re
import smtplib
import ssl
@ -74,7 +76,9 @@ This is an automated message. Please do not reply.""",
},
"updated_event_template": {
"value": """Hello $attendee_name,
The following event has been updated.
$event_title
$event_start_time - $event_end_time
$event_location
@ -189,6 +193,42 @@ def determine_added_removed_and_unaltered_attendees(original_event: 'Event',
return added_attendees, removed_attendees, unaltered_attendees
def event_details_other_than_attendees_changed(original_event: 'Event',
new_event: 'Event') -> bool:
"""
Check if any details other than attendees and IDs have changed between two events.
"""
def hash_dict(d: Dict[str, Any]) -> str:
"""
Create a hash of the dictionary to compare contents.
This will ignore None values and empty strings.
"""
return hashlib.sha1(json.dumps(d).encode("utf8")).hexdigest()
original_event_details = {
"summary": original_event.summary,
"description": original_event.description,
"location": original_event.location,
"datetime_start": original_event.datetime_start.time_string() if original_event.datetime_start else None,
"datetime_end": original_event.datetime_end.time_string() if original_event.datetime_end else None,
"duration": original_event.duration,
"status": original_event.status,
"organizer": original_event.organizer
}
new_event_details = {
"summary": new_event.summary,
"description": new_event.description,
"location": new_event.location,
"datetime_start": new_event.datetime_start.time_string() if new_event.datetime_start else None,
"datetime_end": new_event.datetime_end.time_string() if new_event.datetime_end else None,
"duration": new_event.duration,
"status": new_event.status,
"organizer": new_event.organizer
}
return hash_dict(original_event_details) != hash_dict(new_event_details)
class ContentLine:
_key: str
value: Any
@ -439,6 +479,11 @@ class Event(VComponent):
"""Return the summary of the event."""
return self._get_content_lines("SUMMARY")[0].value
@property
def description(self) -> Optional[str]:
"""Return the description of the event."""
return self._get_content_lines("DESCRIPTION")[0].value
@property
def location(self) -> Optional[str]:
"""Return the location of the event."""
@ -668,7 +713,8 @@ class EmailConfig:
"""
ics_attachment = ICSEmailAttachment(file_content=event.ics_content, file_name=f"{event.file_name}")
return self._prepare_and_send_email(template=self.new_or_added_to_event_template, attendees=attendees, event=event,
return self._prepare_and_send_email(template=self.new_or_added_to_event_template, attendees=attendees,
event=event,
ics_attachment=ics_attachment)
def send_updated_email(self, attendees: List[Attendee], event: EmailEvent) -> bool:
@ -690,7 +736,8 @@ class EmailConfig:
:param event: The event being deleted (or the event the attendee is being removed from).
:return: True if the email was sent successfully, False otherwise.
"""
return self._prepare_and_send_email(template=self.deleted_or_removed_from_event_template, attendees=attendees, event=event,
return self._prepare_and_send_email(template=self.deleted_or_removed_from_event_template, attendees=attendees,
event=event,
ics_attachment=None)
def _prepare_and_send_email(self, template: MessageTemplate, attendees: List[Attendee],
@ -910,7 +957,8 @@ class Hook(BaseHook):
event=email_event
)
if not email_success:
logger.error("Failed to send some or all added email notifications for event: %s", email_event.event.uid)
logger.error("Failed to send some or all added email notifications for event: %s",
email_event.event.uid)
return
# Dealing with an update to an existing event, compare new and previous content.
@ -924,7 +972,8 @@ class Hook(BaseHook):
event=email_event
)
if not email_success:
logger.error("Failed to send some or all added email notifications for event: %s", email_event.event.uid)
logger.error("Failed to send some or all added email notifications for event: %s",
email_event.event.uid)
return
# Determine added, removed, and unaltered attendees
@ -938,7 +987,8 @@ class Hook(BaseHook):
event=email_event
)
if not email_success:
logger.error("Failed to send some or all added email notifications for event: %s", email_event.event.uid)
logger.error("Failed to send some or all added email notifications for event: %s",
email_event.event.uid)
# Notify removed attendees as "event deleted"
if removed_attendees:
@ -947,18 +997,22 @@ class Hook(BaseHook):
event=email_event
)
if not email_success:
logger.error("Failed to send some or all removed email notifications for event: %s", email_event.event.uid)
logger.error("Failed to send some or all removed email notifications for event: %s",
email_event.event.uid)
# Notify unaltered attendees as "event updated"
if unaltered_attendees:
# TODO: Determine WHAT was updated in the event and send a more specific message if needed
# TODO: Don't send an email to unaltered attendees if only change was adding/removing other attendees
# Notify unaltered attendees as "event updated" if details other than attendees have changed
if unaltered_attendees and event_details_other_than_attendees_changed(original_event=previous_event,
new_event=new_event):
email_success: bool = self.email_config.send_updated_email( # type: ignore
attendees=unaltered_attendees,
event=email_event
)
if not email_success:
logger.error("Failed to send some or all updated email notifications for event: %s", email_event.event.uid)
logger.error("Failed to send some or all updated email notifications for event: %s",
email_event.event.uid)
# Skip sending notifications to existing attendees if the only changes made to the event
# were the addition/removal of other attendees.
return
@ -979,7 +1033,8 @@ class Hook(BaseHook):
event=email_event
)
if not email_success:
logger.error("Failed to send some or all deleted email notifications for event: %s", email_event.event.uid)
logger.error("Failed to send some or all deleted email notifications for event: %s",
email_event.event.uid)
return