|
| 1 | +require "placeos-driver" |
| 2 | +require "placeos-driver/interface/mailer" |
| 3 | +require "placeos-driver/interface/mailer_templates" |
| 4 | + |
| 5 | +# other data |
| 6 | +require "../wiegand/models" |
| 7 | +require "../place/visitor_models" |
| 8 | + |
| 9 | +class InnerRange::IntegritiBookingCheckin < PlaceOS::Driver |
| 10 | + include PlaceOS::Driver::Interface::MailerTemplates |
| 11 | + |
| 12 | + descriptive_name "Integriti Visitor Access" |
| 13 | + generic_name :VisitorAccess |
| 14 | + |
| 15 | + default_settings({ |
| 16 | + timezone: "GMT", |
| 17 | + date_time_format: "%c", |
| 18 | + time_format: "%l:%M%p", |
| 19 | + date_format: "%A, %-d %B", |
| 20 | + visitor_access_template: "visitor_access", |
| 21 | + determine_host_name_using: "calendar-driver", |
| 22 | + }) |
| 23 | + |
| 24 | + @time_zone : Time::Location = Time::Location.load("GMT") |
| 25 | + |
| 26 | + # See: https://crystal-lang.org/api/0.35.1/Time/Format.html |
| 27 | + @date_time_format : String = "%c" |
| 28 | + @time_format : String = "%l:%M%p" |
| 29 | + @date_format : String = "%A, %-d %B" |
| 30 | + |
| 31 | + @visitor_access_template : String = "visitor_access" |
| 32 | + @determine_host_name_using : String = "calendar-driver" |
| 33 | + |
| 34 | + @users_granted_access : UInt64 = 0_u64 |
| 35 | + |
| 36 | + def on_load |
| 37 | + # Guest has arrived in the lobby |
| 38 | + monitor("staff/guest/checkin") { |_subscription, payload| guest_checked_in(payload.gsub(/[^[:print:]]/, "")) } |
| 39 | + on_update |
| 40 | + end |
| 41 | + |
| 42 | + def on_update |
| 43 | + @date_time_format = setting?(String, :date_time_format) || "%c" |
| 44 | + @time_format = setting?(String, :time_format) || "%l:%M%p" |
| 45 | + @date_format = setting?(String, :date_format) || "%A, %-d %B" |
| 46 | + @visitor_access_template = setting?(String, :visitor_access_template) || "visitor_access" |
| 47 | + @determine_host_name_using = setting?(String, :determine_host_name_using) || "calendar-driver" |
| 48 | + |
| 49 | + time_zone = setting?(String, :timezone).presence || config.control_system.try(&.timezone) || "GMT" |
| 50 | + @time_zone = Time::Location.load(time_zone) |
| 51 | + |
| 52 | + @control_system_zone_list = nil |
| 53 | + @building_id = nil |
| 54 | + @building_zone = nil |
| 55 | + end |
| 56 | + |
| 57 | + accessor locations : LocationServices_1 |
| 58 | + accessor integriti : Integriti_1 |
| 59 | + accessor staff_api : StaffAPI_1 |
| 60 | + accessor calendar : Calendar_1 |
| 61 | + |
| 62 | + def mailer |
| 63 | + system.implementing(Interface::Mailer)[0] |
| 64 | + end |
| 65 | + |
| 66 | + getter control_system_zone_list : Array(String) do |
| 67 | + config.control_system.not_nil!.zones |
| 68 | + end |
| 69 | + |
| 70 | + getter building_id : String do |
| 71 | + locations.building_id.get.as_s |
| 72 | + end |
| 73 | + |
| 74 | + class ZoneDetails |
| 75 | + include JSON::Serializable |
| 76 | + |
| 77 | + property id : String |
| 78 | + property name : String |
| 79 | + property display_name : String? |
| 80 | + property location : String? |
| 81 | + property tags : Array(String) |
| 82 | + property parent_id : String? |
| 83 | + end |
| 84 | + |
| 85 | + getter building_zone : ZoneDetails do |
| 86 | + ZoneDetails.from_json staff_api.zone(building_id).get.to_json |
| 87 | + end |
| 88 | + |
| 89 | + protected def guest_checked_in(payload) |
| 90 | + logger.debug { "received guest event payload: #{payload}" } |
| 91 | + guest_details = Place::GuestNotification.from_json payload |
| 92 | + zones = guest_details.zones |
| 93 | + return unless zones |
| 94 | + |
| 95 | + # ensure the event is for this building |
| 96 | + if (config.control_system.not_nil!.zones & zones).empty? |
| 97 | + logger.debug { "ignoring event as does not match any zones" } |
| 98 | + return |
| 99 | + end |
| 100 | + |
| 101 | + case guest_details |
| 102 | + when Place::GuestCheckin |
| 103 | + grant_and_notify_access( |
| 104 | + guest_details.attendee_email, |
| 105 | + guest_details.attendee_name.as(String), |
| 106 | + guest_details.host.as(String), |
| 107 | + guest_details.event_summary, |
| 108 | + guest_details.event_starting |
| 109 | + ) |
| 110 | + self[:users_granted_access] = @users_granted_access += 1 |
| 111 | + else |
| 112 | + logger.debug { "ignoring event as not a checkin: #{guest_details.class}" } |
| 113 | + end |
| 114 | + end |
| 115 | + |
| 116 | + def grant_and_notify_access( |
| 117 | + visitor_email : String, |
| 118 | + visitor_name : String, |
| 119 | + host_email : String, |
| 120 | + event_title : String?, |
| 121 | + event_start : Int64 |
| 122 | + ) |
| 123 | + local_start_time = Time.unix(event_start).in(@time_zone) |
| 124 | + late_in_day = local_start_time.at_end_of_day - 7.hours |
| 125 | + |
| 126 | + access_from = (local_start_time - 15.minutes).to_unix |
| 127 | + access_until = local_start_time < late_in_day ? late_in_day : (local_start_time + 2.hours) |
| 128 | + card_details = integriti.grant_guest_access(visitor_name, visitor_email, access_from, access_until).get |
| 129 | + card_facility = card_details["card_facility"].as_i64.to_u32 |
| 130 | + card_number = card_details["card_number"].as_i64.to_u32 |
| 131 | + |
| 132 | + # remove the 2 sign bits |
| 133 | + wiegand = Wiegand::Wiegand26.from_components(facility: card_facility, card_number: card_number) |
| 134 | + raw = (wiegand.wiegand & (Wiegand::Wiegand26::FACILITY_MASK | Wiegand::Wiegand26::CARD_MASK)) >> 1 |
| 135 | + data = raw.to_s(16).upcase.rjust(6, '0') |
| 136 | + |
| 137 | + # convert to hex and create QR code |
| 138 | + qr_png = mailer.generate_png_qrcode(text: data, size: 256).get.as_s |
| 139 | + attach = [ |
| 140 | + { |
| 141 | + file_name: "access.png", |
| 142 | + content: qr_png, |
| 143 | + content_id: visitor_email, |
| 144 | + }, |
| 145 | + ] |
| 146 | + |
| 147 | + mailer.send_template( |
| 148 | + visitor_email, |
| 149 | + {"visitor_invited", @visitor_access_template}, |
| 150 | + { |
| 151 | + visitor_email: visitor_email, |
| 152 | + visitor_name: visitor_name, |
| 153 | + host_name: get_host_name(host_email), |
| 154 | + host_email: host_email, |
| 155 | + building_name: building_zone.display_name.presence || building_zone.name, |
| 156 | + event_title: event_title, |
| 157 | + event_start: local_start_time.to_s(@time_format), |
| 158 | + event_date: local_start_time.to_s(@date_format), |
| 159 | + event_time: local_start_time.to_s(@time_format), |
| 160 | + }, |
| 161 | + attach |
| 162 | + ) |
| 163 | + end |
| 164 | + |
| 165 | + def template_fields : Array(TemplateFields) |
| 166 | + time_now = Time.utc.in(@time_zone) |
| 167 | + |
| 168 | + invitation_fields = [ |
| 169 | + {name: "visitor_email", description: "Email address of the visiting guest"}, |
| 170 | + {name: "visitor_name", description: "Full name of the visiting guest"}, |
| 171 | + {name: "host_name", description: "Name of the person hosting the visitor"}, |
| 172 | + {name: "host_email", description: "Email address of the host"}, |
| 173 | + {name: "building_name", description: "Name of the building where the visit occurs"}, |
| 174 | + {name: "event_title", description: "Title or purpose of the visit"}, |
| 175 | + {name: "event_start", description: "Start time (e.g., #{time_now.to_s(@time_format)})"}, |
| 176 | + {name: "event_date", description: "Date of the visit (e.g., #{time_now.to_s(@date_format)})"}, |
| 177 | + {name: "event_time", description: "Time of the visit (or 'all day' for 24-hour events)"}, |
| 178 | + ] |
| 179 | + |
| 180 | + [ |
| 181 | + TemplateFields.new( |
| 182 | + trigger: {"visitor_invited", @visitor_access_template}, |
| 183 | + name: "Visitor invited", |
| 184 | + description: "Visitor entry security email with QR code for access", |
| 185 | + fields: invitation_fields |
| 186 | + ), |
| 187 | + ] |
| 188 | + end |
| 189 | + |
| 190 | + protected def get_host_name(host_email) |
| 191 | + @determine_host_name_using == "staff-api-driver" ? get_host_name_from_staff_api_driver(host_email) : get_host_name_from_calendar_driver(host_email) |
| 192 | + end |
| 193 | + |
| 194 | + protected def get_host_name_from_calendar_driver(host_email) |
| 195 | + calendar.get_user(host_email).get["name"] |
| 196 | + rescue error |
| 197 | + logger.error { "issue loading host details #{host_email}" } |
| 198 | + return "your host" |
| 199 | + end |
| 200 | + |
| 201 | + protected def get_host_name_from_staff_api_driver(host_email, retries = 0) |
| 202 | + staff_api.staff_details(host_email).get["name"].as_s.split('(')[0] |
| 203 | + rescue error |
| 204 | + if retries > 3 |
| 205 | + logger.error { "issue loading host details #{host_email}" } |
| 206 | + return "your host" |
| 207 | + end |
| 208 | + sleep 1.second |
| 209 | + get_host_name_from_staff_api_driver(host_email, retries + 1) |
| 210 | + end |
| 211 | +end |
0 commit comments