Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions drivers/place/staff_api.cr
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,13 @@ class Place::StaffAPI < PlaceOS::Driver
JSON.parse(response.body)
end

def event_guests(event_id : String, system_id : String)
logger.debug { "getting guests for event #{event_id} in system #{system_id}" }
response = get("/api/staff/v1/events/#{event_id}/guests?system_id=#{system_id}", headers: authentication)
raise "issue getting guests for event #{event_id}: #{response.status_code}" unless response.success?
JSON.parse(response.body)
end

# lists asset IDs based on the parameters provided
#
# booking_type is required unless event_id or ical_uid is present
Expand Down
161 changes: 145 additions & 16 deletions drivers/place/visitor_mailer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ class Place::VisitorMailer < PlaceOS::Driver
# Booking details have changed — notify all visitors if relevant fields changed
monitor("staff/booking/changed") { |_subscription, payload| booking_changed_event(payload.gsub(/[^[:print:]]/, "")) }

# Calendar event details have changed — notify visitors / previous host
monitor("staff/event/changed") { |_subscription, payload| event_changed_event(payload.gsub(/[^[:print:]]/, "")) }

on_update
end

Expand Down Expand Up @@ -553,8 +556,10 @@ class Place::VisitorMailer < PlaceOS::Driver
logger.debug { "received booking changed payload: #{payload}" }
details = BookingChanged.from_json payload

# only respond to full changes, not metadata-only updates
return unless details.action == "changed"
# Only process actions that can carry visitor-relevant changes.
# Using an allowlist ensures new action types (e.g. "approved", "rejected",
# "checked_in") are ignored by default and don't trigger spurious emails.
return unless details.action.in?("changed", "metadata_changed")

# ensure the event is for this building
if zones = details.zones
Expand All @@ -574,6 +579,9 @@ class Place::VisitorMailer < PlaceOS::Driver
if prev_start = details.previous_booking_start
fields_changed = true if prev_start != details.booking_start
end
if prev_end = details.previous_booking_end
fields_changed = true if prev_end != details.booking_end
end

# Location changed: zones identify the building/room the visitor should attend
if prev_zones = details.previous_zones
Expand Down Expand Up @@ -607,29 +615,157 @@ class Place::VisitorMailer < PlaceOS::Driver
end

guests = staff_api.booking_guests(details.id).get.as_a
send_booking_changed_emails(
guests,
details.user_email,
details.booking_start,
details.title,
details.previous_booking_start,
previous_building_name,
previous_room_name,
)
rescue error
logger.error { error.inspect_with_backtrace }
self[:error_count] = @error_count += 1
self[:last_error] = {
error: error.message,
time: Time.local.to_s,
user: payload,
}
end

protected def event_changed_event(payload)
logger.debug { "received event changed payload: #{payload}" }
details = EventChanged.from_json payload

# only respond to updates, not creates or cancellations
return unless details.action == "update"

# ensure the event is for this building
if zones = details.zones
check = [building_zone.id] + @parent_zone_ids

if (check & zones).empty?
logger.debug { "ignoring event_changed as does not match any zones: #{check}" }
return
end
end

# --- Host change notification
if prev_host = details.previous_host_email
if prev_host.downcase != details.host.downcase
send_original_host_email(
@notify_original_host_template,
prev_host,
details.host,
details.title,
details.event_start,
)
end
end

# --- Date / time / location change notification
fields_changed = false

# Date or time changed
if prev_start = details.previous_event_start
fields_changed = true if prev_start != details.event_start
end
if prev_end = details.previous_event_end
fields_changed = true if prev_end != details.event_end
end

# Location changed (system_id represents the room)
if prev_sys = details.previous_system_id
fields_changed = true if prev_sys != details.system_id
end

return unless fields_changed

