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:
parent
3dbe68705b
commit
d541719c4c
2 changed files with 71 additions and 16 deletions
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue