Skip to content

Commit

Permalink
Merge branch 'master' into PPT-1721-at_capacity_mailer
Browse files Browse the repository at this point in the history
  • Loading branch information
stakach authored Feb 4, 2025
2 parents 38462aa + dd73cde commit a9ce87a
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 28 deletions.
20 changes: 19 additions & 1 deletion drivers/gallagher/rest_api.cr
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,24 @@ class Gallagher::RestAPI < PlaceOS::Driver
# Door Security Interface
# =======================

# user id => email
@user_email_cache : Hash(String, String?) = {} of String => String?

def get_cardholder_email(user_id : String?) : String?
return nil unless user_id

if @user_email_cache.has_key? user_id
return @user_email_cache[user_id]
end

details = get_cardholder(user_id)
email_key = "@#{@unique_pdf_name}"
@user_email_cache[user_id] = details.json_unmapped[email_key]?.try(&.as_s)
rescue error
logger.warn(exception: error) { "failed to lookup email for user: #{user_id}" }
nil
end

def door_list : Array(Door)
doors.map { |d| Door.new(d.id, d.name) }
end
Expand Down Expand Up @@ -608,7 +626,7 @@ class Gallagher::RestAPI < PlaceOS::Driver
action: mapped.action,
card_id: event.card.try &.number,
user_name: event.cardholder.try &.name,
user_email: nil
user_email: get_cardholder_email(event.cardholder.try &.id)
).to_json)
end
end
Expand Down
28 changes: 27 additions & 1 deletion drivers/inner_range/integriti.cr
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class InnerRange::Integriti < PlaceOS::Driver
custom_field_hid_origo: "cf_HasVirtualCard",
custom_field_email: "cf_EmailAddress",
custom_field_phone: "cf_Mobile",
custom_field_csv_sync: "cf_CSVCustom",

# 16 bit card number in Wiegand 26
# Ideally guests have their own site id and the full range of card numbers
Expand All @@ -48,6 +49,7 @@ class InnerRange::Integriti < PlaceOS::Driver
@cf_origo = setting?(String, :custom_field_hid_origo) || "cf_HasVirtualCard"
@cf_email = setting?(String, :custom_field_email) || "cf_EmailAddress"
@cf_phone = setting?(String, :custom_field_phone) || "cf_Mobile"
@cf_csv = setting?(String, :custom_field_csv_sync) || "cf_CSVCustom"
@guest_card_template = setting?(String, :guest_card_template) || ""
guest_card_start = setting?(UInt16, :guest_card_start) || 0_u16
guest_card_end = setting?(UInt16, :guest_card_end) || (UInt16::MAX - 1_u16)
Expand Down Expand Up @@ -77,6 +79,7 @@ class InnerRange::Integriti < PlaceOS::Driver
getter cf_email : String = "cf_EmailAddress"
getter cf_phone : String = "cf_Mobile"
getter cf_origo : String = "cf_HasVirtualCard"
getter cf_csv : String = "cf_CSVCustom"
getter guest_card_template : String = ""
getter guest_access_group : String = ""
@guest_card_range : Range(UInt16, UInt16) = 0_u16..UInt16::MAX
Expand Down Expand Up @@ -624,6 +627,7 @@ class InnerRange::Integriti < PlaceOS::Driver
"cf_origo" => origo : Bool,
"cf_phone" => phone : String,
"cf_email" => email : String,
"cf_csv" => csv : String,
"PrimaryPermissionGroup" => primary_permission_group : PermissionGroup,
})

Expand All @@ -636,6 +640,7 @@ class InnerRange::Integriti < PlaceOS::Driver
"cf_origo" => origo : Bool,
"cf_phone" => phone : String,
"cf_email" => email : String,
"cf_csv" => csv : String,
"PrimaryPermissionGroup" => primary_permission_group : PermissionGroup, # ref only
}) do
def site_id
Expand Down Expand Up @@ -674,18 +679,39 @@ class InnerRange::Integriti < PlaceOS::Driver
end

@[PlaceOS::Driver::Security(Level::Support)]
def create_user(name : String, email : String, phone : String? = nil, site_id : String | Int64? = nil) : String
def create_user(name : String, email : String, phone : String? = nil, site_id : String | Int64? = nil, csv : String? = nil) : String
first_name, second_name = name.split(' ', 2)
user = extract_add_or_update_result(add_entry("User", UpdateFields{
"FirstName" => first_name,
"SecondName" => second_name,
"Site" => Ref.new("SiteKeyword", (site_id || default_site_id).to_s),
cf_email => email.strip.downcase,
cf_phone => phone,
cf_csv => csv,
}.compact!))
user.address.as(String)
end

@[PlaceOS::Driver::Security(Level::Support)]
def update_user_custom(
user_id : String,
email : String? = nil,
phone : String? = nil,
origo : Bool? = nil,
csv : String? = nil,
)
fields = UpdateFields{
cf_email => email.try(&.strip.downcase),
cf_phone => phone,
cf_origo => origo,
cf_csv => csv,
}.compact!

return nil if fields.empty?

extract_add_or_update_result(update_entry("User", user_id, fields))
end

# ================
# User Permissions
# ================
Expand Down
144 changes: 134 additions & 10 deletions drivers/inner_range/integriti_user_sync.cr
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,36 @@ class InnerRange::IntegritiUserSync < PlaceOS::Driver
sync_cron: "0 21 * * *",
integriti_security_group: "QG15",

_csv_sync_mappings: {
parking: {
default: "unisex with parking",
female: "Female with parking",
male: "Male with parking",
},
default: {
default: "unisex without parking",
female: "Female without parking",
male: "Male without parking",
},
},

# use these for enabling push notifications
# push_authority: "authority-GAdySsf05mL"
# push_notification_url: "https://placeos-dev.aca.im/api/engine/v2/notifications/office365"
_push_authority: "authority-GAdySsf05mL",
_push_notification_url: "https://placeos-dev.aca.im/api/engine/v2/notifications/office365",
})

accessor directory : Calendar_1
accessor integriti : Integriti_1
accessor staff_api : StaffAPI_1

@time_zone : Time::Location = Time::Location.load("GMT")

@syncing : Bool = false
@sync_mutex : Mutex = Mutex.new
@sync_requests : Int32 = 0

getter csv_sync_mappings : Hash(String, Hash(String, String))? = nil

def on_update
@time_zone_string = setting?(String, :time_zone).presence || config.control_system.not_nil!.timezone.presence || "GMT"
@time_zone = Time::Location.load(@time_zone_string)
Expand All @@ -37,6 +53,8 @@ class InnerRange::IntegritiUserSync < PlaceOS::Driver
@user_group_id = setting(String, :user_group_id)
@integriti_security_group = setting(String, :integriti_security_group)

@csv_sync_mappings = setting?(Hash(String, Hash(String, String)), :csv_sync_mappings)

@graph_group_id = nil

schedule.clear
Expand Down Expand Up @@ -86,6 +104,7 @@ class InnerRange::IntegritiUserSync < PlaceOS::Driver
protected def sync_users
# get the list of users in the integriti permissions group: (i.e. QG2)
email_to_user_id = integriti.managed_users_in_group(integriti_security_group).get.as_h.transform_values(&.as_s)
logger.debug { "Number of users in Integrity security group: #{email_to_user_id.size}" }

ad_emails = [] of String
new_users = [] of DirUser
Expand All @@ -95,10 +114,20 @@ class InnerRange::IntegritiUserSync < PlaceOS::Driver
loop do
# keep track of users that need to be created
users.each do |user|
user_email = user.email.downcase
unless user.suspended
ad_emails << user_email
new_users << user unless email_to_user_id[user_email]?
user_email = user.email.strip.downcase
user.email = user_email
username = user.username.strip.downcase
user.username = username
# handle cases where email may not equal username (and already configured in the system)
user_id = email_to_user_id[username]?
if user_id.nil? && username != user_email
if user_id = email_to_user_id[user_email]?
email_to_user_id[username] = user_id
end
end
ad_emails << username
new_users << user unless user_id
end
end

Expand All @@ -107,10 +136,13 @@ class InnerRange::IntegritiUserSync < PlaceOS::Driver

# ensure we don't blow any request limits
logger.debug { "fetching next page..." }
sleep 1
sleep 500.milliseconds
users = Array(DirUser).from_json directory.get_members(user_group_id, next_page).get.to_json
end

logger.debug { "Number of users in Integrity security group: #{email_to_user_id.size}" }
logger.debug { "Number of users in Directory security group: #{ad_emails.size}" }

# find all the users that need to be removed from the group
removed = 0
removed_errors = 0
Expand All @@ -132,17 +164,30 @@ class InnerRange::IntegritiUserSync < PlaceOS::Driver
end
end

logger.debug { "Removed #{removed} users from integrity security group" }

# add the users that need to be in the group
added = 0
added_errors = 0

new_users.each do |user|
user_email = user.email.downcase
username = user.username
user_email = user.email