# Resolve previous location names from previous_system_id when room changed.
# Default previous_room_name to "unknown" when we know there was a different
# previous system — if the lookup succeeds it will be overwritten with the
# real name; if it fails (rescue) the "unknown" default is preserved and the
# recipient can see that the original location could not be determined.
previous_building_name = building_zone.display_name.presence || building_zone.name
previous_room_name = @booking_space_name

if (prev_sys_id = details.previous_system_id) && prev_sys_id != details.system_id
previous_room_name = "unknown"
begin
prev_sys = get_room_details(prev_sys_id)
previous_room_name = prev_sys.display_name.presence || prev_sys.name
if prev_zones = prev_sys.zones
prev_zones.each do |zone_id|
begin
zone = fetch_zone(zone_id)
if zone.tags.includes?(@invite_zone_tag)
previous_building_name = zone.display_name.presence || zone.name
break
end
rescue error
logger.warn(exception: error) { "error looking up previous zone #{zone_id}" }
end
end
end
rescue error
logger.warn(exception: error) { "error looking up previous system #{prev_sys_id}" }
end
end

guests = staff_api.event_guests(details.event_id, details.system_id).get.as_a
send_booking_changed_emails(
guests,
details.host,
details.event_start,
details.title,
details.previous_event_start,
previous_building_name,
previous_room_name,
)
rescue error
logger.error { error.inspect_with_backtrace }
self[:error_count] = @error_count += 1
self[:last_error] = {
error: error.message,
time: Time.local.to_s,
user: payload,
}
end

# Sends booking-changed notification emails to each visitor in the guest list.
private def send_booking_changed_emails(
guests : Array(JSON::Any),
host_email : String,
event_start : Int64,
event_title : String?,
previous_start : Int64?,
previous_building_name : String,
previous_room_name : String,
)
guests.each do |guest|
visitor_email = guest["email"].as_s
visitor_name = guest["name"].as_s?

# don't email staff members
next if !@host_domain_filter.empty? && visitor_email.split('@', 2)[1].downcase.in?(@host_domain_filter)

local_start_time = Time.unix(details.booking_start).in(@time_zone)
local_start_time = Time.unix(event_start).in(@time_zone)

previous_date = details.previous_booking_start.try { |timestamp| Time.unix(timestamp).in(@time_zone).to_s(@date_format) }
previous_time = details.previous_booking_start.try { |timestamp| Time.unix(timestamp).in(@time_zone).to_s(@time_format) }
previous_date = previous_start.try { |timestamp| Time.unix(timestamp).in(@time_zone).to_s(@date_format) }
previous_time = previous_start.try { |timestamp| Time.unix(timestamp).in(@time_zone).to_s(@time_format) }

mailer.send_template(
visitor_email,
{"visitor_invited", @booking_changed_template},
{
visitor_email: visitor_email,
visitor_name: visitor_name,
host_name: get_host_name(details.user_email),
host_email: details.user_email,
host_name: get_host_name(host_email),
host_email: host_email,
room_name: @booking_space_name,
building_name: building_zone.display_name.presence || building_zone.name,
event_title: details.title,
event_title: event_title,
event_start: local_start_time.to_s(@time_format),
event_date: local_start_time.to_s(@date_format),
event_time: local_start_time.to_s(@time_format),
Expand All @@ -642,14 +778,6 @@ class Place::VisitorMailer < PlaceOS::Driver
rescue error
logger.warn(exception: error) { "failed to send booking_changed email to #{visitor_email}" }
end
rescue error
logger.error { error.inspect_with_backtrace }
self[:error_count] = @error_count += 1
self[:last_error] = {
error: error.message,
time: Time.local.to_s,
user: payload,
}
end

@[Security(Level::Support)]
Expand Down Expand Up @@ -830,6 +958,7 @@ class Place::VisitorMailer < PlaceOS::Driver
property name : String
property display_name : String?
property map_id : String?
property zones : Array(String)?
end

protected def get_room_details(system_id : String, retries = 0)
Expand Down
Loading
Loading