begin
# check if the user exists (find by email)
users = integriti.user_id_lookup(user_email).get.as_a.map(&.as_s)
# check if the user exists (find by email and username)
users = integriti.user_id_lookup(username).get.as_a.map(&.as_s)
if users.empty?
users << integriti.create_user(user.name, user_email, user.phone).get.as_s
users = integriti.user_id_lookup(user_email).get.as_a.map(&.as_s) unless user_email == username
if users.empty?
new_user_id = integriti.create_user(user.name, username, user.phone).get.as_s
users << new_user_id
email_to_user_id[username] = new_user_id
else
# we want to update the users email address to be the username
logger.debug { "updating user email #{user_email} to #{username}" }
integriti.update_user_custom(users.first, username)
end
end

# add the user permission group
Expand All @@ -161,11 +206,17 @@ class InnerRange::IntegritiUserSync < PlaceOS::Driver
end
end

logger.debug { "Added #{added} users to integrity security group" }

# CSV array
csv_changed = sync_csv_field(ad_emails, email_to_user_id)

result = {
removed: removed,
removed_errors: removed_errors,
added: added,
added_errors: added_errors,
base_building: csv_changed,
}
logger.info { "integriti user sync results: #{result}" }
result
Expand Down Expand Up @@ -205,4 +256,77 @@ class InnerRange::IntegritiUserSync < PlaceOS::Driver
raise "google is not supported"
end
end

# ===================
# CSV Mappings
# ===================

DEFAULT_KEY = "default"

getter building_id : String { get_building_id.not_nil! }

def get_building_id
building_setting = setting?(String, :building_zone_override)
return building_setting if building_setting.presence
zone_ids = staff_api.zones(tags: "building").get.as_a.map(&.[]("id").as_s)
(zone_ids & system.zones).first
rescue error
logger.warn(exception: error) { "unable to determine building zone id" }
nil
end

protected def sync_csv_field(ad_emails : Array(String), email_to_user_id : Hash(String, String))
mappings = csv_sync_mappings
return "no CSV mappings" unless mappings && !mappings.empty?

check = mappings.keys
check.delete(DEFAULT_KEY)

possible_csv_strings = mappings.values.flat_map do |hash|
hash.values
end

logger.debug { "checking base building access for #{ad_emails.size} users" }

now = Time.local(@time_zone).at_beginning_of_day
end_of_day = 3.days.from_now.in(@time_zone).at_end_of_day
building = building_id

ad_emails.each do |email|
user_id = email_to_user_id[email]?
if user_id.nil?
logger.warn { "unable to apply CSV sync to #{email}. Possibly no matching integriti user" }
next
end

# TODO:: lookup gender
gender = DEFAULT_KEY

# check if the user has any of the required bookings
bookings = check.flat_map do |booking_type|
staff_api.query_bookings(now.to_unix, end_of_day.to_unix, zones: {building}, type: booking_type, email: email).get.as_a
end

key = if booking = bookings.first?
booking["booking_type"].as_s
else
DEFAULT_KEY
end

# ensure appropriate security group is selected
csv_security_group = mappings[key][gender]
user = integriti.user(user_id).get
csv_string = user["cf_csv"].as_s?
if csv_string != csv_security_group
if !csv_string.presence || csv_string.in?(possible_csv_strings)
# change the CSV string of this user
integriti.update_user_custom(user_id, email: email, csv: csv_security_group)
else
logger.debug { "skipping csv update for #{email} as current mapping #{csv_string} may have been manually configured" }
end
end
rescue error
logger.warn(exception: error) { "failed to check csv field for #{email}" }
end
end
end
2 changes: 1 addition & 1 deletion drivers/knx/baos_lighting.cr
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ class KNX::BaosLighting < PlaceOS::Driver
items.each do |item|
value_id = item.id
if area = @area_lookup[value_id]?
@triggers[:"area_#{area}"].each_with_index do |trigger, index|
@triggers[area].each_with_index do |trigger, index|
if value_id == trigger[0]
# We need to coerce the value
check = trigger[1]
Expand Down
3 changes: 3 additions & 0 deletions drivers/place/booking_approval_workflows.cr
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ class Place::BookingApprovalWorkflows < PlaceOS::Driver
logger.debug { "received booking event payload: #{payload}" }
booking_details = Booking.from_json payload

# Only process booking types of interest
return unless booking_details.booking_type == @booking_type

# Ignore when a bookings state is updated
return if {"process_state", "metadata_changed"}.includes?(booking_details.action)

Expand Down
2 changes: 2 additions & 0 deletions drivers/place/booking_model.cr
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class Place::Booking
property booked_by_name : String
property booked_by_email : String

getter checked_out_at : Int64? = nil
getter deleted : Bool? = nil
property checked_in : Bool { false }
property title : String?
property description : String?
Expand Down
Loading

0 comments on commit a9ce87a

Please sign in to comment.