From 653017ebdd92895f7bc7d6386a34b1cb678e1a01 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Mon, 21 May 2018 15:53:27 +1000 Subject: [PATCH 01/37] Add google library --- lib/google/admin.rb | 176 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 lib/google/admin.rb diff --git a/lib/google/admin.rb b/lib/google/admin.rb new file mode 100644 index 00000000..9ea6780e --- /dev/null +++ b/lib/google/admin.rb @@ -0,0 +1,176 @@ +require 'active_support/time' +require 'logger' +require 'googleauth' +require 'google/apis/admin_directory_v1' +require 'google/apis/calendar_v3' + +# # Define our errors +# module Google +# class Error < StandardError +# class ResourceNotFound < Error; end +# class InvalidAuthenticationToken < Error; end +# class BadRequest < Error; end +# class ErrorInvalidIdMalformed < Error; end +# class ErrorAccessDenied < Error; end +# end +# end + +class Google::Admin + TIMEZONE_MAPPING = { + "Sydney": "AUS Eastern Standard Time" + } + def initialize( + json_file_location: nil, + scopes: nil, + admin_email:, + domain:, + logger: Rails.logger + ) + @json_file_location = json_file_location || '/home/aca-apps/ruby-engine-app/keys.json' + @scopes = scopes || [ 'https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/admin.directory.user'] + @admin_email = admin_email + @domain = domain + @authorization = Google::Auth.get_application_default(scopes) + + admin_api = Google::Apis::AdminDirectoryV1 + @admin = admin_api::DirectoryService.new + @authorization.sub = @admin_email + @admin.authorization = @authorization + + calendar_api = Google::Apis::CalendarV3 + @calendar = calendar_api::CalendarService.new + @calendar.authorization = authorization + end + + def get_users(q: nil, limit: nil) + options = { + domain: @domain + } + options[:query] = q if q + options[:maxResults] = (limit || 500) + users = @admin.list_users(options) + users.users + end + + def get_user(user_id:) + options = { + domain: @domain + } + options[:query] = user_id + options[:maxResults] = 1 + users = @admin.list_users(options) + users.users[0] + end + + + def get_available_rooms(room_ids:, start_param:, end_param:) + now = Time.now + start_param = ensure_ruby_date((start_param || now)) + end_param = ensure_ruby_date((end_param || (now + 1.hour))) + + freebusy_items = [] + room_ids.each do |room| + freebusy_items << Google::Apis::CalendarV3::FreeBusyRequestItem.new(id: room) + end + + options = { + items: freebusy_items, + time_min: start_param, + time_max: end_param + } + freebusy_request = Google::Apis::CalendarV3::FreeBusyRequest.new options + + events = @calendar.query_freebusy(freebusy_request).calendars + events.delete_if {|email, resp| !resp.busy.empty? } + end + + def delete_booking(room_id:, booking_id:) + @calendar.delete_event(room_id, booking_id) + end + + def get_bookings(email, start_param, end_param) + if start_param.nil? + start_param = DateTime.now + end_param = DateTime.now + 1.hour + end + + events = calendar.list_events(email, time_min: start_param, time_max: end_param).items + end + + def create_booking(room_email:, start_param:, end_param:, subject:, description:nil, current_user:, attendees: nil, recurrence: nil, timezone:'Sydney') + description = String(description) + attendees = Array(attendees) + + # Get our room + room = Orchestrator::ControlSystem.find_by_email(room_email) + + # Ensure our start and end params are Ruby dates and format them in Graph format + start_param = ensure_ruby_date(start_param) + end_param = ensure_ruby_date(end_param) + + event_params = { + start: Google::Apis::CalendarV3::EventDateTime.new (date_time: start_param, timezone: timezone), + end: Google::Apis::CalendarV3::EventDateTime.new (date_time: end_param, timezone: timezone), + summary: subject, + description: description + } + + # Add the room as an attendee + room_attendee_options = { + resource: true, + display_name: room.name, + email: room_email, + response_status: 'accepted' + } + attendees = [ + Google::Apis::CalendarV3::EventAttendee.new room_attendee_options + ] + + # Add the attendees + attendees.map!{|a| + attendee_options = { + display_name: a[:name], + email: a[:email] + } + Google::Apis::CalendarV3::EventAttendee.new attendee_options + } + event_params[:attendees] = attendees + + # Add the current_user as an attendee + event_params[:creator] = Google::Apis::CalendarV3::Event::Creator.new { display_name: current_user.name, email: current_user.email } + + event = Google::Apis::CalendarV3::Event.new event_params + + @calendar.insert_event(room_email, event) + end + + + # Takes a date of any kind (epoch, string, time object) and returns a time object + def ensure_ruby_date(date) + if !(date.class == DateTime) + if string_is_digits(date) + + # Convert to an integer + date = date.to_i + + # If JavaScript epoch remove milliseconds + if date.to_s.length == 13 + date /= 1000 + end + + # Convert to datetimes + date = DateTime.at(date) + else + date = DateTime.parse(date) + end + end + return date + end + + # Returns true if a string is all digits (used to check for an epoch) + def string_is_digits(string) + string = string.to_s + string.scan(/\D/).empty? + end + +end From 7c3be1775725cbaf59e1175fe200aa64d42c00df Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Mon, 21 May 2018 16:13:42 +1000 Subject: [PATCH 02/37] Minor syntax fixes --- lib/google/admin.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/google/admin.rb b/lib/google/admin.rb index 9ea6780e..bf8b0497 100644 --- a/lib/google/admin.rb +++ b/lib/google/admin.rb @@ -88,7 +88,7 @@ def delete_booking(room_id:, booking_id:) @calendar.delete_event(room_id, booking_id) end - def get_bookings(email, start_param, end_param) + def get_bookings(email:, start_param:nil, end_param:nil) if start_param.nil? start_param = DateTime.now end_param = DateTime.now + 1.hour @@ -109,8 +109,8 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: end_param = ensure_ruby_date(end_param) event_params = { - start: Google::Apis::CalendarV3::EventDateTime.new (date_time: start_param, timezone: timezone), - end: Google::Apis::CalendarV3::EventDateTime.new (date_time: end_param, timezone: timezone), + start: Google::Apis::CalendarV3::EventDateTime.new { date_time: start_param, timezone: timezone }, + end: Google::Apis::CalendarV3::EventDateTime.new { date_time: end_param, timezone: timezone }, summary: subject, description: description } From b560471a9b2ef34a245acb569ce5ba64d86fc7ec Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Mon, 21 May 2018 16:17:32 +1000 Subject: [PATCH 03/37] Refactor code --- lib/google/admin.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/google/admin.rb b/lib/google/admin.rb index bf8b0497..f0099419 100644 --- a/lib/google/admin.rb +++ b/lib/google/admin.rb @@ -105,12 +105,14 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: room = Orchestrator::ControlSystem.find_by_email(room_email) # Ensure our start and end params are Ruby dates and format them in Graph format - start_param = ensure_ruby_date(start_param) - end_param = ensure_ruby_date(end_param) + start_object = ensure_ruby_date(start_param) + end_object = ensure_ruby_date(end_param) + start_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: start_object, timezone: timezone }, + end_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: end_object, timezone: timezone }, event_params = { - start: Google::Apis::CalendarV3::EventDateTime.new { date_time: start_param, timezone: timezone }, - end: Google::Apis::CalendarV3::EventDateTime.new { date_time: end_param, timezone: timezone }, + start: start_param, + end: end_param, summary: subject, description: description } From b487ab4d34416550ebbe57cd713cc09e5a83995d Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Mon, 21 May 2018 16:19:51 +1000 Subject: [PATCH 04/37] Syntax fixes --- lib/google/admin.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/google/admin.rb b/lib/google/admin.rb index f0099419..96c104a1 100644 --- a/lib/google/admin.rb +++ b/lib/google/admin.rb @@ -107,8 +107,8 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: # Ensure our start and end params are Ruby dates and format them in Graph format start_object = ensure_ruby_date(start_param) end_object = ensure_ruby_date(end_param) - start_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: start_object, timezone: timezone }, - end_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: end_object, timezone: timezone }, + start_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: start_object, timezone: timezone } + end_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: end_object, timezone: timezone } event_params = { start: start_param, From da93612e2c3ce2637b522316e0a7c1a2807bb450 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 23 May 2018 16:54:48 +1000 Subject: [PATCH 05/37] Add tested private meeting renaming --- modules/aca/exchange_booking.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/aca/exchange_booking.rb b/modules/aca/exchange_booking.rb index 73d5b27a..ac3e2c61 100644 --- a/modules/aca/exchange_booking.rb +++ b/modules/aca/exchange_booking.rb @@ -754,15 +754,18 @@ def todays_bookings subject = item[:subject] - # Set subject to private if sensitive - if ['private', 'confidential'].include?(meeting.sensitivity.downcase) + + if ['private', 'confidential'].include?(meeting.sensitivity.downcase) || subject.nil? || subject.empty? subject = "Private" + else + subject = subject[:text] end + { :Start => start, :End => ending, - :Subject => subject ? subject[:text] : "Private", + :Subject => subject, :owner => item[:organizer][:elems][0][:mailbox][:elems][0][:name][:text], :setup => 0, :breakdown => 0, From 52d5e184f97d5bcfbeceed318ea9d7ac8464a367 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 30 May 2018 09:45:58 +1000 Subject: [PATCH 06/37] Update logic for authing different APIs --- lib/google/admin.rb | 47 +++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/lib/google/admin.rb b/lib/google/admin.rb index 96c104a1..80cb2efa 100644 --- a/lib/google/admin.rb +++ b/lib/google/admin.rb @@ -30,40 +30,54 @@ def initialize( @scopes = scopes || [ 'https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/admin.directory.user'] @admin_email = admin_email @domain = domain - @authorization = Google::Auth.get_application_default(scopes) admin_api = Google::Apis::AdminDirectoryV1 @admin = admin_api::DirectoryService.new - @authorization.sub = @admin_email - @admin.authorization = @authorization calendar_api = Google::Apis::CalendarV3 @calendar = calendar_api::CalendarService.new - @calendar.authorization = authorization end def get_users(q: nil, limit: nil) + authorization = Google::Auth.get_application_default(@scopes).dup + authorization.sub = @admin_email + @admin.authorization = authorization options = { domain: @domain } options[:query] = q if q - options[:maxResults] = (limit || 500) + options[:max_results] = (limit || 500) users = @admin.list_users(options) - users.users + users.users.map do |u| + primary_email = nil + u.emails.each do |email| + primary_email = email['address'] if email['primary'] + end + { + name: u.name.full_name, + email: primary_email + } + end end def get_user(user_id:) + authorization = Google::Auth.get_application_default(@scopes).dup + authorization.sub = @admin_email + @admin.authorization = authorization options = { domain: @domain } options[:query] = user_id - options[:maxResults] = 1 + options[:max_results] = 1 users = @admin.list_users(options) users.users[0] end def get_available_rooms(room_ids:, start_param:, end_param:) + authorization = Google::Auth.get_application_default(@scopes).dup + authorization.sub = @admin_email + @calendar.authorization = authorization now = Time.now start_param = ensure_ruby_date((start_param || now)) end_param = ensure_ruby_date((end_param || (now + 1.hour))) @@ -85,10 +99,16 @@ def get_available_rooms(room_ids:, start_param:, end_param:) end def delete_booking(room_id:, booking_id:) + authorization = Google::Auth.get_application_default(@scopes).dup + authorization.sub = @admin_email + @calendar.authorization = authorization @calendar.delete_event(room_id, booking_id) end def get_bookings(email:, start_param:nil, end_param:nil) + authorization = Google::Auth.get_application_default(@scopes).dup + authorization.sub = @admin_email + @calendar.authorization = authorization if start_param.nil? start_param = DateTime.now end_param = DateTime.now + 1.hour @@ -98,6 +118,9 @@ def get_bookings(email:, start_param:nil, end_param:nil) end def create_booking(room_email:, start_param:, end_param:, subject:, description:nil, current_user:, attendees: nil, recurrence: nil, timezone:'Sydney') + authorization = Google::Auth.get_application_default(@scopes).dup + authorization.sub = @admin_email + @calendar.authorization = authorization description = String(description) attendees = Array(attendees) @@ -107,8 +130,8 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: # Ensure our start and end params are Ruby dates and format them in Graph format start_object = ensure_ruby_date(start_param) end_object = ensure_ruby_date(end_param) - start_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: start_object, timezone: timezone } - end_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: end_object, timezone: timezone } + start_param = Google::Apis::CalendarV3::EventDateTime.new({ date_time: start_object, timezone: timezone }) + end_param = Google::Apis::CalendarV3::EventDateTime.new({ date_time: end_object, timezone: timezone }) event_params = { start: start_param, @@ -125,7 +148,7 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: response_status: 'accepted' } attendees = [ - Google::Apis::CalendarV3::EventAttendee.new room_attendee_options + Google::Apis::CalendarV3::EventAttendee.new(room_attendee_options) ] # Add the attendees @@ -134,12 +157,12 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: display_name: a[:name], email: a[:email] } - Google::Apis::CalendarV3::EventAttendee.new attendee_options + Google::Apis::CalendarV3::EventAttendee.new(attendee_options) } event_params[:attendees] = attendees # Add the current_user as an attendee - event_params[:creator] = Google::Apis::CalendarV3::Event::Creator.new { display_name: current_user.name, email: current_user.email } + event_params[:creator] = Google::Apis::CalendarV3::Event::Creator.new({ display_name: current_user.name, email: current_user.email }) event = Google::Apis::CalendarV3::Event.new event_params From ad6b536d7b00617b3f8e4e8e75fd78b2fc2718a4 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 30 May 2018 11:38:42 +1000 Subject: [PATCH 07/37] Fix repsonse from get_bookings --- lib/google/admin.rb | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/lib/google/admin.rb b/lib/google/admin.rb index 80cb2efa..afe2881b 100644 --- a/lib/google/admin.rb +++ b/lib/google/admin.rb @@ -114,7 +114,20 @@ def get_bookings(email:, start_param:nil, end_param:nil) end_param = DateTime.now + 1.hour end - events = calendar.list_events(email, time_min: start_param, time_max: end_param).items + events = @calendar.list_events(email, time_min: start_param.iso8601, time_max: end_param.iso8601).items + events.map do |event| + { + start_date: event.start.date_time.to_i * 1000, + end_date: event.end.date_time.to_i * 1000, + Start: event.start.date_time.utc.iso8601, + End: event.end.date_time.utc.iso8601, + subject: event.summary, + title: event.summary, + description: event.description, + attendees: event.attendees.map {|a| {name: a.display_name, email: a.email} }, + id: event.id + } + end end def create_booking(room_email:, start_param:, end_param:, subject:, description:nil, current_user:, attendees: nil, recurrence: nil, timezone:'Sydney') @@ -140,17 +153,6 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: description: description } - # Add the room as an attendee - room_attendee_options = { - resource: true, - display_name: room.name, - email: room_email, - response_status: 'accepted' - } - attendees = [ - Google::Apis::CalendarV3::EventAttendee.new(room_attendee_options) - ] - # Add the attendees attendees.map!{|a| attendee_options = { @@ -159,6 +161,17 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: } Google::Apis::CalendarV3::EventAttendee.new(attendee_options) } + + # Add the room as an attendee + room_attendee_options = { + resource: true, + display_name: room.name, + email: room_email, + response_status: 'accepted' + } + + attendees.push(Google::Apis::CalendarV3::EventAttendee.new(room_attendee_options)) + event_params[:attendees] = attendees # Add the current_user as an attendee @@ -184,9 +197,9 @@ def ensure_ruby_date(date) end # Convert to datetimes - date = DateTime.at(date) + date = DateTime.parse(Time.at(date).to_s) else - date = DateTime.parse(date) + date = DateTime.parse(date.to_s) end end return date From 2abfccd723536ee884c1be5ddafb0e14f54581b5 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 30 May 2018 12:13:36 +1000 Subject: [PATCH 08/37] Update google booking driver --- modules/aca/google_refresh_booking.rb | 60 ++++++++++++++++----------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 19a1e589..444594ac 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -82,7 +82,7 @@ class Aca::GoogleRefreshBooking ews_room: 'room@email.address', # Optional EWS for creating and removing bookings - google_organiser_location: 'attendees' + google_organiser_location: 'attendees', google_client_id: '', google_secret: '', google_redirect_uri: '', @@ -161,6 +161,7 @@ def on_update @google_secret = setting(:google_client_secret) @google_redirect_uri = setting(:google_redirect_uri) @google_refresh_token = setting(:google_refresh_token) + @google_admin_email = setting(:google_admin_email) @google_scope = setting(:google_scope) @google_room = (setting(:google_room) || system.email) # supports: SMTP, PSMTP, SID, UPN (user principle name) @@ -323,21 +324,31 @@ def fetch_bookings(*args) # client = OAuth2::Client.new(@google_client_id, @google_secret, {site: @google_site, token_url: @google_token_url}) - options = { - client_id: @google_client_id, - client_secret: @google_secret, - scope: @google_scope, - redirect_uri: @google_redirect_uri, - refresh_token: @google_refresh_token, - grant_type: "refresh_token" - } - - authorization = Google::Auth::UserRefreshCredentials.new options - - Calendar = Google::Apis::CalendarV3 - calendar = Calendar::CalendarService.new + # options = { + # client_id: @google_client_id, + # client_secret: @google_secret, + # scope: @google_scope, + # redirect_uri: @google_redirect_uri, + # refresh_token: @google_refresh_token, + # grant_type: "refresh_token" + # } + # logger.info "AUTHORIZING WITH OPTIONS:" + # STDERR.puts "AUTHORIZING WITH OPTIONS:" + # STDERR.puts options + # logger.info options + # STDERR.puts @google_admin_email + # logger.info @google_admin_email + # STDERR.flush + + # authorization = Google::Auth::UserRefreshCredentials.new options + + authorization = Google::Auth.get_application_default(@google_scope).dup + authorization.sub = @google_admin_email + + calendar_api = Google::Apis::CalendarV3 + calendar = calendar_api::CalendarService.new calendar.authorization = authorization - events = calendar.list_events(system.email) + events = calendar.list_events(system.email, time_min: Time.now.midnight.iso8601, time_max: Time.now.tomorrow.midnight.iso8601).items task { todays_bookings(events) @@ -567,16 +578,12 @@ def todays_bookings(events) results = [] events.each{|event| - start_time = ActiveSupport::TimeZone.new('UTC').parse(event.start_time).iso8601[0..18] - end_time = ActiveSupport::TimeZone.new('UTC').parse(event.end_time).iso8601[0..18] - - organiser = "" results.push({ - :Start => start_time, - :End => end_time, - :Subject => event.title, - :owner => organiser + :Start => event.start.date_time.utc.iso8601, + :End => event.end.date_time.utc.iso8601, + :Subject => event.summary, + :owner => event.organizer.display_name # :setup => 0, # :breakdown => 0 }) @@ -587,5 +594,10 @@ def todays_bookings(events) results end - # ======================================= + + def log(data) + STDERR.puts data + logger.info data + STDERR.flush + end end From af68d25d161168071037a626eabe7ae109acd8ac Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Fri, 22 Jun 2018 12:33:27 +1000 Subject: [PATCH 09/37] Fix timezone in booking module --- modules/aca/google_booking.rb | 643 -------------------------- modules/aca/google_refresh_booking.rb | 3 +- 2 files changed, 2 insertions(+), 644 deletions(-) delete mode 100644 modules/aca/google_booking.rb diff --git a/modules/aca/google_booking.rb b/modules/aca/google_booking.rb deleted file mode 100644 index a411d391..00000000 --- a/modules/aca/google_booking.rb +++ /dev/null @@ -1,643 +0,0 @@ -# encoding: ASCII-8BIT - -require 'uv-rays' - -# For rounding up to the nearest 15min -# See: http://stackoverflow.com/questions/449271/how-to-round-a-time-down-to-the-nearest-15-minutes-in-ruby -class ActiveSupport::TimeWithZone - def ceil(seconds = 60) - return self if seconds.zero? - Time.at(((self - self.utc_offset).to_f / seconds).ceil * seconds).in_time_zone + self.utc_offset - end -end - - -module Aca; end - -# NOTE:: Requires Settings: -# ======================== -# room_alias: 'rs.au.syd.L16Aitken', -# building: 'DP3', -# level: '16' - -class Aca::GoogleBooking - include ::Orchestrator::Constants - EMAIL_CACHE = ::Concurrent::Map.new - CAN_LDAP = begin - require 'net/ldap' - true - rescue LoadError - false - end - CAN_GOOGLE = begin - require 'google_calendar' - true - rescue LoadError - false - end - - - descriptive_name 'Google Room Bookings' - generic_name :Bookings - implements :logic - - - # The room we are interested in - default_settings({ - update_every: '2m', - - # Moved to System or Zone Setting - # cancel_meeting_after: 900 - - # Card reader IDs if we want to listen for swipe events - card_readers: ['reader_id_1', 'reader_id_2'], - - # Optional LDAP creds for looking up emails - ldap_creds: { - host: 'ldap.org.com', - port: 636, - encryption: { - method: :simple_tls, - tls_options: { - verify_mode: 0 - } - }, - auth: { - method: :simple, - username: 'service account', - password: 'password' - } - }, - tree_base: "ou=User,ou=Accounts,dc=org,dc=com", - - # Optional EWS for creating and removing bookings - ews_creds: [ - 'https://company.com/EWS/Exchange.asmx', - 'service account', - 'password', - { http_opts: { ssl_verify_mode: 0 } } - ], - ews_room: 'room@email.address', - - # Optional EWS for creating and removing bookings - google_organiser_location: 'attendees' - # google_client_id: ENV["GOOGLE_APP_CLIENT_ID"], - # google_secret: ENV["GOOGLE_APP_CLIENT_SECRET"], - # google_scope: ENV['GOOGLE_APP_SCOPE'], - # google_site: ENV["GOOGLE_APP_SITE"], - # google_token_url: ENV["GOOGLE_APP_TOKEN_URL"], - # google_options: { - # site: ENV["GOOGLE_APP_SITE"], - # token_url: ENV["GOOGLE_APP_TOKEN_URL"] - # }, - # google_room: 'room@email.address' - }) - - - def on_load - on_update - end - - def on_update - self[:swiped] ||= 0 - @last_swipe_at = 0 - @use_act_as = setting(:use_act_as) - - self[:hide_all] = setting(:hide_all) || false - self[:touch_enabled] = setting(:touch_enabled) || false - self[:name] = self[:room_name] = setting(:room_name) || system.name - - self[:control_url] = setting(:booking_control_url) || system.config.support_url - self[:booking_controls] = setting(:booking_controls) - self[:booking_catering] = setting(:booking_catering) - self[:booking_hide_details] = setting(:booking_hide_details) - self[:booking_hide_availability] = setting(:booking_hide_availability) - self[:booking_hide_user] = setting(:booking_hide_user) - self[:booking_hide_description] = setting(:booking_hide_description) - self[:booking_hide_timeline] = setting(:booking_hide_timeline) - - # Skype join button available 2min before the start of a meeting - @skype_start_offset = setting(:skype_start_offset) || 120 - - # Skype join button not available in the last 8min of a meeting - @skype_end_offset = setting(:skype_end_offset) || 480 - - # Because restarting the modules results in a 'swipe' of the last read card - ignore_first_swipe = true - - # Is there catering available for this room? - self[:catering] = setting(:catering_system_id) - if self[:catering] - self[:menu] = setting(:menu) - end - - # Do we want to look up the users email address? - if CAN_LDAP - @ldap_creds = setting(:ldap_creds) - if @ldap_creds - encrypt = @ldap_creds[:encryption] - encrypt[:method] = encrypt[:method].to_sym if encrypt && encrypt[:method] - @tree_base = setting(:tree_base) - @ldap_user = @ldap_creds.delete :auth - end - else - logger.warn "net/ldap gem not available" if setting(:ldap_creds) - end - - # Do we want to use exchange web services to manage bookings - if CAN_GOOGLE - logger.debug "Setting GOOGLE" - # :client_id => "237647939013-k5c6ubsa1ddt9o861pinpm2d0hom644j.apps.googleusercontent.com", - # :client_secret => "h0vEuCHMLNH5fTwXj3S8PvkE", - # :calendar => "aca@googexcite.cloud", - # :redirect_url => "http://google.aca.im" - - @google_organiser_location = setting(:google_organiser_location) - @google_client_id = setting(:google_client_id) - @google_secret = setting(:google_client_secret) - @google_redirect_uri = setting(:google_redirect_uri) - @google_refresh_token = setting(:google_refresh_token) - @google_room = (setting(:google_room) || system.email) - # supports: SMTP, PSMTP, SID, UPN (user principle name) - # NOTE:: Using UPN we might be able to remove the LDAP requirement - @google_connect_type = (setting(:google_connect_type) || :SMTP).to_sym - @timezone = setting(:room_timezone) - else - logger.warn "oauth2 gem not available" if setting(:google_creds) - end - - # Load the last known values (persisted to the DB) - self[:waiter_status] = (setting(:waiter_status) || :idle).to_sym - self[:waiter_call] = self[:waiter_status] != :idle - - self[:catering_status] = setting(:last_catering_status) || {} - self[:order_status] = :idle - - self[:last_meeting_started] = setting(:last_meeting_started) - self[:cancel_meeting_after] = setting(:cancel_meeting_after) - - - # unsubscribe to all swipe IDs if any are subscribed - if @subs.present? - @subs.each do |sub| - unsubscribe(sub) - end - - @subs = nil - end - - # Are there any swipe card integrations - if system.exists? :Security - readers = setting(:card_readers) - if readers.present? - security = system[:Security] - - readers = readers.is_a?(Array) ? readers : [readers] - sys = system - @subs = [] - readers.each do |id| - @subs << sys.subscribe(:Security, 1, id.to_s) do |notice| - if ignore_first_swipe - ignore_first_swipe = false - else - swipe_occured(notice.value) - end - end - end - end - end - - fetch_bookings - schedule.clear - schedule.every(setting(:update_every) || '5m') { fetch_bookings } - end - - - def set_light_status(status) - lightbar = system[:StatusLight] - return if lightbar.nil? - - case status.to_sym - when :unavailable - lightbar.colour(:red) - when :available - lightbar.colour(:green) - when :pending - lightbar.colour(:orange) - else - lightbar.colour(:off) - end - end - - - # ====================================== - # Waiter call information - # ====================================== - def waiter_call(state) - status = is_affirmative?(state) - - self[:waiter_call] = status - - # Used to highlight the service button - if status - self[:waiter_status] = :pending - else - self[:waiter_status] = :idle - end - - define_setting(:waiter_status, self[:waiter_status]) - end - - def call_acknowledged - self[:waiter_status] = :accepted - define_setting(:waiter_status, self[:waiter_status]) - end - - - # ====================================== - # Catering Management - # ====================================== - def catering_status(details) - self[:catering_status] = details - - # We'll turn off the green light on the waiter call button - if self[:waiter_status] != :idle && details[:progress] == 'visited' - self[:waiter_call] = false - self[:waiter_status] = :idle - define_setting(:waiter_status, self[:waiter_status]) - end - - define_setting(:last_catering_status, details) - end - - def commit_order(order_details) - self[:order_status] = :pending - status = self[:catering_status] - - if status && status[:progress] == 'visited' - status = status.dup - status[:progress] = 'cleaned' - self[:catering_status] = status - end - - if self[:catering] - sys = system - @oid ||= 1 - systems(self[:catering])[:Orders].add_order({ - id: "#{sys.id}_#{@oid}", - created_at: Time.now.to_i, - room_id: sys.id, - room_name: sys.name, - order: order_details - }) - end - end - - def order_accepted - self[:order_status] = :accepted - end - - def order_complete - self[:order_status] = :idle - end - - - - # ====================================== - # ROOM BOOKINGS: - # ====================================== - def fetch_bookings(*args) - - - # @google_organiser_location = setting(:google_organiser_location) - # @google_client_id = setting(:google_client_id) - # @google_secret = setting(:google_client_secret) - # @google_redirect_uri = setting(:google_redirect_uri) - # @google_refresh_token = setting(:google_refresh_token) - # @google_room = (setting(:google_room) || system.email) - - # client = OAuth2::Client.new(@google_client_id, @google_secret, {site: @google_site, token_url: @google_token_url}) - - # Create an instance of the calendar. - begin - cal = Google::Calendar.new(:client_id => @google_client_id, - :client_secret => @google_client_secret, - :calendar => @google_room, - :redirect_url => @google_redirect_uri # this is what Google uses for 'applications' - ) - cal.connection.login_with_refresh_token(@google_refresh_token) - - events = cal.find_events_in_range(Time.now.midnight, Time.now.tomorrow.midnight, {max_results: 2500}) - rescue Exception => e - logger.debug e.message - logger.debug e.backtrace.inspect - raise e - end - - task { - todays_bookings(events) - }.then(proc { |bookings| - self[:today] = bookings - }, proc { |e| logger.print_error(e, 'error fetching bookings') }) - end - - - # ====================================== - # Meeting Helper Functions - # ====================================== - - def start_meeting(meeting_ref) - self[:last_meeting_started] = meeting_ref - self[:meeting_pending] = meeting_ref - self[:meeting_ending] = false - self[:meeting_pending_notice] = false - define_setting(:last_meeting_started, meeting_ref) - end - - def cancel_meeting(start_time) - task { - delete_ews_booking (start_time / 1000).to_i - }.then(proc { |count| - logger.debug { "successfully removed #{count} bookings" } - - self[:last_meeting_started] = start_time - self[:meeting_pending] = start_time - self[:meeting_ending] = false - self[:meeting_pending_notice] = false - }, proc { |error| - logger.print_error error, 'removing ews booking' - }) - end - - # If last meeting started !== meeting pending then - # we'll show a warning on the in room touch panel - def set_meeting_pending(meeting_ref) - self[:meeting_ending] = false - self[:meeting_pending] = meeting_ref - self[:meeting_pending_notice] = true - end - - # Meeting ending warning indicator - # (When meeting_ending !== last_meeting_started then the warning hasn't been cleared) - # The warning is only displayed when meeting_ending === true - def set_end_meeting_warning(meeting_ref = nil, extendable = false) - if self[:last_meeting_started].nil? || self[:meeting_ending] != (meeting_ref || self[:last_meeting_started]) - self[:meeting_ending] = true - - # Allows meeting ending warnings in all rooms - self[:last_meeting_started] = meeting_ref if meeting_ref - self[:meeting_canbe_extended] = extendable - end - end - - def clear_end_meeting_warning - self[:meeting_ending] = self[:last_meeting_started] - end - # --------- - - def create_meeting(options) - # Check that the required params exist - required_fields = ["start", "end"] - check = required_fields - options.keys - if check != [] - # There are missing required fields - logger.info "Required fields missing: #{check}" - raise "missing required fields: #{check}" - end - - logger.debug "Passed Room options: --------------------" - logger.debug options - logger.debug options.to_json - - req_params = {} - req_params[:room_email] = @ews_room - req_params[:organizer] = options[:user_email] - req_params[:subject] = options[:title] - req_params[:start_time] = Time.at(options[:start].to_i / 1000).utc.iso8601.chop - req_params[:end_time] = Time.at(options[:end].to_i / 1000).utc.iso8601.chop - - - # TODO:: Catch error for booking failure - begin - id = make_google_booking req_params - rescue Exception => e - logger.debug e.message - logger.debug e.backtrace.inspect - raise e - end - - - logger.debug { "successfully created booking: #{id}" } - "Ok" - end - - - protected - - - def swipe_occured(info) - # Update the user details - @last_swipe_at = Time.now.to_i - self[:fullname] = "#{info[:firstname]} #{info[:lastname]}" - self[:username] = info[:staff_id] - email = nil - - if self[:username] && @ldap_creds - email = EMAIL_CACHE[self[:username]] - if email - set_email(email) - logger.debug { "email #{email} found in cache" } - else - # Cache username here as self[:username] might change while we - # looking up the previous username - username = self[:username] - - logger.debug { "looking up email for #{username} - #{self[:fullname]}" } - task { - ldap_lookup_email username - }.then do |email| - if email - logger.debug { "email #{email} found in LDAP" } - EMAIL_CACHE[username] = email - set_email(email) - else - logger.warn "no email found in LDAP for #{username}" - set_email nil - end - end - end - else - logger.warn "no staff ID for user #{self[:fullname]}" - set_email nil - end - end - - def set_email(email) - self[:email] = email - self[:swiped] += 1 - end - - # ==================================== - # LDAP lookup to occur in worker thread - # ==================================== - def ldap_lookup_email(username) - email = EMAIL_CACHE[username] - return email if email - - ldap = Net::LDAP.new @ldap_creds - ldap.authenticate @ldap_user[:username], @ldap_user[:password] if @ldap_user - - login_filter = Net::LDAP::Filter.eq('sAMAccountName', username) - object_filter = Net::LDAP::Filter.eq('objectClass', '*') - treebase = @tree_base - search_attributes = ['mail'] - - email = nil - ldap.bind - ldap.search({ - base: treebase, - filter: object_filter & login_filter, - attributes: search_attributes - }) do |entry| - email = get_attr(entry, 'mail') - end - - # Returns email as a promise - EMAIL_CACHE[username] = email - email - end - - def get_attr(entry, attr_name) - if attr_name != "" && attr_name != nil - entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] - end - end - # ==================================== - - - # ======================================= - # EWS Requests to occur in a worker thread - # ======================================= - def make_google_booking(user_email: nil, subject: 'On the spot booking', room_email:, start_time:, end_time:, organizer:) - - booking_data = { - subject: subject, - start: { dateTime: start_time, timeZone: "UTC" }, - end: { dateTime: end_time, timeZone: "UTC" }, - location: { displayName: @google_room, locationEmailAddress: @google_room }, - attendees: [ emailAddress: { address: organizer, name: "User"}] - }.to_json - - logger.debug "Creating booking:" - logger.debug booking_data - - client = OAuth2::Client.new(@google_client_id, @google_secret, {site: @google_site, token_url: @google_token_url}) - - begin - access_token = client.client_credentials.get_token({ - :scope => @google_scope - # :client_secret => ENV["GOOGLE_APP_CLIENT_SECRET"], - # :client_id => ENV["GOOGLE_APP_CLIENT_ID"] - }).token - rescue Exception => e - logger.debug e.message - logger.debug e.backtrace.inspect - raise e - end - - - # Set out domain, endpoint and content type - domain = 'https://graph.microsoft.com' - host = 'graph.microsoft.com' - endpoint = "/v1.0/users/#{@google_room}/events" - content_type = 'application/json;odata.metadata=minimal;odata.streaming=true' - - # Create the request URI and config - google_api = UV::HttpEndpoint.new(domain, tls_options: {host_name: host}) - headers = { - 'Authorization' => "Bearer #{access_token}", - 'Content-Type' => content_type - } - - # Make the request - response = google_api.post(path: "#{domain}#{endpoint}", body: booking_data, headers: headers).value - - logger.debug response.body - logger.debug response.to_json - logger.debug JSON.parse(response.body)['id'] - - id = JSON.parse(response.body)['id'] - - # Return the booking IDs - id - end - - def delete_ews_booking(delete_at) - now = Time.now - if @timezone - start = now.in_time_zone(@timezone).midnight - ending = now.in_time_zone(@timezone).tomorrow.midnight - else - start = now.midnight - ending = now.tomorrow.midnight - end - - count = 0 - - cli = Viewpoint::EWSClient.new(*@ews_creds) - - if @use_act_as - # TODO:: think this line can be removed?? - delete_at = Time.parse(delete_at.to_s).to_i - - opts = {} - opts[:act_as] = @ews_room if @ews_room - - folder = cli.get_folder(:calendar, opts) - items = folder.items({:calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) - else - cli.set_impersonation(Viewpoint::EWS::ConnectingSID[@ews_connect_type], @ews_room) if @ews_room - items = cli.find_items({:folder_id => :calendar, :calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) - end - - items.each do |meeting| - meeting_time = Time.parse(meeting.ews_item[:start][:text]) - - # Remove any meetings that match the start time provided - if meeting_time.to_i == delete_at - meeting.delete!(:recycle, send_meeting_cancellations: 'SendOnlyToAll') - count += 1 - end - end - - # Return the number of meetings removed - count - end - - def todays_bookings(events) - - events.each{|event| - - # start_time = Time.parse(event['start']['dateTime']).utc.iso8601[0..18] + 'Z' - # end_time = Time.parse(event['end']['dateTime']).utc.iso8601[0..18] + 'Z' - start_time = ActiveSupport::TimeZone.new('UTC').parse(event.start_time).iso8601[0..18] - end_time = ActiveSupport::TimeZone.new('UTC').parse(event.end_time).iso8601[0..18] - - - results.push({ - :Start => start_time, - :End => end_time, - :Subject => event.title, - :owner => organizer - # :setup => 0, - # :breakdown => 0 - }) - } - - logger.info "Got #{results.length} results!" - logger.info results.to_json - - results - end - # ======================================= -end diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 444594ac..8871c2ff 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -164,6 +164,7 @@ def on_update @google_admin_email = setting(:google_admin_email) @google_scope = setting(:google_scope) @google_room = (setting(:google_room) || system.email) + @google_timezone = setting(:timezone) || "Sydney" # supports: SMTP, PSMTP, SID, UPN (user principle name) # NOTE:: Using UPN we might be able to remove the LDAP requirement @google_connect_type = (setting(:google_connect_type) || :SMTP).to_sym @@ -348,7 +349,7 @@ def fetch_bookings(*args) calendar_api = Google::Apis::CalendarV3 calendar = calendar_api::CalendarService.new calendar.authorization = authorization - events = calendar.list_events(system.email, time_min: Time.now.midnight.iso8601, time_max: Time.now.tomorrow.midnight.iso8601).items + events = calendar.list_events(system.email, time_min: ActiveSupport::TimeZone.new(@google_timezone).now.midnight.iso8601, time_max: ActiveSupport::TimeZone.new(@google_timezone).now.tomorrow.midnight.iso8601).items task { todays_bookings(events) From ca4b41cd2a86ed24d736191c69a77c9821b174b9 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Mon, 25 Jun 2018 10:39:20 +1000 Subject: [PATCH 10/37] Remove need for current_user on booking creation --- lib/microsoft/exchange.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/microsoft/exchange.rb b/lib/microsoft/exchange.rb index 54ddda42..e69cb157 100644 --- a/lib/microsoft/exchange.rb +++ b/lib/microsoft/exchange.rb @@ -265,7 +265,7 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: # booking[:body] = description booking[:end] = Time.at(end_param.to_i / 1000).utc.iso8601.chop booking[:required_attendees] = [{ - attendee: { mailbox: { email_address: current_user.email } } + attendee: { mailbox: { email_address: current_user[:email] } } }] attendees.each do |attendee| if attendee.class != String @@ -285,7 +285,7 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: if use_act_as folder = @ews_client.get_folder(:calendar, { act_as: room_email }) else - @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], current_user.email) + @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], current_user[:email]) folder = @ews_client.get_folder(:calendar) end appointment = folder.create_item(booking) From fd0134ddee3626094fa951ce39188ecc2fa83201 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 17 Oct 2018 13:19:58 +1100 Subject: [PATCH 11/37] Update google module to use library --- modules/aca/google_refresh_booking.rb | 67 +++++++++------------------ 1 file changed, 21 insertions(+), 46 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 8871c2ff..1d6eceb3 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -214,6 +214,11 @@ def on_update end end + @google_client = ::Google::Admin.new({ + admin_email: ENV['GOOGLE_ADMIN_EMAIL'], + domain: ENV['GOOGLE_DOMAIN'] + }) + fetch_bookings schedule.clear schedule.every(setting(:update_every) || '5m') { fetch_bookings } @@ -522,54 +527,24 @@ def get_attr(entry, attr_name) # EWS Requests to occur in a worker thread # ======================================= def make_google_booking(user_email: nil, subject: 'On the spot booking', room_email:, start_time:, end_time:, organizer:) - - booking_data = { - subject: subject, - start: { dateTime: start_time, timeZone: "UTC" }, - end: { dateTime: end_time, timeZone: "UTC" }, - location: { displayName: @google_room, locationEmailAddress: @google_room }, - attendees: [ emailAddress: { address: organizer, name: "User"}] - }.to_json - - logger.debug "Creating booking:" - logger.debug booking_data - - client = OAuth2::Client.new(@google_client_id, @google_secret, {site: @google_site, token_url: @google_token_url}) - - begin - access_token = client.client_credentials.get_token({ - :scope => @google_scope - # :client_secret => ENV["GOOGLE_APP_CLIENT_SECRET"], - # :client_id => ENV["GOOGLE_APP_CLIENT_ID"] - }).token - rescue Exception => e - logger.debug e.message - logger.debug e.backtrace.inspect - raise e + if start_time > 1500000000000 + start_time = (start_time.to_i / 1000).to_i + end_time = (end_time.to_i / 1000).to_i + else + start_time = start_time + end_time = end_time end - - # Set out domain, endpoint and content type - domain = 'https://graph.microsoft.com' - host = 'graph.microsoft.com' - endpoint = "/v1.0/users/#{@google_room}/events" - content_type = 'application/json;odata.metadata=minimal;odata.streaming=true' - - # Create the request URI and config - google_api = UV::HttpEndpoint.new(domain, tls_options: {host_name: host}) - headers = { - 'Authorization' => "Bearer #{access_token}", - 'Content-Type' => content_type - } - - # Make the request - response = google_api.post(path: "#{domain}#{endpoint}", body: booking_data, headers: headers).value - - logger.debug response.body - logger.debug response.to_json - logger.debug JSON.parse(response.body)['id'] - - id = JSON.parse(response.body)['id'] + results = @google.create_booking({ + room_email: room_email, + start_param: start_time, + end_param: end_time, + subject: subject, + current_user: (organizer || nil) + timezone: ENV['TIMEZONE'] || 'Sydney' + }) + + id = results['id'] # Return the booking IDs id From fd91cd65540a7f2f068ed2bfcfe35f18d0d73f59 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Sat, 17 Nov 2018 11:41:44 +1100 Subject: [PATCH 12/37] Only impersonate if admin email exists --- lib/google/admin.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/google/admin.rb b/lib/google/admin.rb index afe2881b..66280796 100644 --- a/lib/google/admin.rb +++ b/lib/google/admin.rb @@ -40,7 +40,7 @@ def initialize( def get_users(q: nil, limit: nil) authorization = Google::Auth.get_application_default(@scopes).dup - authorization.sub = @admin_email + authorization.sub = @admin_email if @admin_email @admin.authorization = authorization options = { domain: @domain @@ -62,7 +62,7 @@ def get_users(q: nil, limit: nil) def get_user(user_id:) authorization = Google::Auth.get_application_default(@scopes).dup - authorization.sub = @admin_email + authorization.sub = @admin_email if @admin_email @admin.authorization = authorization options = { domain: @domain @@ -76,7 +76,7 @@ def get_user(user_id:) def get_available_rooms(room_ids:, start_param:, end_param:) authorization = Google::Auth.get_application_default(@scopes).dup - authorization.sub = @admin_email + authorization.sub = @admin_email if @admin_email @calendar.authorization = authorization now = Time.now start_param = ensure_ruby_date((start_param || now)) @@ -100,14 +100,14 @@ def get_available_rooms(room_ids:, start_param:, end_param:) def delete_booking(room_id:, booking_id:) authorization = Google::Auth.get_application_default(@scopes).dup - authorization.sub = @admin_email + authorization.sub = @admin_email if @admin_email @calendar.authorization = authorization @calendar.delete_event(room_id, booking_id) end def get_bookings(email:, start_param:nil, end_param:nil) authorization = Google::Auth.get_application_default(@scopes).dup - authorization.sub = @admin_email + authorization.sub = @admin_email if @admin_email @calendar.authorization = authorization if start_param.nil? start_param = DateTime.now @@ -132,7 +132,7 @@ def get_bookings(email:, start_param:nil, end_param:nil) def create_booking(room_email:, start_param:, end_param:, subject:, description:nil, current_user:, attendees: nil, recurrence: nil, timezone:'Sydney') authorization = Google::Auth.get_application_default(@scopes).dup - authorization.sub = @admin_email + authorization.sub = @admin_email if @admin_email @calendar.authorization = authorization description = String(description) attendees = Array(attendees) From 6e518d3a9c37a829f0fa36111bee4d69675a3e24 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 28 Nov 2018 16:19:28 +1100 Subject: [PATCH 13/37] Fix fetch --- modules/aca/google_refresh_booking.rb | 49 +++++++++------------------ 1 file changed, 16 insertions(+), 33 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 1d6eceb3..c9cee45b 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -314,13 +314,7 @@ def order_complete end - - # ====================================== - # ROOM BOOKINGS: - # ====================================== - def fetch_bookings(*args) - - + def fetch_bookings(*args) # @google_organiser_location = setting(:google_organiser_location) # @google_client_id = setting(:google_client_id) # @google_secret = setting(:google_client_secret) @@ -330,31 +324,21 @@ def fetch_bookings(*args) # client = OAuth2::Client.new(@google_client_id, @google_secret, {site: @google_site, token_url: @google_token_url}) - # options = { - # client_id: @google_client_id, - # client_secret: @google_secret, - # scope: @google_scope, - # redirect_uri: @google_redirect_uri, - # refresh_token: @google_refresh_token, - # grant_type: "refresh_token" - # } - # logger.info "AUTHORIZING WITH OPTIONS:" - # STDERR.puts "AUTHORIZING WITH OPTIONS:" - # STDERR.puts options - # logger.info options - # STDERR.puts @google_admin_email - # logger.info @google_admin_email - # STDERR.flush - - # authorization = Google::Auth::UserRefreshCredentials.new options - - authorization = Google::Auth.get_application_default(@google_scope).dup - authorization.sub = @google_admin_email - - calendar_api = Google::Apis::CalendarV3 - calendar = calendar_api::CalendarService.new + options = { + client_id: @google_client_id, + client_secret: @google_secret, + scope: @google_scope, + redirect_uri: @google_redirect_uri, + refresh_token: @google_refresh_token, + grant_type: "refresh_token" + } + + authorization = Google::Auth::UserRefreshCredentials.new options + + Calendar = Google::Apis::CalendarV3 + calendar = Calendar::CalendarService.new calendar.authorization = authorization - events = calendar.list_events(system.email, time_min: ActiveSupport::TimeZone.new(@google_timezone).now.midnight.iso8601, time_max: ActiveSupport::TimeZone.new(@google_timezone).now.tomorrow.midnight.iso8601).items + events = calendar.list_events(system.email) task { todays_bookings(events) @@ -362,8 +346,7 @@ def fetch_bookings(*args) self[:today] = bookings }, proc { |e| logger.print_error(e, 'error fetching bookings') }) end - - + # ====================================== # Meeting Helper Functions # ====================================== From 23b53a8069b8903194643b17845a3527c3dd63a8 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 28 Nov 2018 16:20:25 +1100 Subject: [PATCH 14/37] Fix fetch --- modules/aca/google_refresh_booking.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index c9cee45b..8e1f44c7 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -346,7 +346,7 @@ def fetch_bookings(*args) self[:today] = bookings }, proc { |e| logger.print_error(e, 'error fetching bookings') }) end - + # ====================================== # Meeting Helper Functions # ====================================== @@ -523,7 +523,7 @@ def make_google_booking(user_email: nil, subject: 'On the spot booking', room_em start_param: start_time, end_param: end_time, subject: subject, - current_user: (organizer || nil) + current_user: (organizer || nil), timezone: ENV['TIMEZONE'] || 'Sydney' }) From c32d1e02383e09531c9514aa95dbd441330ad88d Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 28 Nov 2018 16:28:38 +1100 Subject: [PATCH 15/37] Fix fetch --- modules/aca/google_refresh_booking.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 8e1f44c7..df721e15 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -214,8 +214,8 @@ def on_update end end - @google_client = ::Google::Admin.new({ - admin_email: ENV['GOOGLE_ADMIN_EMAIL'], + @google = ::Google::Admin.new({ + admin_email: nil, domain: ENV['GOOGLE_DOMAIN'] }) @@ -339,6 +339,8 @@ def fetch_bookings(*args) calendar = Calendar::CalendarService.new calendar.authorization = authorization events = calendar.list_events(system.email) + + events = @google.get_bookings(email: system.email, start_param: DateTime.now - 1.day, end_param: DateTime.now + 1.day ) task { todays_bookings(events) From 15697a2ca0a08d0150c95cd9c723a2ee4579c42e Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 28 Nov 2018 16:29:18 +1100 Subject: [PATCH 16/37] Fix fetch --- modules/aca/google_refresh_booking.rb | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index df721e15..d47e2a62 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -315,31 +315,6 @@ def order_complete def fetch_bookings(*args) - # @google_organiser_location = setting(:google_organiser_location) - # @google_client_id = setting(:google_client_id) - # @google_secret = setting(:google_client_secret) - # @google_redirect_uri = setting(:google_redirect_uri) - # @google_refresh_token = setting(:google_refresh_token) - # @google_room = (setting(:google_room) || system.email) - - # client = OAuth2::Client.new(@google_client_id, @google_secret, {site: @google_site, token_url: @google_token_url}) - - options = { - client_id: @google_client_id, - client_secret: @google_secret, - scope: @google_scope, - redirect_uri: @google_redirect_uri, - refresh_token: @google_refresh_token, - grant_type: "refresh_token" - } - - authorization = Google::Auth::UserRefreshCredentials.new options - - Calendar = Google::Apis::CalendarV3 - calendar = Calendar::CalendarService.new - calendar.authorization = authorization - events = calendar.list_events(system.email) - events = @google.get_bookings(email: system.email, start_param: DateTime.now - 1.day, end_param: DateTime.now + 1.day ) task { From 4dc4acf3b24348b66a07814f1e14944eea8634d7 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 28 Nov 2018 16:32:00 +1100 Subject: [PATCH 17/37] Fix fetch --- modules/aca/google_refresh_booking.rb | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index d47e2a62..107be065 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -139,7 +139,7 @@ def on_update if CAN_LDAP @ldap_creds = setting(:ldap_creds) if @ldap_creds - encrypt = @ldap_creds[:encryption] + encrypt = @ldap_creds[:encryption] encrypt[:method] = encrypt[:method].to_sym if encrypt && encrypt[:method] @tree_base = setting(:tree_base) @ldap_user = @ldap_creds.delete :auth @@ -511,22 +511,9 @@ def make_google_booking(user_email: nil, subject: 'On the spot booking', room_em end def todays_bookings(events) - results = [] - - events.each{|event| - - results.push({ - :Start => event.start.date_time.utc.iso8601, - :End => event.end.date_time.utc.iso8601, - :Subject => event.summary, - :owner => event.organizer.display_name - # :setup => 0, - # :breakdown => 0 - }) - } - logger.info "Got #{results.length} results!" - logger.info results.to_json + logger.info "Got #{events.length} results!" + logger.info events.to_json results end From 4c48efc5a6638a3548731f6aa42e392061ee7095 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 28 Nov 2018 16:33:11 +1100 Subject: [PATCH 18/37] Fix fetch --- modules/aca/google_refresh_booking.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 107be065..8dbe682a 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -515,7 +515,7 @@ def todays_bookings(events) logger.info "Got #{events.length} results!" logger.info events.to_json - results + events end def log(data) From 1ee0d79459f66d61d1ce021a89498c398ebf930f Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Mon, 13 May 2019 10:12:41 +1000 Subject: [PATCH 19/37] Update exchange related files for google branch --- lib/microsoft/exchange.rb | 282 ++++++++++++++++++++------------ modules/aca/exchange_booking.rb | 99 ++++++++--- 2 files changed, 253 insertions(+), 128 deletions(-) diff --git a/lib/microsoft/exchange.rb b/lib/microsoft/exchange.rb index e69cb157..33419d60 100644 --- a/lib/microsoft/exchange.rb +++ b/lib/microsoft/exchange.rb @@ -12,6 +12,7 @@ def initialize( service_account_email:, service_account_password:, internet_proxy:nil, + hide_all_day_bookings:false, logger: Rails.logger ) begin @@ -24,10 +25,11 @@ def initialize( @service_account_email = service_account_email @service_account_password = service_account_password @internet_proxy = internet_proxy + @hide_all_day_bookings = hide_all_day_bookings ews_opts = { http_opts: { ssl_verify_mode: 0 } } ews_opts[:http_opts][:http_client] = @internet_proxy if @internet_proxy STDERR.puts '--------------- NEW CLIENT CREATED --------------' - STDERR.puts "At URL: #{@ews_url} with email: #{@service_account_email} and password #{@service_account_password}" + STDERR.puts "At URL: #{@ews_url} with email: #{@service_account_email}" STDERR.puts '-------------------------------------------------' @ews_client ||= Viewpoint::EWSClient.new @ews_url, @service_account_email, @service_account_password, ews_opts end @@ -124,9 +126,7 @@ def find_time(cal_event, time) start_time = nil elems.each do |item| if item[time] - Time.use_zone 'Sydney' do - start_time = Time.parse(item[time][:text]) - end + start_time = ActiveSupport::TimeZone.new("UTC").parse(item[time][:text]) break end end @@ -135,60 +135,67 @@ def find_time(cal_event, time) def get_available_rooms(rooms:, start_time:, end_time:) free_rooms = [] - + start_time = start_time.utc + end_time = end_time.utc STDERR.puts "Getting available rooms with" STDERR.puts start_time STDERR.puts end_time - STDERR.flush - - # Get booking data for all rooms between time range bounds - user_free_busy = @ews_client.get_user_availability(rooms, - start_time: start_time, - end_time: end_time, - requested_view: :detailed, - time_zone: { - bias: -600, - standard_time: { - bias: -60, - time: "03:00:00", - day_order: 1, - month: 10, - day_of_week: 'Sunday' - }, - daylight_time: { - bias: 0, - time: "02:00:00", - day_order: 1, - month: 4, - day_of_week: 'Sunday' + STDERR.flush + + rooms.each_slice(30).each do |room_subset| + + # Get booking data for all rooms between time range bounds + user_free_busy = @ews_client.get_user_availability(room_subset, + start_time: start_time, + end_time: end_time, + requested_view: :detailed, + time_zone: { + bias: -0, + standard_time: { + bias: 0, + time: "03:00:00", + day_order: 1, + month: 10, + day_of_week: 'Sunday' + }, + daylight_time: { + bias: 0, + time: "02:00:00", + day_order: 1, + month: 4, + day_of_week: 'Sunday' + } } - } - ) + ) - user_free_busy.body[0][:get_user_availability_response][:elems][0][:free_busy_response_array][:elems].each_with_index {|r,index| - # Remove meta data (business hours and response type) - resp = r[:free_busy_response][:elems][1][:free_busy_view][:elems].delete_if { |item| item[:free_busy_view_type] || item[:working_hours] } + user_free_busy.body[0][:get_user_availability_response][:elems][0][:free_busy_response_array][:elems].each_with_index {|r,index| + # Remove meta data (business hours and response type) + resp = r[:free_busy_response][:elems][1][:free_busy_view][:elems].delete_if { |item| item[:free_busy_view_type] || item[:working_hours] } - # Back to back meetings still show up so we need to remove these from the results - if resp.length == 1 - resp = resp[0][:calendar_event_array][:elems] + # Back to back meetings still show up so we need to remove these from the results + if resp.length == 1 + resp = resp[0][:calendar_event_array][:elems] - if resp.length == 0 - free_rooms.push(rooms[index]) - elsif resp.length == 1 - free_rooms.push(rooms[index]) if find_time(resp[0], :start_time) == end_time - free_rooms.push(rooms[index]) if find_time(resp[0], :end_time) == start_time + if resp.length == 0 + free_rooms.push(room_subset[index]) + elsif resp.length == 1 + free_rooms.push(room_subset[index]) if find_time(resp[0], :start_time).to_i == end_time.to_i + free_rooms.push(room_subset[index]) if find_time(resp[0], :end_time).to_i == start_time.to_i + end + elsif resp.length == 0 + # If response length is 0 then the room is free + free_rooms.push(room_subset[index]) end - elsif resp.length == 0 - # If response length is 0 then the room is free - free_rooms.push(rooms[index]) - end - } + } + end + free_rooms end def get_bookings(email:, start_param:DateTime.now.midnight, end_param:(DateTime.now.midnight + 2.days), use_act_as: false) - begin + begin + # Get all the room emails + room_emails = Orchestrator::ControlSystem.all.to_a.map { |sys| sys.email } if [Integer, String].include?(start_param.class) start_param = DateTime.parse(Time.at(start_param.to_i / 1000).to_s) end_param = DateTime.parse(Time.at(end_param.to_i / 1000).to_s) @@ -207,35 +214,49 @@ def get_bookings(email:, start_param:DateTime.now.midnight, end_param:(DateTime. # events = @ews_client.get_item(:calendar, opts = {act_as: email}).items events.each{|event| event.get_all_properties! + internal_domain = Mail::Address.new(event.organizer.email).domain booking = {} booking[:subject] = event.subject booking[:title] = event.subject + booking[:id] = event.id # booking[:start_date] = event.start.utc.iso8601 # booking[:end_date] = event.end.utc.iso8601 - booking[:start_date] = event.start.to_i * 1000 - booking[:end_date] = event.end.to_i * 1000 + booking[:start] = event.start.to_i * 1000 + booking[:end] = event.end.to_i * 1000 booking[:body] = event.body booking[:organiser] = { name: event.organizer.name, email: event.organizer.email } - booking[:attendees] = event.required_attendees.map {|attendee| + booking[:attendees] = event.required_attendees.map {|attendee| + if room_emails.include?(attendee.email) + booking[:room_id] = attendee.email + next + end + email_domain = Mail::Address.new(attendee.email).domain { name: attendee.name, - email: attendee.email + email: attendee.email, + visitor: (internal_domain != email_domain), + organisation: attendee.email.split('@')[1..-1].join("").split('.')[0].capitalize } - } if event.required_attendees + }.compact if event.required_attendees + if @hide_all_day_bookings + STDERR.puts "SKIPPING #{event.subject}" + STDERR.flush + next if event.end.to_time - event.start.to_time > 86399 + end bookings.push(booking) } bookings - rescue Exception => msg - STDERR.puts msg - STDERR.flush - return [] - end + rescue Exception => msg + STDERR.puts msg + STDERR.flush + return [] + end end - def create_booking(room_email:, start_param:, end_param:, subject:, description:nil, current_user:, attendees: nil, timezone:'Sydney', use_act_as: false) + def create_booking(room_email:, start_param:, end_param:, subject:, description:nil, current_user:, attendees: nil, timezone:'Sydney', permission: 'impersonation', mailbox_location: 'user') STDERR.puts "CREATING NEW BOOKING IN LIBRARY" STDERR.puts "room_email is #{room_email}" STDERR.puts "start_param is #{start_param}" @@ -251,9 +272,14 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: booking = {} + + # Allow for naming of subject or title booking[:subject] = subject booking[:title] = subject - # booking[:location] = room_email + booking[:location] = Orchestrator::ControlSystem.find_by_email(room_email).name + + + # Set the room email as a resource booking[:resources] = [{ attendee: { mailbox: { @@ -261,13 +287,28 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: } } }] - booking[:start] = Time.at(start_param.to_i / 1000).utc.iso8601.chop - # booking[:body] = description - booking[:end] = Time.at(end_param.to_i / 1000).utc.iso8601.chop + + # start_object = Time.at(start_param.to_i) + # end_object = Time.at(end_param.to_i) + + start_object = ensure_ruby_date(start_param.to_i) + end_object = ensure_ruby_date(end_param.to_i) + + + # Add start and end times + booking[:start] = start_object.utc.iso8601.chop + booking[:end] = end_object.utc.iso8601.chop + + # Add the current user passed in as an attendee + mailbox = { email_address: current_user.email } + mailbox[:name] = current_user.name if current_user.name booking[:required_attendees] = [{ - attendee: { mailbox: { email_address: current_user[:email] } } + attendee: { mailbox: mailbox } }] + + # Add the attendees attendees.each do |attendee| + # If we don't have an array of emails then it's an object in the form {email: "a@b.com", name: "Blahman Blahson"} if attendee.class != String attendee = attendee['email'] end @@ -275,61 +316,98 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: attendee: { mailbox: { email_address: attendee}} }) end - booking[:required_attendees].push({ - attendee: { mailbox: { email_address: room_email}} - }) + + # Add the room as an attendee (it seems as if some clients require this and others don't) + booking[:required_attendees].push({ attendee: { mailbox: { email_address: room_email}}}) booking[:body] = description + + # A little debugging STDERR.puts "MAKING REQUEST WITH" STDERR.puts booking STDERR.flush - if use_act_as - folder = @ews_client.get_folder(:calendar, { act_as: room_email }) - else - @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], current_user[:email]) + + if mailbox_location == 'user' + mailbox = current_user.email + elsif mailbox_location == 'room' + mailbox = room_email + end + + # Determine whether to use delegation, impersonation or neither + if permission == 'delegation' + folder = @ews_client.get_folder(:calendar, { act_as: mailbox }) + elsif permission == 'impersonation' + @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], mailbox) + folder = @ews_client.get_folder(:calendar) + elsif permission == 'none' || permission.nil? folder = @ews_client.get_folder(:calendar) end + + # Create the booking and return data relating to it appointment = folder.create_item(booking) { id: appointment.id, start: start_param, end: end_param, attendees: attendees, - # location: Orchestrator::ControlSystem.find_by_email(room_email).name, subject: subject } end - def update_booking(booking_id:, room_id:, start_param:nil, end_param:nil, subject:nil, description:nil, attendees:nil, timezone:'Sydney') - # We will always need a room and endpoint passed in - room = Orchestrator::ControlSystem.find(room_id) - endpoint = "/v1.0/users/#{room.email}/events/#{booking_id}" - event = {} - event[:subject] = subject if subject - - event[:start] = { - dateTime: start_param, - timeZone: TIMEZONE_MAPPING[timezone.to_sym] - } if start_param - - event[:end] = { - dateTime: end_param, - timeZone: TIMEZONE_MAPPING[timezone.to_sym] - } if end_param - - event[:body] = { - contentType: 'html', - content: description - } if description - - # Let's assume that the request has the current user and room as an attendee already - event[:attendees] = attendees.map{|a| - { emailAddress: { - address: a[:email], - name: a[:name] - } } - } if attendees - - response = JSON.parse(graph_request('patch', endpoint, event).to_json.value.body)['value'] + def update_booking(booking_id:, room_email:nil, start_param:nil, end_param:nil, subject:nil, description:nil, current_user:nil, attendees: nil, timezone:'Sydney', permission: 'impersonation', mailbox_location: 'user') + + event = @ews_client.get_item(booking_id) + booking = {} + + # Add attendees if passed in + attendees = Array(attendees) + attendees.each do |attendee| + if attendee.class != String + attendee = attendee['email'] + end + booking[:required_attendees] ||= [] + booking[:required_attendees].push({ + attendee: { mailbox: { email_address: attendee}} + }) + end if attendees && !attendees.empty? + + # Add subject or title + booking[:subject] = subject if subject + booking[:title] = subject if subject + + # Add location + booking[:location] = Orchestrator::ControlSystem.find_by_email(room_email).name if room_email + + # Add new times if passed + booking[:start] = Time.at(start_param.to_i / 1000).utc.iso8601.chop if start_param + booking[:end] = Time.at(end_param.to_i / 1000).utc.iso8601.chop if end_param + + if mailbox_location == 'user' + mailbox = current_user.email + elsif mailbox_location == 'room' + mailbox = room_email + end + + if permission == 'impersonation' + @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], mailbox) + end + + new_booking = event.update_item!(booking) + + + { + id: new_booking.id, + start: new_booking.start, + end: new_booking.end, + attendees: new_booking.required_attendees, + subject: new_booking.subject + } + end + + def delete_booking(booking_id:, mailbox:) + @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], mailbox) + booking = @ews_client.get_item(booking_id) + booking.delete!(:recycle, send_meeting_cancellations: "SendOnlyToAll") + 200 end # Takes a date of any kind (epoch, string, time object) and returns a time object @@ -360,4 +438,4 @@ def string_is_digits(string) string.scan(/\D/).empty? end -end +end \ No newline at end of file diff --git a/modules/aca/exchange_booking.rb b/modules/aca/exchange_booking.rb index ac3e2c61..d322de28 100644 --- a/modules/aca/exchange_booking.rb +++ b/modules/aca/exchange_booking.rb @@ -99,6 +99,8 @@ def on_update self[:description] = setting(:description) || nil self[:title] = setting(:title) || nil self[:timeout] = setting(:timeout) || false + self[:booking_endable] = setting(:booking_endable) || false + self[:booking_ask_end] = setting(:booking_ask_end) || false self[:control_url] = setting(:booking_control_url) || system.config.support_url self[:booking_controls] = setting(:booking_controls) @@ -113,9 +115,12 @@ def on_update self[:booking_min_duration] = setting(:booking_min_duration) self[:booking_disable_future] = setting(:booking_disable_future) self[:booking_max_duration] = setting(:booking_max_duration) + self[:booking_cancel_timeout] = setting(:booking_cancel_timeout) + self[:hide_all_day_bookings] = setting(:hide_all_day_bookings) self[:timeout] = setting(:timeout) self[:arrow_direction] = setting(:arrow_direction) self[:icon] = setting(:icon) + @hide_all_day_bookings = Boolean(setting(:hide_all_day_bookings)) @check_meeting_ending = setting(:check_meeting_ending) # seconds before meeting ending @extend_meeting_by = setting(:extend_meeting_by) || 15.minutes.to_i @@ -204,8 +209,12 @@ def on_update end schedule.clear - schedule.in(rand(10000)) { fetch_bookings } - schedule.every((setting(:update_every) || 120000).to_i + rand(10000)) { fetch_bookings } + schedule.in(rand(10000)) { + fetch_bookings(true) + } + schedule.every((setting(:update_every) || 120000).to_i + rand(10000)) { + fetch_bookings + } end @@ -349,10 +358,11 @@ def order_complete # ====================================== # ROOM BOOKINGS: # ====================================== - def fetch_bookings(*args) + def fetch_bookings(first=false) logger.debug { "looking up todays emails for #{@ews_room}" } + skype_exists = system.exists?(:Skype) task { - todays_bookings + todays_bookings(first, skype_exists) }.then(proc { |bookings| self[:today] = bookings if @check_meeting_ending @@ -374,7 +384,7 @@ def start_meeting(meeting_ref) define_setting(:last_meeting_started, meeting_ref) end - def cancel_meeting(start_time) + def cancel_meeting(start_time, reason = "timeout") task { if start_time.class == Integer delete_ews_booking (start_time / 1000).to_i @@ -384,10 +394,10 @@ def cancel_meeting(start_time) delete_ews_booking start_time.to_i end }.then(proc { |count| - logger.debug { "successfully removed #{count} bookings" } + logger.warn { "successfully removed #{count} bookings due to #{reason}" } - self[:last_meeting_started] = start_time - self[:meeting_pending] = start_time + self[:last_meeting_started] = 0 + self[:meeting_pending] = 0 self[:meeting_ending] = false self[:meeting_pending_notice] = false @@ -401,6 +411,7 @@ def cancel_meeting(start_time) # If last meeting started !== meeting pending then # we'll show a warning on the in room touch panel def set_meeting_pending(meeting_ref) + return if self[:last_meeting_started] == meeting_ref self[:meeting_ending] = false self[:meeting_pending] = meeting_ref self[:meeting_pending_notice] = true @@ -509,7 +520,24 @@ def extend_meeting return false unless starting ending = starting + @extend_meeting_by - create_meeting start: starting * 1000, end: ending * 1000, title: @current_meeting_title + create_meeting(start: starting * 1000, end: ending * 1000, title: @current_meeting_title).then do + start_meeting(starting * 1000) + end + end + + def send_email(title, body, to) + task { + cli = Viewpoint::EWSClient.new(*@ews_creds) + opts = {} + + if @use_act_as + opts[:act_as] = @ews_room if @ews_room + else + cli.set_impersonation(Viewpoint::EWS::ConnectingSID[@ews_connect_type], @ews_room) if @ews_room + end + + cli.send_message subject: title, body: body, to_recipients: to + } end @@ -663,6 +691,8 @@ def delete_ews_booking(delete_at) # Remove any meetings that match the start time provided if meeting_time.to_i == delete_at + # new_booking = meeting.update_item!({ end: Time.now.utc.iso8601.chop }) + meeting.delete!(:recycle, send_meeting_cancellations: 'SendOnlyToAll') count += 1 end @@ -672,7 +702,7 @@ def delete_ews_booking(delete_at) count end - def todays_bookings + def todays_bookings(first=false, skype_exists=false) now = Time.now if @timezone start = now.in_time_zone(@timezone).midnight @@ -683,7 +713,7 @@ def todays_bookings end cli = Viewpoint::EWSClient.new(*@ews_creds) - + if @use_act_as opts = {} @@ -696,11 +726,11 @@ def todays_bookings items = cli.find_items({:folder_id => :calendar, :calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) end - skype_exists = set_skype_url = system.exists?(:Skype) + set_skype_url = skype_exists set_skype_url = true if @force_skype_extract now_int = now.to_i - items.select! { |booking| !booking.cancelled? } + # items.select! { |booking| !booking.cancelled? } results = items.collect do |meeting| item = meeting.ews_item start = item[:start][:text] @@ -716,14 +746,28 @@ def todays_bookings end_integer = real_end.to_i - @skype_end_offset if now_int > start_integer && now_int < end_integer + if first + self[:last_meeting_started] = start_integer * 1000 + end meeting.get_all_properties! if meeting.body - # Lync: - # Skype: - body_parts = meeting.body.split('OutJoinLink"') - if body_parts.length > 1 - links = body_parts[-1].split('"').select { |link| link.start_with?('https://') } + match = meeting.body.match(/\"pexip\:\/\/(.+?)\"/) + if match + set_skype_url = false + self[:pexip_meeting_uid] = start_integer + self[:pexip_meeting_address] = match[1] + else + links = URI.extract(meeting.body).select { |url| url.start_with?('https://meet.lync') } + if links.empty? + # Lync: + # Skype: + body_parts = meeting.body.split('OutJoinLink"') + if body_parts.length > 1 + links = body_parts[-1].split('"').select { |link| link.start_with?('https://') } + end + end + if links[0].present? if now_int > join_integer self[:can_join_skype_meeting] = true @@ -732,6 +776,7 @@ def todays_bookings self[:skype_meeting_pending] = true end set_skype_url = false + self[:skype_meeting_address] = links[0] system[:Skype].set_uri(links[0]) if skype_exists end end @@ -749,19 +794,19 @@ def todays_bookings logger.debug { item.inspect } + if @hide_all_day_bookings + next if Time.parse(ending) - Time.parse(start) > 86399 + end + # Prevent connections handing with TIME_WAIT # cli.ews.connection.httpcli.reset_all - subject = item[:subject] - - - if ['private', 'confidential'].include?(meeting.sensitivity.downcase) || subject.nil? || subject.empty? - subject = "Private" + if ["Private", "Confidential"].include?(meeting.sensitivity) + subject = meeting.sensitivity else - subject = subject[:text] + subject = item[:subject][:text] end - { :Start => start, :End => ending, @@ -773,8 +818,10 @@ def todays_bookings :end_epoch => real_end.to_i } end + results.compact! if set_skype_url + self[:pexip_meeting_address] = nil self[:can_join_skype_meeting] = false self[:skype_meeting_pending] = false system[:Skype].set_uri(nil) if skype_exists @@ -783,4 +830,4 @@ def todays_bookings results end # ======================================= -end +end \ No newline at end of file From 85ee787c9dccfdc388138cccc41f199004d43623 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Mon, 21 May 2018 15:53:27 +1000 Subject: [PATCH 20/37] Add google library --- lib/google/admin.rb | 176 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 lib/google/admin.rb diff --git a/lib/google/admin.rb b/lib/google/admin.rb new file mode 100644 index 00000000..9ea6780e --- /dev/null +++ b/lib/google/admin.rb @@ -0,0 +1,176 @@ +require 'active_support/time' +require 'logger' +require 'googleauth' +require 'google/apis/admin_directory_v1' +require 'google/apis/calendar_v3' + +# # Define our errors +# module Google +# class Error < StandardError +# class ResourceNotFound < Error; end +# class InvalidAuthenticationToken < Error; end +# class BadRequest < Error; end +# class ErrorInvalidIdMalformed < Error; end +# class ErrorAccessDenied < Error; end +# end +# end + +class Google::Admin + TIMEZONE_MAPPING = { + "Sydney": "AUS Eastern Standard Time" + } + def initialize( + json_file_location: nil, + scopes: nil, + admin_email:, + domain:, + logger: Rails.logger + ) + @json_file_location = json_file_location || '/home/aca-apps/ruby-engine-app/keys.json' + @scopes = scopes || [ 'https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/admin.directory.user'] + @admin_email = admin_email + @domain = domain + @authorization = Google::Auth.get_application_default(scopes) + + admin_api = Google::Apis::AdminDirectoryV1 + @admin = admin_api::DirectoryService.new + @authorization.sub = @admin_email + @admin.authorization = @authorization + + calendar_api = Google::Apis::CalendarV3 + @calendar = calendar_api::CalendarService.new + @calendar.authorization = authorization + end + + def get_users(q: nil, limit: nil) + options = { + domain: @domain + } + options[:query] = q if q + options[:maxResults] = (limit || 500) + users = @admin.list_users(options) + users.users + end + + def get_user(user_id:) + options = { + domain: @domain + } + options[:query] = user_id + options[:maxResults] = 1 + users = @admin.list_users(options) + users.users[0] + end + + + def get_available_rooms(room_ids:, start_param:, end_param:) + now = Time.now + start_param = ensure_ruby_date((start_param || now)) + end_param = ensure_ruby_date((end_param || (now + 1.hour))) + + freebusy_items = [] + room_ids.each do |room| + freebusy_items << Google::Apis::CalendarV3::FreeBusyRequestItem.new(id: room) + end + + options = { + items: freebusy_items, + time_min: start_param, + time_max: end_param + } + freebusy_request = Google::Apis::CalendarV3::FreeBusyRequest.new options + + events = @calendar.query_freebusy(freebusy_request).calendars + events.delete_if {|email, resp| !resp.busy.empty? } + end + + def delete_booking(room_id:, booking_id:) + @calendar.delete_event(room_id, booking_id) + end + + def get_bookings(email, start_param, end_param) + if start_param.nil? + start_param = DateTime.now + end_param = DateTime.now + 1.hour + end + + events = calendar.list_events(email, time_min: start_param, time_max: end_param).items + end + + def create_booking(room_email:, start_param:, end_param:, subject:, description:nil, current_user:, attendees: nil, recurrence: nil, timezone:'Sydney') + description = String(description) + attendees = Array(attendees) + + # Get our room + room = Orchestrator::ControlSystem.find_by_email(room_email) + + # Ensure our start and end params are Ruby dates and format them in Graph format + start_param = ensure_ruby_date(start_param) + end_param = ensure_ruby_date(end_param) + + event_params = { + start: Google::Apis::CalendarV3::EventDateTime.new (date_time: start_param, timezone: timezone), + end: Google::Apis::CalendarV3::EventDateTime.new (date_time: end_param, timezone: timezone), + summary: subject, + description: description + } + + # Add the room as an attendee + room_attendee_options = { + resource: true, + display_name: room.name, + email: room_email, + response_status: 'accepted' + } + attendees = [ + Google::Apis::CalendarV3::EventAttendee.new room_attendee_options + ] + + # Add the attendees + attendees.map!{|a| + attendee_options = { + display_name: a[:name], + email: a[:email] + } + Google::Apis::CalendarV3::EventAttendee.new attendee_options + } + event_params[:attendees] = attendees + + # Add the current_user as an attendee + event_params[:creator] = Google::Apis::CalendarV3::Event::Creator.new { display_name: current_user.name, email: current_user.email } + + event = Google::Apis::CalendarV3::Event.new event_params + + @calendar.insert_event(room_email, event) + end + + + # Takes a date of any kind (epoch, string, time object) and returns a time object + def ensure_ruby_date(date) + if !(date.class == DateTime) + if string_is_digits(date) + + # Convert to an integer + date = date.to_i + + # If JavaScript epoch remove milliseconds + if date.to_s.length == 13 + date /= 1000 + end + + # Convert to datetimes + date = DateTime.at(date) + else + date = DateTime.parse(date) + end + end + return date + end + + # Returns true if a string is all digits (used to check for an epoch) + def string_is_digits(string) + string = string.to_s + string.scan(/\D/).empty? + end + +end From 9dabe8a6d83e574502cc34fbf67bf34599186935 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Mon, 21 May 2018 16:13:42 +1000 Subject: [PATCH 21/37] Minor syntax fixes --- lib/google/admin.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/google/admin.rb b/lib/google/admin.rb index 9ea6780e..bf8b0497 100644 --- a/lib/google/admin.rb +++ b/lib/google/admin.rb @@ -88,7 +88,7 @@ def delete_booking(room_id:, booking_id:) @calendar.delete_event(room_id, booking_id) end - def get_bookings(email, start_param, end_param) + def get_bookings(email:, start_param:nil, end_param:nil) if start_param.nil? start_param = DateTime.now end_param = DateTime.now + 1.hour @@ -109,8 +109,8 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: end_param = ensure_ruby_date(end_param) event_params = { - start: Google::Apis::CalendarV3::EventDateTime.new (date_time: start_param, timezone: timezone), - end: Google::Apis::CalendarV3::EventDateTime.new (date_time: end_param, timezone: timezone), + start: Google::Apis::CalendarV3::EventDateTime.new { date_time: start_param, timezone: timezone }, + end: Google::Apis::CalendarV3::EventDateTime.new { date_time: end_param, timezone: timezone }, summary: subject, description: description } From 84fc4ef1b05ea44263fa5dd11b4022c0a36b5f51 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Mon, 21 May 2018 16:17:32 +1000 Subject: [PATCH 22/37] Refactor code --- lib/google/admin.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/google/admin.rb b/lib/google/admin.rb index bf8b0497..f0099419 100644 --- a/lib/google/admin.rb +++ b/lib/google/admin.rb @@ -105,12 +105,14 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: room = Orchestrator::ControlSystem.find_by_email(room_email) # Ensure our start and end params are Ruby dates and format them in Graph format - start_param = ensure_ruby_date(start_param) - end_param = ensure_ruby_date(end_param) + start_object = ensure_ruby_date(start_param) + end_object = ensure_ruby_date(end_param) + start_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: start_object, timezone: timezone }, + end_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: end_object, timezone: timezone }, event_params = { - start: Google::Apis::CalendarV3::EventDateTime.new { date_time: start_param, timezone: timezone }, - end: Google::Apis::CalendarV3::EventDateTime.new { date_time: end_param, timezone: timezone }, + start: start_param, + end: end_param, summary: subject, description: description } From b6e813b56017d1d7b3d13afc3b71acafdf67b11f Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Mon, 21 May 2018 16:19:51 +1000 Subject: [PATCH 23/37] Syntax fixes --- lib/google/admin.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/google/admin.rb b/lib/google/admin.rb index f0099419..96c104a1 100644 --- a/lib/google/admin.rb +++ b/lib/google/admin.rb @@ -107,8 +107,8 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: # Ensure our start and end params are Ruby dates and format them in Graph format start_object = ensure_ruby_date(start_param) end_object = ensure_ruby_date(end_param) - start_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: start_object, timezone: timezone }, - end_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: end_object, timezone: timezone }, + start_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: start_object, timezone: timezone } + end_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: end_object, timezone: timezone } event_params = { start: start_param, From 19eec0fb1f8965783860a6f57b03ae81aaf5e9a1 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 23 May 2018 16:54:48 +1000 Subject: [PATCH 24/37] Add tested private meeting renaming --- lib/microsoft/exchange.rb | 441 ----------------- modules/aca/exchange_booking.rb | 833 -------------------------------- 2 files changed, 1274 deletions(-) delete mode 100644 lib/microsoft/exchange.rb delete mode 100644 modules/aca/exchange_booking.rb diff --git a/lib/microsoft/exchange.rb b/lib/microsoft/exchange.rb deleted file mode 100644 index c364ee95..00000000 --- a/lib/microsoft/exchange.rb +++ /dev/null @@ -1,441 +0,0 @@ -require 'active_support/time' -require 'logger' - -module Microsoft; end - -class Microsoft::Exchange - TIMEZONE_MAPPING = { - "Sydney": "AUS Eastern Standard Time" - } - def initialize( - ews_url:, - service_account_email:, - service_account_password:, - internet_proxy:nil, - hide_all_day_bookings:false, - logger: Rails.logger - ) - begin - require 'viewpoint2' - rescue LoadError - STDERR.puts 'VIEWPOINT NOT PRESENT' - STDERR.flush - end - @ews_url = ews_url - @service_account_email = service_account_email - @service_account_password = service_account_password - @internet_proxy = internet_proxy - @hide_all_day_bookings = hide_all_day_bookings - ews_opts = { http_opts: { ssl_verify_mode: 0 } } - ews_opts[:http_opts][:http_client] = @internet_proxy if @internet_proxy - STDERR.puts '--------------- NEW CLIENT CREATED --------------' - STDERR.puts "At URL: #{@ews_url} with email: #{@service_account_email}" - STDERR.puts '-------------------------------------------------' - @ews_client ||= Viewpoint::EWSClient.new @ews_url, @service_account_email, @service_account_password, ews_opts - end - - def basic_text(field, name) - field[name][:text] - end - - def close - @ews_client.ews.connection.httpcli.reset_all - end - - def username(field, name=nil) - username = field[:email_addresses][:elems][0][:entry][:text].split("@")[0] - if ['smt','sip'].include?(username.downcase[0..2]) - username = username.gsub(/SMTP:|SIP:|sip:|smtp:/,'') - else - username = field[:email_addresses][:elems][-1][:entry][:text].split("@")[0] - if ['smt','sip'].include?(username.downcase[0..2]) - username = username.gsub(/SMTP:|SIP:|sip:|smtp:/,'') - else - username = field[:email_addresses][:elems][1][:entry][:text].split("@")[0] - if ['smt','sip'].include?(username.downcase[0..2]) - username = username.gsub(/SMTP:|SIP:|sip:|smtp:/,'') - else - username = nil - end - end - end - username - end - - def phone_list(field, name=nil) - phone = nil - field[:phone_numbers][:elems].each do |entry| - type = entry[:entry][:attribs][:key] - text = entry[:entry][:text] - - next unless text.present? - - if type == "MobilePhone" - return text - elsif type == "BusinessPhone" || phone.present? - phone = text - end - end - phone - end - - - def get_users(q: nil, limit: nil) - ews_users = @ews_client.search_contacts(q) - users = [] - fields = { - display_name: 'name:basic_text', - phone_numbers: 'phone:phone_list', - culture: 'locale:basic_text', - department: 'department:basic_text', - email_addresses: 'id:username' - } - keys = fields.keys - ews_users.each do |user| - begin - output = {} - user[:resolution][:elems][1][:contact][:elems].each do |field| - key = field.keys[0] - if keys.include?(key) - splits = fields[key].split(':') - output[splits[0]] = self.__send__(splits[1], field, key) - end - end - if output['name'].nil? - output['name'] = user[:resolution][:elems][0][:mailbox][:elems][0][:name][:text] - end - output['email'] = user[:resolution][:elems][0][:mailbox][:elems][1][:email_address][:text] - users.push(output) - rescue => e - STDERR.puts "GOT USER WITHOUT EMAIL" - STDERR.puts user - STDERR.flush - end - end - limit ||= users.length - limit = limit.to_i - 1 - return users[0..limit.to_i] - end - - def get_user(user_id:) - get_users(q: user_id, limit: 1)[0] - end - - def find_time(cal_event, time) - elems = cal_event[:calendar_event][:elems] - start_time = nil - elems.each do |item| - if item[time] - start_time = ActiveSupport::TimeZone.new("UTC").parse(item[time][:text]) - break - end - end - start_time - end - - def get_available_rooms(rooms:, start_time:, end_time:) - free_rooms = [] - start_time = start_time.utc - end_time = end_time.utc - STDERR.puts "Getting available rooms with" - STDERR.puts start_time - STDERR.puts end_time - STDERR.flush - - rooms.each_slice(30).each do |room_subset| - - # Get booking data for all rooms between time range bounds - user_free_busy = @ews_client.get_user_availability(room_subset, - start_time: start_time, - end_time: end_time, - requested_view: :detailed, - time_zone: { - bias: -0, - standard_time: { - bias: 0, - time: "03:00:00", - day_order: 1, - month: 10, - day_of_week: 'Sunday' - }, - daylight_time: { - bias: 0, - time: "02:00:00", - day_order: 1, - month: 4, - day_of_week: 'Sunday' - } - } - ) - - user_free_busy.body[0][:get_user_availability_response][:elems][0][:free_busy_response_array][:elems].each_with_index {|r,index| - # Remove meta data (business hours and response type) - resp = r[:free_busy_response][:elems][1][:free_busy_view][:elems].delete_if { |item| item[:free_busy_view_type] || item[:working_hours] } - - # Back to back meetings still show up so we need to remove these from the results - if resp.length == 1 - resp = resp[0][:calendar_event_array][:elems] - - if resp.length == 0 - free_rooms.push(room_subset[index]) - elsif resp.length == 1 - free_rooms.push(room_subset[index]) if find_time(resp[0], :start_time).to_i == end_time.to_i - free_rooms.push(room_subset[index]) if find_time(resp[0], :end_time).to_i == start_time.to_i - end - elsif resp.length == 0 - # If response length is 0 then the room is free - free_rooms.push(room_subset[index]) - end - } - end - - free_rooms - end - - def get_bookings(email:, start_param:DateTime.now.midnight, end_param:(DateTime.now.midnight + 2.days), use_act_as: false) - begin - # Get all the room emails - room_emails = Orchestrator::ControlSystem.all.to_a.map { |sys| sys.email } - if [Integer, String].include?(start_param.class) - start_param = DateTime.parse(Time.at(start_param.to_i / 1000).to_s) - end_param = DateTime.parse(Time.at(end_param.to_i / 1000).to_s) - end - STDERR.puts '---------------- GETTING BOOKINGS ---------------' - STDERR.puts "At email: #{email} with start: #{start_param} and end: #{end_param}" - STDERR.puts '-------------------------------------------------' - bookings = [] - if use_act_as - calendar_id = @ews_client.get_folder(:calendar, opts = {act_as: email }).id - events = @ews_client.find_items(folder_id: calendar_id, calendar_view: {start_date: start_param, end_date: end_param}) - else - @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], email) - events = @ews_client.find_items({:folder_id => :calendar, :calendar_view => {:start_date => start_param.utc.iso8601, :end_date => end_param.utc.iso8601}}) - end - # events = @ews_client.get_item(:calendar, opts = {act_as: email}).items - events.each{|event| - event.get_all_properties! - internal_domain = Mail::Address.new(event.organizer.email).domain - booking = {} - booking[:subject] = event.subject - booking[:title] = event.subject - booking[:id] = event.id - # booking[:start_date] = event.start.utc.iso8601 - # booking[:end_date] = event.end.utc.iso8601 - booking[:start] = event.start.to_i * 1000 - booking[:end] = event.end.to_i * 1000 - booking[:body] = event.body - booking[:organiser] = { - name: event.organizer.name, - email: event.organizer.email - } - booking[:attendees] = event.required_attendees.map {|attendee| - if room_emails.include?(attendee.email) - booking[:room_id] = attendee.email - next - end - email_domain = Mail::Address.new(attendee.email).domain - { - name: attendee.name, - email: attendee.email, - visitor: (internal_domain != email_domain), - organisation: attendee.email.split('@')[1..-1].join("").split('.')[0].capitalize - } - }.compact if event.required_attendees - if @hide_all_day_bookings - STDERR.puts "SKIPPING #{event.subject}" - STDERR.flush - next if event.end.to_time - event.start.to_time > 86399 - end - bookings.push(booking) - } - bookings - rescue Exception => msg - STDERR.puts msg - STDERR.flush - return [] - end - end - - def create_booking(room_email:, start_param:, end_param:, subject:, description:nil, current_user:, attendees: nil, timezone:'Sydney', permission: 'impersonation', mailbox_location: 'user') - STDERR.puts "CREATING NEW BOOKING IN LIBRARY" - STDERR.puts "room_email is #{room_email}" - STDERR.puts "start_param is #{start_param}" - STDERR.puts "end_param is #{end_param}" - STDERR.puts "subject is #{subject}" - STDERR.puts "description is #{description}" - STDERR.puts "current_user is #{current_user}" - STDERR.puts "attendees is #{attendees}" - STDERR.puts "timezone is #{timezone}" - STDERR.flush - # description = String(description) - attendees = Array(attendees) - - - booking = {} - - # Allow for naming of subject or title - booking[:subject] = subject - booking[:title] = subject - booking[:location] = Orchestrator::ControlSystem.find_by_email(room_email).name - - - # Set the room email as a resource - booking[:resources] = [{ - attendee: { - mailbox: { - email_address: room_email - } - } - }] - - # start_object = Time.at(start_param.to_i) - # end_object = Time.at(end_param.to_i) - - start_object = ensure_ruby_date(start_param.to_i) - end_object = ensure_ruby_date(end_param.to_i) - - - # Add start and end times - booking[:start] = start_object.utc.iso8601.chop - booking[:end] = end_object.utc.iso8601.chop - - # Add the current user passed in as an attendee - mailbox = { email_address: current_user.email } - mailbox[:name] = current_user.name if current_user.name - booking[:required_attendees] = [{ - attendee: { mailbox: mailbox } - }] - - # Add the attendees - attendees.each do |attendee| - # If we don't have an array of emails then it's an object in the form {email: "a@b.com", name: "Blahman Blahson"} - if attendee.class != String - attendee = attendee['email'] - end - booking[:required_attendees].push({ - attendee: { mailbox: { email_address: attendee}} - }) - end - - # Add the room as an attendee (it seems as if some clients require this and others don't) - booking[:required_attendees].push({ attendee: { mailbox: { email_address: room_email}}}) - booking[:body] = description - - # A little debugging - STDERR.puts "MAKING REQUEST WITH" - STDERR.puts booking - STDERR.flush - - if mailbox_location == 'user' - mailbox = current_user.email - elsif mailbox_location == 'room' - mailbox = room_email - end - - # Determine whether to use delegation, impersonation or neither - if permission == 'delegation' - folder = @ews_client.get_folder(:calendar, { act_as: mailbox }) - elsif permission == 'impersonation' - @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], mailbox) - folder = @ews_client.get_folder(:calendar) - elsif permission == 'none' || permission.nil? - folder = @ews_client.get_folder(:calendar) - end - - # Create the booking and return data relating to it - appointment = folder.create_item(booking) - { - id: appointment.id, - start: start_param, - end: end_param, - attendees: attendees, - subject: subject - } - end - - def update_booking(booking_id:, room_email:nil, start_param:nil, end_param:nil, subject:nil, description:nil, current_user:nil, attendees: nil, timezone:'Sydney', permission: 'impersonation', mailbox_location: 'user') - - event = @ews_client.get_item(booking_id) - booking = {} - - # Add attendees if passed in - attendees = Array(attendees) - attendees.each do |attendee| - if attendee.class != String - attendee = attendee['email'] - end - booking[:required_attendees] ||= [] - booking[:required_attendees].push({ - attendee: { mailbox: { email_address: attendee}} - }) - end if attendees && !attendees.empty? - - # Add subject or title - booking[:subject] = subject if subject - booking[:title] = subject if subject - - # Add location - booking[:location] = Orchestrator::ControlSystem.find_by_email(room_email).name if room_email - - # Add new times if passed - booking[:start] = Time.at(start_param.to_i / 1000).utc.iso8601.chop if start_param - booking[:end] = Time.at(end_param.to_i / 1000).utc.iso8601.chop if end_param - - if mailbox_location == 'user' - mailbox = current_user.email - elsif mailbox_location == 'room' - mailbox = room_email - end - - if permission == 'impersonation' - @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], mailbox) - end - - new_booking = event.update_item!(booking) - - - { - id: new_booking.id, - start: new_booking.start, - end: new_booking.end, - attendees: new_booking.required_attendees, - subject: new_booking.subject - } - end - - def delete_booking(booking_id:, mailbox:) - @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], mailbox) - booking = @ews_client.get_item(booking_id) - booking.delete!(:recycle, send_meeting_cancellations: "SendOnlyToAll") - 200 - end - - # Takes a date of any kind (epoch, string, time object) and returns a time object - def ensure_ruby_date(date) - if !(date.class == Time || date.class == DateTime) - if string_is_digits(date) - - # Convert to an integer - date = date.to_i - - # If JavaScript epoch remove milliseconds - if date.to_s.length == 13 - date /= 1000 - end - - # Convert to datetimes - date = Time.at(date) - else - date = Time.parse(date) - end - end - return date - end - - # Returns true if a string is all digits (used to check for an epoch) - def string_is_digits(string) - string = string.to_s - string.scan(/\D/).empty? - end - -end diff --git a/modules/aca/exchange_booking.rb b/modules/aca/exchange_booking.rb deleted file mode 100644 index 684c6496..00000000 --- a/modules/aca/exchange_booking.rb +++ /dev/null @@ -1,833 +0,0 @@ -# For rounding up to the nearest 15min -# See: http://stackoverflow.com/questions/449271/how-to-round-a-time-down-to-the-nearest-15-minutes-in-ruby -class ActiveSupport::TimeWithZone - def ceil(seconds = 60) - return self if seconds.zero? - Time.at(((self - self.utc_offset).to_f / seconds).ceil * seconds).in_time_zone + self.utc_offset - end -end - -require 'microsoft/exchange' - -module Aca; end - -# NOTE:: Requires Settings: -# ======================== -# room_alias: 'rs.au.syd.L16Aitken', -# building: 'DP3', -# level: '16' - -class Aca::ExchangeBooking - include ::Orchestrator::Constants - EMAIL_CACHE = ::Concurrent::Map.new - CAN_LDAP = begin - require 'net/ldap' - true - rescue LoadError - false - end - CAN_EWS = begin - require 'viewpoint2' - true - rescue LoadError - begin - require 'viewpoint' - true - rescue LoadError - false - end - false - end - - - descriptive_name 'Exchange Room Bookings' - generic_name :Bookings - implements :logic - - - # The room we are interested in - default_settings({ - update_every: '2m', - - # Moved to System or Zone Setting - # cancel_meeting_after: 900 - - # Card reader IDs if we want to listen for swipe events - card_readers: ['reader_id_1', 'reader_id_2'], - - # Optional LDAP creds for looking up emails - ldap_creds: { - host: 'ldap.org.com', - port: 636, - encryption: { - method: :simple_tls, - tls_options: { - verify_mode: 0 - } - }, - auth: { - method: :simple, - username: 'service account', - password: 'password' - } - }, - tree_base: "ou=User,ou=Accounts,dc=org,dc=com", - - # Optional EWS for creating and removing bookings - ews_creds: [ - 'https://company.com/EWS/Exchange.asmx', - 'service account', - 'password', - { http_opts: { ssl_verify_mode: 0 } } - ], - ews_room: 'room@email.address' - }) - - - def on_load - on_update - end - - def on_update - self[:swiped] ||= 0 - @last_swipe_at = 0 - @use_act_as = setting(:use_act_as) - - self[:hide_all] = setting(:hide_all) || false - self[:touch_enabled] = setting(:touch_enabled) || false - self[:name] = self[:room_name] = setting(:room_name) || system.name - self[:description] = setting(:description) || nil - self[:title] = setting(:title) || nil - self[:timeout] = setting(:timeout) || false - self[:booking_endable] = setting(:booking_endable) || false - self[:booking_ask_end] = setting(:booking_ask_end) || false - - self[:control_url] = setting(:booking_control_url) || system.config.support_url - self[:booking_controls] = setting(:booking_controls) - self[:booking_catering] = setting(:booking_catering) - self[:booking_hide_details] = setting(:booking_hide_details) - self[:booking_hide_availability] = setting(:booking_hide_availability) - self[:booking_hide_user] = setting(:booking_hide_user) - self[:booking_hide_description] = setting(:booking_hide_description) - self[:booking_hide_timeline] = setting(:booking_hide_timeline) - self[:last_meeting_started] = setting(:last_meeting_started) - self[:cancel_meeting_after] = setting(:cancel_meeting_after) - self[:booking_min_duration] = setting(:booking_min_duration) - self[:booking_disable_future] = setting(:booking_disable_future) - self[:booking_max_duration] = setting(:booking_max_duration) - self[:booking_cancel_timeout] = setting(:booking_cancel_timeout) - self[:hide_all_day_bookings] = setting(:hide_all_day_bookings) - self[:timeout] = setting(:timeout) - self[:arrow_direction] = setting(:arrow_direction) - self[:icon] = setting(:icon) - @hide_all_day_bookings = Boolean(setting(:hide_all_day_bookings)) - - @check_meeting_ending = setting(:check_meeting_ending) # seconds before meeting ending - @extend_meeting_by = setting(:extend_meeting_by) || 15.minutes.to_i - - # Skype join button available 2min before the start of a meeting - @skype_start_offset = setting(:skype_start_offset) || 120 - @skype_check_offset = setting(:skype_check_offset) || 380 # 5min + 20 seconds - - # Skype join button not available in the last 8min of a meeting - @skype_end_offset = setting(:skype_end_offset) || 480 - @force_skype_extract = setting(:force_skype_extract) - - # Because restarting the modules results in a 'swipe' of the last read card - ignore_first_swipe = true - - # Is there catering available for this room? - self[:catering] = setting(:catering_system_id) - if self[:catering] - self[:menu] = setting(:menu) - end - - # Do we want to look up the users email address? - if CAN_LDAP - @ldap_creds = setting(:ldap_creds) - if @ldap_creds - encrypt = @ldap_creds[:encryption] - encrypt[:method] = encrypt[:method].to_sym if encrypt && encrypt[:method] - @tree_base = setting(:tree_base) - @ldap_user = @ldap_creds.delete :auth - end - else - logger.warn "net/ldap gem not available" if setting(:ldap_creds) - end - - # Do we want to use exchange web services to manage bookings - if CAN_EWS - @ews_creds = setting(:ews_creds) - @ews_room = (setting(:ews_room) || system.email) if @ews_creds - # supports: SMTP, PSMTP, SID, UPN (user principle name) - # NOTE:: Using UPN we might be able to remove the LDAP requirement - @ews_connect_type = (setting(:ews_connect_type) || :SMTP).to_sym - @timezone = setting(:room_timezone) - else - logger.warn "viewpoint gem not available" if setting(:ews_creds) - end - - # Load the last known values (persisted to the DB) - self[:waiter_status] = (setting(:waiter_status) || :idle).to_sym - self[:waiter_call] = self[:waiter_status] != :idle - - self[:catering_status] = setting(:last_catering_status) || {} - self[:order_status] = :idle - - self[:last_meeting_started] = setting(:last_meeting_started) - self[:cancel_meeting_after] = setting(:cancel_meeting_after) - - - # unsubscribe to all swipe IDs if any are subscribed - if @subs.present? - @subs.each do |sub| - unsubscribe(sub) - end - - @subs = nil - end - - # Are there any swipe card integrations - if system.exists? :Security - readers = setting(:card_readers) - if readers.present? - security = system[:Security] - - readers = readers.is_a?(Array) ? readers : [readers] - sys = system - @subs = [] - readers.each do |id| - @subs << sys.subscribe(:Security, 1, id.to_s) do |notice| - if ignore_first_swipe - ignore_first_swipe = false - else - swipe_occured(notice.value) - end - end - end - end - end - - schedule.clear - schedule.in(rand(10000)) { - fetch_bookings(true) - } - schedule.every((setting(:update_every) || 120000).to_i + rand(10000)) { - fetch_bookings - } - end - - - def set_light_status(status) - lightbar = system[:StatusLight] - return if lightbar.nil? - - case status.to_sym - when :unavailable - lightbar.colour(:red) - when :available - lightbar.colour(:green) - when :pending - lightbar.colour(:orange) - else - lightbar.colour(:off) - end - end - - def directory_search(q, limit: 30) - # Ensure only a single search is occuring at a time - if @dir_search - @dir_search = q - return - end - - ews = ::Microsoft::Exchange.new({ - ews_url: ENV['EWS_URL'] || 'https://outlook.office365.com/ews/Exchange.asmx', - service_account_email: ENV['OFFICE_ACCOUNT_EMAIL'], - service_account_password: ENV['OFFICE_ACCOUNT_PASSWORD'], - internet_proxy: ENV['INTERNET_PROXY'] - }) - - @dir_search = q - self[:searching] = true - begin - # sip_spd:Auto sip_num:email@address.com - entries = [] - task { ews.get_users(q: q, limit: limit) }.value.each do |entry| - phone = entry['phone'] - - entries << entry - entries << ({ - name: entry['name'], - phone: phone.gsub(/\D+/, '') - }) if phone - end - - # Ensure the results are unique and pushed to the client - entries[0][:id] = rand(10000) if entries.length > 0 - self[:directory] = entries - rescue => e - logger.print_error e, 'searching directory' - self[:directory] = [] - end - - # Update the search if a change was requested while a search was occuring - if @dir_search != q - q = @dir_search - thread.next_tick { directory_search(q, limit) } - else - self[:searching] = false - end - - @dir_search = nil - end - - # ====================================== - # Waiter call information - # ====================================== - def waiter_call(state) - status = is_affirmative?(state) - - self[:waiter_call] = status - - # Used to highlight the service button - if status - self[:waiter_status] = :pending - else - self[:waiter_status] = :idle - end - - define_setting(:waiter_status, self[:waiter_status]) - end - - def call_acknowledged - self[:waiter_status] = :accepted - define_setting(:waiter_status, self[:waiter_status]) - end - - - # ====================================== - # Catering Management - # ====================================== - def catering_status(details) - self[:catering_status] = details - - # We'll turn off the green light on the waiter call button - if self[:waiter_status] != :idle && details[:progress] == 'visited' - self[:waiter_call] = false - self[:waiter_status] = :idle - define_setting(:waiter_status, self[:waiter_status]) - end - - define_setting(:last_catering_status, details) - end - - def commit_order(order_details) - self[:order_status] = :pending - status = self[:catering_status] - - if status && status[:progress] == 'visited' - status = status.dup - status[:progress] = 'cleaned' - self[:catering_status] = status - end - - if self[:catering] - sys = system - @oid ||= 1 - systems(self[:catering])[:Orders].add_order({ - id: "#{sys.id}_#{@oid}", - created_at: Time.now.to_i, - room_id: sys.id, - room_name: sys.name, - order: order_details - }) - end - end - - def order_accepted - self[:order_status] = :accepted - end - - def order_complete - self[:order_status] = :idle - end - - - - # ====================================== - # ROOM BOOKINGS: - # ====================================== - def fetch_bookings(first=false) - logger.debug { "looking up todays emails for #{@ews_room}" } - skype_exists = system.exists?(:Skype) - task { - todays_bookings(first, skype_exists) - }.then(proc { |bookings| - self[:today] = bookings - if @check_meeting_ending - should_notify? - end - }, proc { |e| logger.print_error(e, 'error fetching bookings') }) - end - - - # ====================================== - # Meeting Helper Functions - # ====================================== - - def start_meeting(meeting_ref) - self[:last_meeting_started] = meeting_ref - self[:meeting_pending] = meeting_ref - self[:meeting_ending] = false - self[:meeting_pending_notice] = false - define_setting(:last_meeting_started, meeting_ref) - end - - def cancel_meeting(start_time, reason = "timeout") - task { - if start_time.class == Integer - delete_ews_booking (start_time / 1000).to_i - else - # Converts to time object regardless of start_time being string or time object - start_time = Time.parse(start_time.to_s) - delete_ews_booking start_time.to_i - end - }.then(proc { |count| - logger.warn { "successfully removed #{count} bookings due to #{reason}" } - - self[:last_meeting_started] = 0 - self[:meeting_pending] = 0 - self[:meeting_ending] = false - self[:meeting_pending_notice] = false - - fetch_bookings - true - }, proc { |error| - logger.print_error error, 'removing ews booking' - }) - end - - # If last meeting started !== meeting pending then - # we'll show a warning on the in room touch panel - def set_meeting_pending(meeting_ref) - return if self[:last_meeting_started] == meeting_ref - self[:meeting_ending] = false - self[:meeting_pending] = meeting_ref - self[:meeting_pending_notice] = true - end - - # Meeting ending warning indicator - # (When meeting_ending !== last_meeting_started then the warning hasn't been cleared) - # The warning is only displayed when meeting_ending === true - def set_end_meeting_warning(meeting_ref = nil, extendable = false) - if self[:last_meeting_started].nil? || self[:meeting_ending] != (meeting_ref || self[:last_meeting_started]) - self[:meeting_ending] = true - - # Allows meeting ending warnings in all rooms - self[:last_meeting_started] = meeting_ref if meeting_ref - self[:meeting_canbe_extended] = extendable - end - end - - def clear_end_meeting_warning - self[:meeting_ending] = self[:last_meeting_started] - end - # --------- - - def create_meeting(options) - # Check that the required params exist - required_fields = [:start, :end] - check = required_fields - options.keys.collect(&:to_sym) - if check != [] - # There are missing required fields - logger.info "Required fields missing: #{check}" - raise "missing required fields: #{check}" - end - - req_params = {} - req_params[:room_email] = @ews_room - req_params[:subject] = options[:title] - req_params[:start_time] = Time.at(options[:start].to_i / 1000).utc.iso8601.chop - req_params[:end_time] = Time.at(options[:end].to_i / 1000).utc.iso8601.chop - - task { - username = options[:user] - if username.present? - - user_email = ldap_lookup_email(username) - if user_email - req_params[:user_email] = user_email - make_ews_booking req_params - else - raise "couldn't find user: #{username}" - end - - else - make_ews_booking req_params - end - }.then(proc { |id| - fetch_bookings - logger.debug { "successfully created booking: #{id}" } - "Ok" - }, proc { |error| - logger.print_error error, 'creating ad hoc booking' - thread.reject error # propogate the error - }) - end - - def should_notify? - bookings = self[:today] - return unless bookings.present? - now = Time.now.to_i - - current = nil - pending = nil - found = false - - bookings.sort! { |a, b| a[:end_epoch] <=> b[:end_epoch] } - bookings.each do |booking| - starting = booking[:start_epoch] - ending = booking[:end_epoch] - - if starting < now && ending > now - found = true - current = ending - @current_meeting_title = booking[:Subject] - elsif found - pending = starting - break - end - end - - if !current - self[:meeting_canbe_extended] = false - return - end - - check_start = current - @check_meeting_ending - check_extend = current + @extend_meeting_by - - if now >= check_start && (pending.nil? || pending >= check_extend) - self[:meeting_canbe_extended] = current - else - self[:meeting_canbe_extended] = false - end - end - - def extend_meeting - starting = self[:meeting_canbe_extended] - return false unless starting - - ending = starting + @extend_meeting_by - create_meeting(start: starting * 1000, end: ending * 1000, title: @current_meeting_title).then do - start_meeting(starting * 1000) - end - end - - def send_email(title, body, to) - task { - cli = Viewpoint::EWSClient.new(*@ews_creds) - opts = {} - - if @use_act_as - opts[:act_as] = @ews_room if @ews_room - else - cli.set_impersonation(Viewpoint::EWS::ConnectingSID[@ews_connect_type], @ews_room) if @ews_room - end - - cli.send_message subject: title, body: body, to_recipients: to - } - end - - - protected - - - def swipe_occured(info) - # Update the user details - @last_swipe_at = Time.now.to_i - self[:fullname] = "#{info[:firstname]} #{info[:lastname]}" - self[:username] = info[:staff_id] - email = nil - - if self[:username] && @ldap_creds - email = EMAIL_CACHE[self[:username]] - if email - set_email(email) - logger.debug { "email #{email} found in cache" } - else - # Cache username here as self[:username] might change while we - # looking up the previous username - username = self[:username] - - logger.debug { "looking up email for #{username} - #{self[:fullname]}" } - task { - ldap_lookup_email username - }.then do |email| - if email - logger.debug { "email #{email} found in LDAP" } - EMAIL_CACHE[username] = email - set_email(email) - else - logger.warn "no email found in LDAP for #{username}" - set_email nil - end - end - end - else - logger.warn "no staff ID for user #{self[:fullname]}" - set_email nil - end - end - - def set_email(email) - self[:email] = email - self[:swiped] += 1 - end - - # ==================================== - # LDAP lookup to occur in worker thread - # ==================================== - def ldap_lookup_email(username) - email = EMAIL_CACHE[username] - return email if email - - ldap = Net::LDAP.new @ldap_creds - ldap.authenticate @ldap_user[:username], @ldap_user[:password] if @ldap_user - - login_filter = Net::LDAP::Filter.eq('sAMAccountName', username) - object_filter = Net::LDAP::Filter.eq('objectClass', '*') - treebase = @tree_base - search_attributes = ['mail'] - - email = nil - ldap.bind - ldap.search({ - base: treebase, - filter: object_filter & login_filter, - attributes: search_attributes - }) do |entry| - email = get_attr(entry, 'mail') - end - - # Returns email as a promise - EMAIL_CACHE[username] = email - email - end - - def get_attr(entry, attr_name) - if attr_name != "" && attr_name != nil - entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] - end - end - # ==================================== - - - # ======================================= - # EWS Requests to occur in a worker thread - # ======================================= - def make_ews_booking(user_email: nil, subject: 'On the spot booking', room_email:, start_time:, end_time:) - user_email ||= self[:email] # if swipe card used - - booking = { - subject: subject, - start: start_time, - end: end_time - } - - if user_email - booking[:required_attendees] = [{ - attendee: { mailbox: { email_address: user_email } } - }] - end - - cli = Viewpoint::EWSClient.new(*@ews_creds) - opts = {} - - if @use_act_as - opts[:act_as] = @ews_room if @ews_room - else - cli.set_impersonation(Viewpoint::EWS::ConnectingSID[@ews_connect_type], @ews_room) if @ews_room - end - - folder = cli.get_folder(:calendar, opts) - appointment = folder.create_item(booking) - - # Return the booking IDs - appointment.item_id - end - - def delete_ews_booking(delete_at) - now = Time.now - if @timezone - start = now.in_time_zone(@timezone).midnight - ending = now.in_time_zone(@timezone).tomorrow.midnight - else - start = now.midnight - ending = now.tomorrow.midnight - end - - count = 0 - - cli = Viewpoint::EWSClient.new(*@ews_creds) - - if @use_act_as - # TODO:: think this line can be removed?? - # delete_at = Time.parse(delete_at.to_s).to_i - - opts = {} - opts[:act_as] = @ews_room if @ews_room - - folder = cli.get_folder(:calendar, opts) - items = folder.items({:calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) - else - cli.set_impersonation(Viewpoint::EWS::ConnectingSID[@ews_connect_type], @ews_room) if @ews_room - items = cli.find_items({:folder_id => :calendar, :calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) - end - - items.each do |meeting| - meeting_time = Time.parse(meeting.ews_item[:start][:text]) - - # Remove any meetings that match the start time provided - if meeting_time.to_i == delete_at - # new_booking = meeting.update_item!({ end: Time.now.utc.iso8601.chop }) - - meeting.delete!(:recycle, send_meeting_cancellations: 'SendOnlyToAll') - count += 1 - end - end - - # Return the number of meetings removed - count - end - - def todays_bookings(first=false, skype_exists=false) - now = Time.now - if @timezone - start = now.in_time_zone(@timezone).midnight - ending = now.in_time_zone(@timezone).tomorrow.midnight - else - start = now.midnight - ending = now.tomorrow.midnight - end - - cli = Viewpoint::EWSClient.new(*@ews_creds) - - - if @use_act_as - opts = {} - opts[:act_as] = @ews_room if @ews_room - - folder = cli.get_folder(:calendar, opts) - items = folder.items({:calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) - else - cli.set_impersonation(Viewpoint::EWS::ConnectingSID[@ews_connect_type], @ews_room) if @ews_room - items = cli.find_items({:folder_id => :calendar, :calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) - end - - set_skype_url = skype_exists - set_skype_url = true if @force_skype_extract - now_int = now.to_i - - # items.select! { |booking| !booking.cancelled? } - results = items.collect do |meeting| - item = meeting.ews_item - start = item[:start][:text] - ending = item[:end][:text] - - real_start = Time.parse(start) - real_end = Time.parse(ending) - - # Extract the skype meeting URL - if set_skype_url - start_integer = real_start.to_i - @skype_check_offset - join_integer = real_start.to_i - @skype_start_offset - end_integer = real_end.to_i - @skype_end_offset - - if now_int > start_integer && now_int < end_integer - if first - self[:last_meeting_started] = start_integer * 1000 - end - meeting.get_all_properties! - - if meeting.body - match = meeting.body.match(/\"pexip\:\/\/(.+?)\"/) - if match - set_skype_url = false - self[:pexip_meeting_uid] = start_integer - self[:pexip_meeting_address] = match[1] - else - links = URI.extract(meeting.body).select { |url| url.start_with?('https://meet.lync') } - if links.empty? - # Lync: - # Skype: - body_parts = meeting.body.split('OutJoinLink"') - if body_parts.length > 1 - links = body_parts[-1].split('"').select { |link| link.start_with?('https://') } - end - end - - if links[0].present? - if now_int > join_integer - self[:can_join_skype_meeting] = true - self[:skype_meeting_pending] = true - else - self[:skype_meeting_pending] = true - end - set_skype_url = false - self[:skype_meeting_address] = links[0] - system[:Skype].set_uri(links[0]) if skype_exists - end - end - end - end - - if @timezone - start = real_start.in_time_zone(@timezone).iso8601[0..18] - ending = real_end.in_time_zone(@timezone).iso8601[0..18] - end - elsif @timezone - start = Time.parse(start).in_time_zone(@timezone).iso8601[0..18] - ending = Time.parse(ending).in_time_zone(@timezone).iso8601[0..18] - end - - logger.debug { item.inspect } - - if @hide_all_day_bookings - next if Time.parse(ending) - Time.parse(start) > 86399 - end - - # Prevent connections handing with TIME_WAIT - # cli.ews.connection.httpcli.reset_all - - if ["Private", "Confidential"].include?(meeting.sensitivity) - subject = meeting.sensitivity - else - subject = item[:subject][:text] - end - - { - :Start => start, - :End => ending, - :Subject => subject, - :owner => item[:organizer][:elems][0][:mailbox][:elems][0][:name][:text], - :setup => 0, - :breakdown => 0, - :start_epoch => real_start.to_i, - :end_epoch => real_end.to_i - } - end - results.compact! - - if set_skype_url - self[:pexip_meeting_address] = nil - self[:can_join_skype_meeting] = false - self[:skype_meeting_pending] = false - system[:Skype].set_uri(nil) if skype_exists - end - - results - end - # ======================================= -end From 67fbe4bad50fbace6b0f577fa2effb66f7ee5316 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 30 May 2018 09:45:58 +1000 Subject: [PATCH 25/37] Update logic for authing different APIs --- lib/google/admin.rb | 47 +++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/lib/google/admin.rb b/lib/google/admin.rb index 96c104a1..80cb2efa 100644 --- a/lib/google/admin.rb +++ b/lib/google/admin.rb @@ -30,40 +30,54 @@ def initialize( @scopes = scopes || [ 'https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/admin.directory.user'] @admin_email = admin_email @domain = domain - @authorization = Google::Auth.get_application_default(scopes) admin_api = Google::Apis::AdminDirectoryV1 @admin = admin_api::DirectoryService.new - @authorization.sub = @admin_email - @admin.authorization = @authorization calendar_api = Google::Apis::CalendarV3 @calendar = calendar_api::CalendarService.new - @calendar.authorization = authorization end def get_users(q: nil, limit: nil) + authorization = Google::Auth.get_application_default(@scopes).dup + authorization.sub = @admin_email + @admin.authorization = authorization options = { domain: @domain } options[:query] = q if q - options[:maxResults] = (limit || 500) + options[:max_results] = (limit || 500) users = @admin.list_users(options) - users.users + users.users.map do |u| + primary_email = nil + u.emails.each do |email| + primary_email = email['address'] if email['primary'] + end + { + name: u.name.full_name, + email: primary_email + } + end end def get_user(user_id:) + authorization = Google::Auth.get_application_default(@scopes).dup + authorization.sub = @admin_email + @admin.authorization = authorization options = { domain: @domain } options[:query] = user_id - options[:maxResults] = 1 + options[:max_results] = 1 users = @admin.list_users(options) users.users[0] end def get_available_rooms(room_ids:, start_param:, end_param:) + authorization = Google::Auth.get_application_default(@scopes).dup + authorization.sub = @admin_email + @calendar.authorization = authorization now = Time.now start_param = ensure_ruby_date((start_param || now)) end_param = ensure_ruby_date((end_param || (now + 1.hour))) @@ -85,10 +99,16 @@ def get_available_rooms(room_ids:, start_param:, end_param:) end def delete_booking(room_id:, booking_id:) + authorization = Google::Auth.get_application_default(@scopes).dup + authorization.sub = @admin_email + @calendar.authorization = authorization @calendar.delete_event(room_id, booking_id) end def get_bookings(email:, start_param:nil, end_param:nil) + authorization = Google::Auth.get_application_default(@scopes).dup + authorization.sub = @admin_email + @calendar.authorization = authorization if start_param.nil? start_param = DateTime.now end_param = DateTime.now + 1.hour @@ -98,6 +118,9 @@ def get_bookings(email:, start_param:nil, end_param:nil) end def create_booking(room_email:, start_param:, end_param:, subject:, description:nil, current_user:, attendees: nil, recurrence: nil, timezone:'Sydney') + authorization = Google::Auth.get_application_default(@scopes).dup + authorization.sub = @admin_email + @calendar.authorization = authorization description = String(description) attendees = Array(attendees) @@ -107,8 +130,8 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: # Ensure our start and end params are Ruby dates and format them in Graph format start_object = ensure_ruby_date(start_param) end_object = ensure_ruby_date(end_param) - start_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: start_object, timezone: timezone } - end_param = Google::Apis::CalendarV3::EventDateTime.new { date_time: end_object, timezone: timezone } + start_param = Google::Apis::CalendarV3::EventDateTime.new({ date_time: start_object, timezone: timezone }) + end_param = Google::Apis::CalendarV3::EventDateTime.new({ date_time: end_object, timezone: timezone }) event_params = { start: start_param, @@ -125,7 +148,7 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: response_status: 'accepted' } attendees = [ - Google::Apis::CalendarV3::EventAttendee.new room_attendee_options + Google::Apis::CalendarV3::EventAttendee.new(room_attendee_options) ] # Add the attendees @@ -134,12 +157,12 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: display_name: a[:name], email: a[:email] } - Google::Apis::CalendarV3::EventAttendee.new attendee_options + Google::Apis::CalendarV3::EventAttendee.new(attendee_options) } event_params[:attendees] = attendees # Add the current_user as an attendee - event_params[:creator] = Google::Apis::CalendarV3::Event::Creator.new { display_name: current_user.name, email: current_user.email } + event_params[:creator] = Google::Apis::CalendarV3::Event::Creator.new({ display_name: current_user.name, email: current_user.email }) event = Google::Apis::CalendarV3::Event.new event_params From b7c5e7a3525058b5707e0aa638292654d66b8f6f Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 30 May 2018 11:38:42 +1000 Subject: [PATCH 26/37] Fix repsonse from get_bookings --- lib/google/admin.rb | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/lib/google/admin.rb b/lib/google/admin.rb index 80cb2efa..afe2881b 100644 --- a/lib/google/admin.rb +++ b/lib/google/admin.rb @@ -114,7 +114,20 @@ def get_bookings(email:, start_param:nil, end_param:nil) end_param = DateTime.now + 1.hour end - events = calendar.list_events(email, time_min: start_param, time_max: end_param).items + events = @calendar.list_events(email, time_min: start_param.iso8601, time_max: end_param.iso8601).items + events.map do |event| + { + start_date: event.start.date_time.to_i * 1000, + end_date: event.end.date_time.to_i * 1000, + Start: event.start.date_time.utc.iso8601, + End: event.end.date_time.utc.iso8601, + subject: event.summary, + title: event.summary, + description: event.description, + attendees: event.attendees.map {|a| {name: a.display_name, email: a.email} }, + id: event.id + } + end end def create_booking(room_email:, start_param:, end_param:, subject:, description:nil, current_user:, attendees: nil, recurrence: nil, timezone:'Sydney') @@ -140,17 +153,6 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: description: description } - # Add the room as an attendee - room_attendee_options = { - resource: true, - display_name: room.name, - email: room_email, - response_status: 'accepted' - } - attendees = [ - Google::Apis::CalendarV3::EventAttendee.new(room_attendee_options) - ] - # Add the attendees attendees.map!{|a| attendee_options = { @@ -159,6 +161,17 @@ def create_booking(room_email:, start_param:, end_param:, subject:, description: } Google::Apis::CalendarV3::EventAttendee.new(attendee_options) } + + # Add the room as an attendee + room_attendee_options = { + resource: true, + display_name: room.name, + email: room_email, + response_status: 'accepted' + } + + attendees.push(Google::Apis::CalendarV3::EventAttendee.new(room_attendee_options)) + event_params[:attendees] = attendees # Add the current_user as an attendee @@ -184,9 +197,9 @@ def ensure_ruby_date(date) end # Convert to datetimes - date = DateTime.at(date) + date = DateTime.parse(Time.at(date).to_s) else - date = DateTime.parse(date) + date = DateTime.parse(date.to_s) end end return date From 1c9d7c1e785b21f57507db9c34b16c8d974c6498 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 30 May 2018 12:13:36 +1000 Subject: [PATCH 27/37] Update google booking driver --- modules/aca/google_refresh_booking.rb | 57 ++++++++++++++++----------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 50bc39aa..2eca67eb 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -161,6 +161,7 @@ def on_update @google_secret = setting(:google_client_secret) @google_redirect_uri = setting(:google_redirect_uri) @google_refresh_token = setting(:google_refresh_token) + @google_admin_email = setting(:google_admin_email) @google_scope = setting(:google_scope) @google_room = (setting(:google_room) || system.email) # supports: SMTP, PSMTP, SID, UPN (user principle name) @@ -323,21 +324,32 @@ def fetch_bookings(*args) # client = OAuth2::Client.new(@google_client_id, @google_secret, {site: @google_site, token_url: @google_token_url}) - options = { - client_id: @google_client_id, - client_secret: @google_secret, - scope: @google_scope, - redirect_uri: @google_redirect_uri, - refresh_token: @google_refresh_token, - grant_type: "refresh_token" - } - - authorization = Google::Auth::UserRefreshCredentials.new options - #Calendar = Google::Apis::CalendarV3 - calendar = Calendar::CalendarService.new + # options = { + # client_id: @google_client_id, + # client_secret: @google_secret, + # scope: @google_scope, + # redirect_uri: @google_redirect_uri, + # refresh_token: @google_refresh_token, + # grant_type: "refresh_token" + # } + # logger.info "AUTHORIZING WITH OPTIONS:" + # STDERR.puts "AUTHORIZING WITH OPTIONS:" + # STDERR.puts options + # logger.info options + # STDERR.puts @google_admin_email + # logger.info @google_admin_email + # STDERR.flush + + # authorization = Google::Auth::UserRefreshCredentials.new options + + authorization = Google::Auth.get_application_default(@google_scope).dup + authorization.sub = @google_admin_email + + calendar_api = Google::Apis::CalendarV3 + calendar = calendar_api::CalendarService.new calendar.authorization = authorization - events = calendar.list_events(system.email) + events = calendar.list_events(system.email, time_min: Time.now.midnight.iso8601, time_max: Time.now.tomorrow.midnight.iso8601).items task { todays_bookings(events) @@ -567,16 +579,12 @@ def todays_bookings(events) results = [] events.each{|event| - start_time = ActiveSupport::TimeZone.new('UTC').parse(event.start_time).iso8601[0..18] - end_time = ActiveSupport::TimeZone.new('UTC').parse(event.end_time).iso8601[0..18] - - organiser = "" results.push({ - :Start => start_time, - :End => end_time, - :Subject => event.title, - :owner => organiser + :Start => event.start.date_time.utc.iso8601, + :End => event.end.date_time.utc.iso8601, + :Subject => event.summary, + :owner => event.organizer.display_name # :setup => 0, # :breakdown => 0 }) @@ -587,5 +595,10 @@ def todays_bookings(events) results end - # ======================================= + + def log(data) + STDERR.puts data + logger.info data + STDERR.flush + end end From 9e48313564ff6268df28319062249d0fa20a15c2 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Fri, 22 Jun 2018 12:33:27 +1000 Subject: [PATCH 28/37] Fix timezone in booking module --- modules/aca/google_booking.rb | 643 -------------------------- modules/aca/google_refresh_booking.rb | 3 +- 2 files changed, 2 insertions(+), 644 deletions(-) delete mode 100644 modules/aca/google_booking.rb diff --git a/modules/aca/google_booking.rb b/modules/aca/google_booking.rb deleted file mode 100644 index a411d391..00000000 --- a/modules/aca/google_booking.rb +++ /dev/null @@ -1,643 +0,0 @@ -# encoding: ASCII-8BIT - -require 'uv-rays' - -# For rounding up to the nearest 15min -# See: http://stackoverflow.com/questions/449271/how-to-round-a-time-down-to-the-nearest-15-minutes-in-ruby -class ActiveSupport::TimeWithZone - def ceil(seconds = 60) - return self if seconds.zero? - Time.at(((self - self.utc_offset).to_f / seconds).ceil * seconds).in_time_zone + self.utc_offset - end -end - - -module Aca; end - -# NOTE:: Requires Settings: -# ======================== -# room_alias: 'rs.au.syd.L16Aitken', -# building: 'DP3', -# level: '16' - -class Aca::GoogleBooking - include ::Orchestrator::Constants - EMAIL_CACHE = ::Concurrent::Map.new - CAN_LDAP = begin - require 'net/ldap' - true - rescue LoadError - false - end - CAN_GOOGLE = begin - require 'google_calendar' - true - rescue LoadError - false - end - - - descriptive_name 'Google Room Bookings' - generic_name :Bookings - implements :logic - - - # The room we are interested in - default_settings({ - update_every: '2m', - - # Moved to System or Zone Setting - # cancel_meeting_after: 900 - - # Card reader IDs if we want to listen for swipe events - card_readers: ['reader_id_1', 'reader_id_2'], - - # Optional LDAP creds for looking up emails - ldap_creds: { - host: 'ldap.org.com', - port: 636, - encryption: { - method: :simple_tls, - tls_options: { - verify_mode: 0 - } - }, - auth: { - method: :simple, - username: 'service account', - password: 'password' - } - }, - tree_base: "ou=User,ou=Accounts,dc=org,dc=com", - - # Optional EWS for creating and removing bookings - ews_creds: [ - 'https://company.com/EWS/Exchange.asmx', - 'service account', - 'password', - { http_opts: { ssl_verify_mode: 0 } } - ], - ews_room: 'room@email.address', - - # Optional EWS for creating and removing bookings - google_organiser_location: 'attendees' - # google_client_id: ENV["GOOGLE_APP_CLIENT_ID"], - # google_secret: ENV["GOOGLE_APP_CLIENT_SECRET"], - # google_scope: ENV['GOOGLE_APP_SCOPE'], - # google_site: ENV["GOOGLE_APP_SITE"], - # google_token_url: ENV["GOOGLE_APP_TOKEN_URL"], - # google_options: { - # site: ENV["GOOGLE_APP_SITE"], - # token_url: ENV["GOOGLE_APP_TOKEN_URL"] - # }, - # google_room: 'room@email.address' - }) - - - def on_load - on_update - end - - def on_update - self[:swiped] ||= 0 - @last_swipe_at = 0 - @use_act_as = setting(:use_act_as) - - self[:hide_all] = setting(:hide_all) || false - self[:touch_enabled] = setting(:touch_enabled) || false - self[:name] = self[:room_name] = setting(:room_name) || system.name - - self[:control_url] = setting(:booking_control_url) || system.config.support_url - self[:booking_controls] = setting(:booking_controls) - self[:booking_catering] = setting(:booking_catering) - self[:booking_hide_details] = setting(:booking_hide_details) - self[:booking_hide_availability] = setting(:booking_hide_availability) - self[:booking_hide_user] = setting(:booking_hide_user) - self[:booking_hide_description] = setting(:booking_hide_description) - self[:booking_hide_timeline] = setting(:booking_hide_timeline) - - # Skype join button available 2min before the start of a meeting - @skype_start_offset = setting(:skype_start_offset) || 120 - - # Skype join button not available in the last 8min of a meeting - @skype_end_offset = setting(:skype_end_offset) || 480 - - # Because restarting the modules results in a 'swipe' of the last read card - ignore_first_swipe = true - - # Is there catering available for this room? - self[:catering] = setting(:catering_system_id) - if self[:catering] - self[:menu] = setting(:menu) - end - - # Do we want to look up the users email address? - if CAN_LDAP - @ldap_creds = setting(:ldap_creds) - if @ldap_creds - encrypt = @ldap_creds[:encryption] - encrypt[:method] = encrypt[:method].to_sym if encrypt && encrypt[:method] - @tree_base = setting(:tree_base) - @ldap_user = @ldap_creds.delete :auth - end - else - logger.warn "net/ldap gem not available" if setting(:ldap_creds) - end - - # Do we want to use exchange web services to manage bookings - if CAN_GOOGLE - logger.debug "Setting GOOGLE" - # :client_id => "237647939013-k5c6ubsa1ddt9o861pinpm2d0hom644j.apps.googleusercontent.com", - # :client_secret => "h0vEuCHMLNH5fTwXj3S8PvkE", - # :calendar => "aca@googexcite.cloud", - # :redirect_url => "http://google.aca.im" - - @google_organiser_location = setting(:google_organiser_location) - @google_client_id = setting(:google_client_id) - @google_secret = setting(:google_client_secret) - @google_redirect_uri = setting(:google_redirect_uri) - @google_refresh_token = setting(:google_refresh_token) - @google_room = (setting(:google_room) || system.email) - # supports: SMTP, PSMTP, SID, UPN (user principle name) - # NOTE:: Using UPN we might be able to remove the LDAP requirement - @google_connect_type = (setting(:google_connect_type) || :SMTP).to_sym - @timezone = setting(:room_timezone) - else - logger.warn "oauth2 gem not available" if setting(:google_creds) - end - - # Load the last known values (persisted to the DB) - self[:waiter_status] = (setting(:waiter_status) || :idle).to_sym - self[:waiter_call] = self[:waiter_status] != :idle - - self[:catering_status] = setting(:last_catering_status) || {} - self[:order_status] = :idle - - self[:last_meeting_started] = setting(:last_meeting_started) - self[:cancel_meeting_after] = setting(:cancel_meeting_after) - - - # unsubscribe to all swipe IDs if any are subscribed - if @subs.present? - @subs.each do |sub| - unsubscribe(sub) - end - - @subs = nil - end - - # Are there any swipe card integrations - if system.exists? :Security - readers = setting(:card_readers) - if readers.present? - security = system[:Security] - - readers = readers.is_a?(Array) ? readers : [readers] - sys = system - @subs = [] - readers.each do |id| - @subs << sys.subscribe(:Security, 1, id.to_s) do |notice| - if ignore_first_swipe - ignore_first_swipe = false - else - swipe_occured(notice.value) - end - end - end - end - end - - fetch_bookings - schedule.clear - schedule.every(setting(:update_every) || '5m') { fetch_bookings } - end - - - def set_light_status(status) - lightbar = system[:StatusLight] - return if lightbar.nil? - - case status.to_sym - when :unavailable - lightbar.colour(:red) - when :available - lightbar.colour(:green) - when :pending - lightbar.colour(:orange) - else - lightbar.colour(:off) - end - end - - - # ====================================== - # Waiter call information - # ====================================== - def waiter_call(state) - status = is_affirmative?(state) - - self[:waiter_call] = status - - # Used to highlight the service button - if status - self[:waiter_status] = :pending - else - self[:waiter_status] = :idle - end - - define_setting(:waiter_status, self[:waiter_status]) - end - - def call_acknowledged - self[:waiter_status] = :accepted - define_setting(:waiter_status, self[:waiter_status]) - end - - - # ====================================== - # Catering Management - # ====================================== - def catering_status(details) - self[:catering_status] = details - - # We'll turn off the green light on the waiter call button - if self[:waiter_status] != :idle && details[:progress] == 'visited' - self[:waiter_call] = false - self[:waiter_status] = :idle - define_setting(:waiter_status, self[:waiter_status]) - end - - define_setting(:last_catering_status, details) - end - - def commit_order(order_details) - self[:order_status] = :pending - status = self[:catering_status] - - if status && status[:progress] == 'visited' - status = status.dup - status[:progress] = 'cleaned' - self[:catering_status] = status - end - - if self[:catering] - sys = system - @oid ||= 1 - systems(self[:catering])[:Orders].add_order({ - id: "#{sys.id}_#{@oid}", - created_at: Time.now.to_i, - room_id: sys.id, - room_name: sys.name, - order: order_details - }) - end - end - - def order_accepted - self[:order_status] = :accepted - end - - def order_complete - self[:order_status] = :idle - end - - - - # ====================================== - # ROOM BOOKINGS: - # ====================================== - def fetch_bookings(*args) - - - # @google_organiser_location = setting(:google_organiser_location) - # @google_client_id = setting(:google_client_id) - # @google_secret = setting(:google_client_secret) - # @google_redirect_uri = setting(:google_redirect_uri) - # @google_refresh_token = setting(:google_refresh_token) - # @google_room = (setting(:google_room) || system.email) - - # client = OAuth2::Client.new(@google_client_id, @google_secret, {site: @google_site, token_url: @google_token_url}) - - # Create an instance of the calendar. - begin - cal = Google::Calendar.new(:client_id => @google_client_id, - :client_secret => @google_client_secret, - :calendar => @google_room, - :redirect_url => @google_redirect_uri # this is what Google uses for 'applications' - ) - cal.connection.login_with_refresh_token(@google_refresh_token) - - events = cal.find_events_in_range(Time.now.midnight, Time.now.tomorrow.midnight, {max_results: 2500}) - rescue Exception => e - logger.debug e.message - logger.debug e.backtrace.inspect - raise e - end - - task { - todays_bookings(events) - }.then(proc { |bookings| - self[:today] = bookings - }, proc { |e| logger.print_error(e, 'error fetching bookings') }) - end - - - # ====================================== - # Meeting Helper Functions - # ====================================== - - def start_meeting(meeting_ref) - self[:last_meeting_started] = meeting_ref - self[:meeting_pending] = meeting_ref - self[:meeting_ending] = false - self[:meeting_pending_notice] = false - define_setting(:last_meeting_started, meeting_ref) - end - - def cancel_meeting(start_time) - task { - delete_ews_booking (start_time / 1000).to_i - }.then(proc { |count| - logger.debug { "successfully removed #{count} bookings" } - - self[:last_meeting_started] = start_time - self[:meeting_pending] = start_time - self[:meeting_ending] = false - self[:meeting_pending_notice] = false - }, proc { |error| - logger.print_error error, 'removing ews booking' - }) - end - - # If last meeting started !== meeting pending then - # we'll show a warning on the in room touch panel - def set_meeting_pending(meeting_ref) - self[:meeting_ending] = false - self[:meeting_pending] = meeting_ref - self[:meeting_pending_notice] = true - end - - # Meeting ending warning indicator - # (When meeting_ending !== last_meeting_started then the warning hasn't been cleared) - # The warning is only displayed when meeting_ending === true - def set_end_meeting_warning(meeting_ref = nil, extendable = false) - if self[:last_meeting_started].nil? || self[:meeting_ending] != (meeting_ref || self[:last_meeting_started]) - self[:meeting_ending] = true - - # Allows meeting ending warnings in all rooms - self[:last_meeting_started] = meeting_ref if meeting_ref - self[:meeting_canbe_extended] = extendable - end - end - - def clear_end_meeting_warning - self[:meeting_ending] = self[:last_meeting_started] - end - # --------- - - def create_meeting(options) - # Check that the required params exist - required_fields = ["start", "end"] - check = required_fields - options.keys - if check != [] - # There are missing required fields - logger.info "Required fields missing: #{check}" - raise "missing required fields: #{check}" - end - - logger.debug "Passed Room options: --------------------" - logger.debug options - logger.debug options.to_json - - req_params = {} - req_params[:room_email] = @ews_room - req_params[:organizer] = options[:user_email] - req_params[:subject] = options[:title] - req_params[:start_time] = Time.at(options[:start].to_i / 1000).utc.iso8601.chop - req_params[:end_time] = Time.at(options[:end].to_i / 1000).utc.iso8601.chop - - - # TODO:: Catch error for booking failure - begin - id = make_google_booking req_params - rescue Exception => e - logger.debug e.message - logger.debug e.backtrace.inspect - raise e - end - - - logger.debug { "successfully created booking: #{id}" } - "Ok" - end - - - protected - - - def swipe_occured(info) - # Update the user details - @last_swipe_at = Time.now.to_i - self[:fullname] = "#{info[:firstname]} #{info[:lastname]}" - self[:username] = info[:staff_id] - email = nil - - if self[:username] && @ldap_creds - email = EMAIL_CACHE[self[:username]] - if email - set_email(email) - logger.debug { "email #{email} found in cache" } - else - # Cache username here as self[:username] might change while we - # looking up the previous username - username = self[:username] - - logger.debug { "looking up email for #{username} - #{self[:fullname]}" } - task { - ldap_lookup_email username - }.then do |email| - if email - logger.debug { "email #{email} found in LDAP" } - EMAIL_CACHE[username] = email - set_email(email) - else - logger.warn "no email found in LDAP for #{username}" - set_email nil - end - end - end - else - logger.warn "no staff ID for user #{self[:fullname]}" - set_email nil - end - end - - def set_email(email) - self[:email] = email - self[:swiped] += 1 - end - - # ==================================== - # LDAP lookup to occur in worker thread - # ==================================== - def ldap_lookup_email(username) - email = EMAIL_CACHE[username] - return email if email - - ldap = Net::LDAP.new @ldap_creds - ldap.authenticate @ldap_user[:username], @ldap_user[:password] if @ldap_user - - login_filter = Net::LDAP::Filter.eq('sAMAccountName', username) - object_filter = Net::LDAP::Filter.eq('objectClass', '*') - treebase = @tree_base - search_attributes = ['mail'] - - email = nil - ldap.bind - ldap.search({ - base: treebase, - filter: object_filter & login_filter, - attributes: search_attributes - }) do |entry| - email = get_attr(entry, 'mail') - end - - # Returns email as a promise - EMAIL_CACHE[username] = email - email - end - - def get_attr(entry, attr_name) - if attr_name != "" && attr_name != nil - entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] - end - end - # ==================================== - - - # ======================================= - # EWS Requests to occur in a worker thread - # ======================================= - def make_google_booking(user_email: nil, subject: 'On the spot booking', room_email:, start_time:, end_time:, organizer:) - - booking_data = { - subject: subject, - start: { dateTime: start_time, timeZone: "UTC" }, - end: { dateTime: end_time, timeZone: "UTC" }, - location: { displayName: @google_room, locationEmailAddress: @google_room }, - attendees: [ emailAddress: { address: organizer, name: "User"}] - }.to_json - - logger.debug "Creating booking:" - logger.debug booking_data - - client = OAuth2::Client.new(@google_client_id, @google_secret, {site: @google_site, token_url: @google_token_url}) - - begin - access_token = client.client_credentials.get_token({ - :scope => @google_scope - # :client_secret => ENV["GOOGLE_APP_CLIENT_SECRET"], - # :client_id => ENV["GOOGLE_APP_CLIENT_ID"] - }).token - rescue Exception => e - logger.debug e.message - logger.debug e.backtrace.inspect - raise e - end - - - # Set out domain, endpoint and content type - domain = 'https://graph.microsoft.com' - host = 'graph.microsoft.com' - endpoint = "/v1.0/users/#{@google_room}/events" - content_type = 'application/json;odata.metadata=minimal;odata.streaming=true' - - # Create the request URI and config - google_api = UV::HttpEndpoint.new(domain, tls_options: {host_name: host}) - headers = { - 'Authorization' => "Bearer #{access_token}", - 'Content-Type' => content_type - } - - # Make the request - response = google_api.post(path: "#{domain}#{endpoint}", body: booking_data, headers: headers).value - - logger.debug response.body - logger.debug response.to_json - logger.debug JSON.parse(response.body)['id'] - - id = JSON.parse(response.body)['id'] - - # Return the booking IDs - id - end - - def delete_ews_booking(delete_at) - now = Time.now - if @timezone - start = now.in_time_zone(@timezone).midnight - ending = now.in_time_zone(@timezone).tomorrow.midnight - else - start = now.midnight - ending = now.tomorrow.midnight - end - - count = 0 - - cli = Viewpoint::EWSClient.new(*@ews_creds) - - if @use_act_as - # TODO:: think this line can be removed?? - delete_at = Time.parse(delete_at.to_s).to_i - - opts = {} - opts[:act_as] = @ews_room if @ews_room - - folder = cli.get_folder(:calendar, opts) - items = folder.items({:calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) - else - cli.set_impersonation(Viewpoint::EWS::ConnectingSID[@ews_connect_type], @ews_room) if @ews_room - items = cli.find_items({:folder_id => :calendar, :calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) - end - - items.each do |meeting| - meeting_time = Time.parse(meeting.ews_item[:start][:text]) - - # Remove any meetings that match the start time provided - if meeting_time.to_i == delete_at - meeting.delete!(:recycle, send_meeting_cancellations: 'SendOnlyToAll') - count += 1 - end - end - - # Return the number of meetings removed - count - end - - def todays_bookings(events) - - events.each{|event| - - # start_time = Time.parse(event['start']['dateTime']).utc.iso8601[0..18] + 'Z' - # end_time = Time.parse(event['end']['dateTime']).utc.iso8601[0..18] + 'Z' - start_time = ActiveSupport::TimeZone.new('UTC').parse(event.start_time).iso8601[0..18] - end_time = ActiveSupport::TimeZone.new('UTC').parse(event.end_time).iso8601[0..18] - - - results.push({ - :Start => start_time, - :End => end_time, - :Subject => event.title, - :owner => organizer - # :setup => 0, - # :breakdown => 0 - }) - } - - logger.info "Got #{results.length} results!" - logger.info results.to_json - - results - end - # ======================================= -end diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 2eca67eb..01d4829d 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -164,6 +164,7 @@ def on_update @google_admin_email = setting(:google_admin_email) @google_scope = setting(:google_scope) @google_room = (setting(:google_room) || system.email) + @google_timezone = setting(:timezone) || "Sydney" # supports: SMTP, PSMTP, SID, UPN (user principle name) # NOTE:: Using UPN we might be able to remove the LDAP requirement @google_connect_type = (setting(:google_connect_type) || :SMTP).to_sym @@ -349,7 +350,7 @@ def fetch_bookings(*args) calendar_api = Google::Apis::CalendarV3 calendar = calendar_api::CalendarService.new calendar.authorization = authorization - events = calendar.list_events(system.email, time_min: Time.now.midnight.iso8601, time_max: Time.now.tomorrow.midnight.iso8601).items + events = calendar.list_events(system.email, time_min: ActiveSupport::TimeZone.new(@google_timezone).now.midnight.iso8601, time_max: ActiveSupport::TimeZone.new(@google_timezone).now.tomorrow.midnight.iso8601).items task { todays_bookings(events) From dc4768148a10eff98297bda2e6ed3b518ab5d4a1 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 17 Oct 2018 13:19:58 +1100 Subject: [PATCH 29/37] Update google module to use library --- modules/aca/google_refresh_booking.rb | 67 +++++++++------------------ 1 file changed, 21 insertions(+), 46 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 01d4829d..abf1b055 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -214,6 +214,11 @@ def on_update end end + @google_client = ::Google::Admin.new({ + admin_email: ENV['GOOGLE_ADMIN_EMAIL'], + domain: ENV['GOOGLE_DOMAIN'] + }) + fetch_bookings schedule.clear schedule.every(setting(:update_every) || '5m') { fetch_bookings } @@ -523,54 +528,24 @@ def get_attr(entry, attr_name) # EWS Requests to occur in a worker thread # ======================================= def make_google_booking(user_email: nil, subject: 'On the spot booking', room_email:, start_time:, end_time:, organizer:) - - booking_data = { - subject: subject, - start: { dateTime: start_time, timeZone: "UTC" }, - end: { dateTime: end_time, timeZone: "UTC" }, - location: { displayName: @google_room, locationEmailAddress: @google_room }, - attendees: [ emailAddress: { address: organizer, name: "User"}] - }.to_json - - logger.debug "Creating booking:" - logger.debug booking_data - - client = OAuth2::Client.new(@google_client_id, @google_secret, {site: @google_site, token_url: @google_token_url}) - - begin - access_token = client.client_credentials.get_token({ - :scope => @google_scope - # :client_secret => ENV["GOOGLE_APP_CLIENT_SECRET"], - # :client_id => ENV["GOOGLE_APP_CLIENT_ID"] - }).token - rescue Exception => e - logger.debug e.message - logger.debug e.backtrace.inspect - raise e + if start_time > 1500000000000 + start_time = (start_time.to_i / 1000).to_i + end_time = (end_time.to_i / 1000).to_i + else + start_time = start_time + end_time = end_time end - - # Set out domain, endpoint and content type - domain = 'https://graph.microsoft.com' - host = 'graph.microsoft.com' - endpoint = "/v1.0/users/#{@google_room}/events" - content_type = 'application/json;odata.metadata=minimal;odata.streaming=true' - - # Create the request URI and config - google_api = UV::HttpEndpoint.new(domain, tls_options: {host_name: host}) - headers = { - 'Authorization' => "Bearer #{access_token}", - 'Content-Type' => content_type - } - - # Make the request - response = google_api.post(path: "#{domain}#{endpoint}", body: booking_data, headers: headers).value - - logger.debug response.body - logger.debug response.to_json - logger.debug JSON.parse(response.body)['id'] - - id = JSON.parse(response.body)['id'] + results = @google.create_booking({ + room_email: room_email, + start_param: start_time, + end_param: end_time, + subject: subject, + current_user: (organizer || nil) + timezone: ENV['TIMEZONE'] || 'Sydney' + }) + + id = results['id'] # Return the booking IDs id From 717c70e32a9f8eec62e625c7ef83385d88067e66 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Sat, 17 Nov 2018 11:41:44 +1100 Subject: [PATCH 30/37] Only impersonate if admin email exists --- lib/google/admin.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/google/admin.rb b/lib/google/admin.rb index afe2881b..66280796 100644 --- a/lib/google/admin.rb +++ b/lib/google/admin.rb @@ -40,7 +40,7 @@ def initialize( def get_users(q: nil, limit: nil) authorization = Google::Auth.get_application_default(@scopes).dup - authorization.sub = @admin_email + authorization.sub = @admin_email if @admin_email @admin.authorization = authorization options = { domain: @domain @@ -62,7 +62,7 @@ def get_users(q: nil, limit: nil) def get_user(user_id:) authorization = Google::Auth.get_application_default(@scopes).dup - authorization.sub = @admin_email + authorization.sub = @admin_email if @admin_email @admin.authorization = authorization options = { domain: @domain @@ -76,7 +76,7 @@ def get_user(user_id:) def get_available_rooms(room_ids:, start_param:, end_param:) authorization = Google::Auth.get_application_default(@scopes).dup - authorization.sub = @admin_email + authorization.sub = @admin_email if @admin_email @calendar.authorization = authorization now = Time.now start_param = ensure_ruby_date((start_param || now)) @@ -100,14 +100,14 @@ def get_available_rooms(room_ids:, start_param:, end_param:) def delete_booking(room_id:, booking_id:) authorization = Google::Auth.get_application_default(@scopes).dup - authorization.sub = @admin_email + authorization.sub = @admin_email if @admin_email @calendar.authorization = authorization @calendar.delete_event(room_id, booking_id) end def get_bookings(email:, start_param:nil, end_param:nil) authorization = Google::Auth.get_application_default(@scopes).dup - authorization.sub = @admin_email + authorization.sub = @admin_email if @admin_email @calendar.authorization = authorization if start_param.nil? start_param = DateTime.now @@ -132,7 +132,7 @@ def get_bookings(email:, start_param:nil, end_param:nil) def create_booking(room_email:, start_param:, end_param:, subject:, description:nil, current_user:, attendees: nil, recurrence: nil, timezone:'Sydney') authorization = Google::Auth.get_application_default(@scopes).dup - authorization.sub = @admin_email + authorization.sub = @admin_email if @admin_email @calendar.authorization = authorization description = String(description) attendees = Array(attendees) From fc50c01c6ee7b8dccf6cd1338e5d049a9896ed48 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 28 Nov 2018 16:19:28 +1100 Subject: [PATCH 31/37] Fix fetch --- modules/aca/google_refresh_booking.rb | 55 +++++++-------------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index abf1b055..abed4e83 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -314,48 +314,22 @@ def order_complete end - - # ====================================== - # ROOM BOOKINGS: - # ====================================== def fetch_bookings(*args) + options = { + client_id: @google_client_id, + client_secret: @google_secret, + scope: @google_scope, + redirect_uri: @google_redirect_uri, + refresh_token: @google_refresh_token, + grant_type: "refresh_token" + } - - # @google_organiser_location = setting(:google_organiser_location) - # @google_client_id = setting(:google_client_id) - # @google_secret = setting(:google_client_secret) - # @google_redirect_uri = setting(:google_redirect_uri) - # @google_refresh_token = setting(:google_refresh_token) - # @google_room = (setting(:google_room) || system.email) - - # client = OAuth2::Client.new(@google_client_id, @google_secret, {site: @google_site, token_url: @google_token_url}) - - - # options = { - # client_id: @google_client_id, - # client_secret: @google_secret, - # scope: @google_scope, - # redirect_uri: @google_redirect_uri, - # refresh_token: @google_refresh_token, - # grant_type: "refresh_token" - # } - # logger.info "AUTHORIZING WITH OPTIONS:" - # STDERR.puts "AUTHORIZING WITH OPTIONS:" - # STDERR.puts options - # logger.info options - # STDERR.puts @google_admin_email - # logger.info @google_admin_email - # STDERR.flush - - # authorization = Google::Auth::UserRefreshCredentials.new options - - authorization = Google::Auth.get_application_default(@google_scope).dup - authorization.sub = @google_admin_email - - calendar_api = Google::Apis::CalendarV3 - calendar = calendar_api::CalendarService.new + authorization = Google::Auth::UserRefreshCredentials.new options + + Calendar = Google::Apis::CalendarV3 + calendar = Calendar::CalendarService.new calendar.authorization = authorization - events = calendar.list_events(system.email, time_min: ActiveSupport::TimeZone.new(@google_timezone).now.midnight.iso8601, time_max: ActiveSupport::TimeZone.new(@google_timezone).now.tomorrow.midnight.iso8601).items + events = calendar.list_events(system.email) task { todays_bookings(events) @@ -363,8 +337,7 @@ def fetch_bookings(*args) self[:today] = bookings }, proc { |e| logger.print_error(e, 'error fetching bookings') }) end - - + # ====================================== # Meeting Helper Functions # ====================================== From cbc2f802850c3bed30fb46440a878e9d8bd96450 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 28 Nov 2018 16:20:25 +1100 Subject: [PATCH 32/37] Fix fetch --- modules/aca/google_refresh_booking.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index abed4e83..8b80081e 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -337,7 +337,7 @@ def fetch_bookings(*args) self[:today] = bookings }, proc { |e| logger.print_error(e, 'error fetching bookings') }) end - + # ====================================== # Meeting Helper Functions # ====================================== @@ -514,7 +514,7 @@ def make_google_booking(user_email: nil, subject: 'On the spot booking', room_em start_param: start_time, end_param: end_time, subject: subject, - current_user: (organizer || nil) + current_user: (organizer || nil), timezone: ENV['TIMEZONE'] || 'Sydney' }) From be1d88edf9779b1a1696a6304722097fd7e29213 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 28 Nov 2018 16:28:38 +1100 Subject: [PATCH 33/37] Fix fetch --- modules/aca/google_refresh_booking.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 8b80081e..68973be3 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -214,8 +214,8 @@ def on_update end end - @google_client = ::Google::Admin.new({ - admin_email: ENV['GOOGLE_ADMIN_EMAIL'], + @google = ::Google::Admin.new({ + admin_email: nil, domain: ENV['GOOGLE_DOMAIN'] }) @@ -330,6 +330,8 @@ def fetch_bookings(*args) calendar = Calendar::CalendarService.new calendar.authorization = authorization events = calendar.list_events(system.email) + + events = @google.get_bookings(email: system.email, start_param: DateTime.now - 1.day, end_param: DateTime.now + 1.day ) task { todays_bookings(events) From 1337134878d9ba704b7dcfe78bd0302c33ac38a9 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 28 Nov 2018 16:29:18 +1100 Subject: [PATCH 34/37] Fix fetch --- modules/aca/google_refresh_booking.rb | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 68973be3..9fa1067d 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -314,23 +314,7 @@ def order_complete end - def fetch_bookings(*args) - options = { - client_id: @google_client_id, - client_secret: @google_secret, - scope: @google_scope, - redirect_uri: @google_redirect_uri, - refresh_token: @google_refresh_token, - grant_type: "refresh_token" - } - - authorization = Google::Auth::UserRefreshCredentials.new options - - Calendar = Google::Apis::CalendarV3 - calendar = Calendar::CalendarService.new - calendar.authorization = authorization - events = calendar.list_events(system.email) - + def fetch_bookings(*args) events = @google.get_bookings(email: system.email, start_param: DateTime.now - 1.day, end_param: DateTime.now + 1.day ) task { From e2bd81a1509819799ddfeb9d8149e605910f86a1 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 28 Nov 2018 16:32:00 +1100 Subject: [PATCH 35/37] Fix fetch --- modules/aca/google_refresh_booking.rb | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 9fa1067d..7111d948 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -139,7 +139,7 @@ def on_update if CAN_LDAP @ldap_creds = setting(:ldap_creds) if @ldap_creds - encrypt = @ldap_creds[:encryption] + encrypt = @ldap_creds[:encryption] encrypt[:method] = encrypt[:method].to_sym if encrypt && encrypt[:method] @tree_base = setting(:tree_base) @ldap_user = @ldap_creds.delete :auth @@ -511,22 +511,9 @@ def make_google_booking(user_email: nil, subject: 'On the spot booking', room_em end def todays_bookings(events) - results = [] - - events.each{|event| - - results.push({ - :Start => event.start.date_time.utc.iso8601, - :End => event.end.date_time.utc.iso8601, - :Subject => event.summary, - :owner => event.organizer.display_name - # :setup => 0, - # :breakdown => 0 - }) - } - logger.info "Got #{results.length} results!" - logger.info results.to_json + logger.info "Got #{events.length} results!" + logger.info events.to_json results end From 638a948b74f8609c7ad090ac9d2982a4a7e48e65 Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Wed, 28 Nov 2018 16:33:11 +1100 Subject: [PATCH 36/37] Fix fetch --- modules/aca/google_refresh_booking.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/aca/google_refresh_booking.rb b/modules/aca/google_refresh_booking.rb index 7111d948..728e378d 100644 --- a/modules/aca/google_refresh_booking.rb +++ b/modules/aca/google_refresh_booking.rb @@ -515,7 +515,7 @@ def todays_bookings(events) logger.info "Got #{events.length} results!" logger.info events.to_json - results + events end def log(data) From 2a110c07d55c35affe13c1062d5aea28354c1e8e Mon Sep 17 00:00:00 2001 From: Cameron Reeves Date: Mon, 13 May 2019 10:12:41 +1000 Subject: [PATCH 37/37] Update exchange related files for google branch --- lib/microsoft/exchange.rb | 441 +++++++++++++++++++++++++ modules/aca/findme_booking.rb | 602 ---------------------------------- 2 files changed, 441 insertions(+), 602 deletions(-) create mode 100644 lib/microsoft/exchange.rb delete mode 100644 modules/aca/findme_booking.rb diff --git a/lib/microsoft/exchange.rb b/lib/microsoft/exchange.rb new file mode 100644 index 00000000..33419d60 --- /dev/null +++ b/lib/microsoft/exchange.rb @@ -0,0 +1,441 @@ +require 'active_support/time' +require 'logger' + +module Microsoft; end + +class Microsoft::Exchange + TIMEZONE_MAPPING = { + "Sydney": "AUS Eastern Standard Time" + } + def initialize( + ews_url:, + service_account_email:, + service_account_password:, + internet_proxy:nil, + hide_all_day_bookings:false, + logger: Rails.logger + ) + begin + require 'viewpoint2' + rescue LoadError + STDERR.puts 'VIEWPOINT NOT PRESENT' + STDERR.flush + end + @ews_url = ews_url + @service_account_email = service_account_email + @service_account_password = service_account_password + @internet_proxy = internet_proxy + @hide_all_day_bookings = hide_all_day_bookings + ews_opts = { http_opts: { ssl_verify_mode: 0 } } + ews_opts[:http_opts][:http_client] = @internet_proxy if @internet_proxy + STDERR.puts '--------------- NEW CLIENT CREATED --------------' + STDERR.puts "At URL: #{@ews_url} with email: #{@service_account_email}" + STDERR.puts '-------------------------------------------------' + @ews_client ||= Viewpoint::EWSClient.new @ews_url, @service_account_email, @service_account_password, ews_opts + end + + def basic_text(field, name) + field[name][:text] + end + + def close + @ews_client.ews.connection.httpcli.reset_all + end + + def username(field, name=nil) + username = field[:email_addresses][:elems][0][:entry][:text].split("@")[0] + if ['smt','sip'].include?(username.downcase[0..2]) + username = username.gsub(/SMTP:|SIP:|sip:|smtp:/,'') + else + username = field[:email_addresses][:elems][-1][:entry][:text].split("@")[0] + if ['smt','sip'].include?(username.downcase[0..2]) + username = username.gsub(/SMTP:|SIP:|sip:|smtp:/,'') + else + username = field[:email_addresses][:elems][1][:entry][:text].split("@")[0] + if ['smt','sip'].include?(username.downcase[0..2]) + username = username.gsub(/SMTP:|SIP:|sip:|smtp:/,'') + else + username = nil + end + end + end + username + end + + def phone_list(field, name=nil) + phone = nil + field[:phone_numbers][:elems].each do |entry| + type = entry[:entry][:attribs][:key] + text = entry[:entry][:text] + + next unless text.present? + + if type == "MobilePhone" + return text + elsif type == "BusinessPhone" || phone.present? + phone = text + end + end + phone + end + + + def get_users(q: nil, limit: nil) + ews_users = @ews_client.search_contacts(q) + users = [] + fields = { + display_name: 'name:basic_text', + phone_numbers: 'phone:phone_list', + culture: 'locale:basic_text', + department: 'department:basic_text', + email_addresses: 'id:username' + } + keys = fields.keys + ews_users.each do |user| + begin + output = {} + user[:resolution][:elems][1][:contact][:elems].each do |field| + key = field.keys[0] + if keys.include?(key) + splits = fields[key].split(':') + output[splits[0]] = self.__send__(splits[1], field, key) + end + end + if output['name'].nil? + output['name'] = user[:resolution][:elems][0][:mailbox][:elems][0][:name][:text] + end + output['email'] = user[:resolution][:elems][0][:mailbox][:elems][1][:email_address][:text] + users.push(output) + rescue => e + STDERR.puts "GOT USER WITHOUT EMAIL" + STDERR.puts user + STDERR.flush + end + end + limit ||= users.length + limit = limit.to_i - 1 + return users[0..limit.to_i] + end + + def get_user(user_id:) + get_users(q: user_id, limit: 1)[0] + end + + def find_time(cal_event, time) + elems = cal_event[:calendar_event][:elems] + start_time = nil + elems.each do |item| + if item[time] + start_time = ActiveSupport::TimeZone.new("UTC").parse(item[time][:text]) + break + end + end + start_time + end + + def get_available_rooms(rooms:, start_time:, end_time:) + free_rooms = [] + start_time = start_time.utc + end_time = end_time.utc + STDERR.puts "Getting available rooms with" + STDERR.puts start_time + STDERR.puts end_time + STDERR.flush + + rooms.each_slice(30).each do |room_subset| + + # Get booking data for all rooms between time range bounds + user_free_busy = @ews_client.get_user_availability(room_subset, + start_time: start_time, + end_time: end_time, + requested_view: :detailed, + time_zone: { + bias: -0, + standard_time: { + bias: 0, + time: "03:00:00", + day_order: 1, + month: 10, + day_of_week: 'Sunday' + }, + daylight_time: { + bias: 0, + time: "02:00:00", + day_order: 1, + month: 4, + day_of_week: 'Sunday' + } + } + ) + + user_free_busy.body[0][:get_user_availability_response][:elems][0][:free_busy_response_array][:elems].each_with_index {|r,index| + # Remove meta data (business hours and response type) + resp = r[:free_busy_response][:elems][1][:free_busy_view][:elems].delete_if { |item| item[:free_busy_view_type] || item[:working_hours] } + + # Back to back meetings still show up so we need to remove these from the results + if resp.length == 1 + resp = resp[0][:calendar_event_array][:elems] + + if resp.length == 0 + free_rooms.push(room_subset[index]) + elsif resp.length == 1 + free_rooms.push(room_subset[index]) if find_time(resp[0], :start_time).to_i == end_time.to_i + free_rooms.push(room_subset[index]) if find_time(resp[0], :end_time).to_i == start_time.to_i + end + elsif resp.length == 0 + # If response length is 0 then the room is free + free_rooms.push(room_subset[index]) + end + } + end + + free_rooms + end + + def get_bookings(email:, start_param:DateTime.now.midnight, end_param:(DateTime.now.midnight + 2.days), use_act_as: false) + begin + # Get all the room emails + room_emails = Orchestrator::ControlSystem.all.to_a.map { |sys| sys.email } + if [Integer, String].include?(start_param.class) + start_param = DateTime.parse(Time.at(start_param.to_i / 1000).to_s) + end_param = DateTime.parse(Time.at(end_param.to_i / 1000).to_s) + end + STDERR.puts '---------------- GETTING BOOKINGS ---------------' + STDERR.puts "At email: #{email} with start: #{start_param} and end: #{end_param}" + STDERR.puts '-------------------------------------------------' + bookings = [] + if use_act_as + calendar_id = @ews_client.get_folder(:calendar, opts = {act_as: email }).id + events = @ews_client.find_items(folder_id: calendar_id, calendar_view: {start_date: start_param, end_date: end_param}) + else + @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], email) + events = @ews_client.find_items({:folder_id => :calendar, :calendar_view => {:start_date => start_param.utc.iso8601, :end_date => end_param.utc.iso8601}}) + end + # events = @ews_client.get_item(:calendar, opts = {act_as: email}).items + events.each{|event| + event.get_all_properties! + internal_domain = Mail::Address.new(event.organizer.email).domain + booking = {} + booking[:subject] = event.subject + booking[:title] = event.subject + booking[:id] = event.id + # booking[:start_date] = event.start.utc.iso8601 + # booking[:end_date] = event.end.utc.iso8601 + booking[:start] = event.start.to_i * 1000 + booking[:end] = event.end.to_i * 1000 + booking[:body] = event.body + booking[:organiser] = { + name: event.organizer.name, + email: event.organizer.email + } + booking[:attendees] = event.required_attendees.map {|attendee| + if room_emails.include?(attendee.email) + booking[:room_id] = attendee.email + next + end + email_domain = Mail::Address.new(attendee.email).domain + { + name: attendee.name, + email: attendee.email, + visitor: (internal_domain != email_domain), + organisation: attendee.email.split('@')[1..-1].join("").split('.')[0].capitalize + } + }.compact if event.required_attendees + if @hide_all_day_bookings + STDERR.puts "SKIPPING #{event.subject}" + STDERR.flush + next if event.end.to_time - event.start.to_time > 86399 + end + bookings.push(booking) + } + bookings + rescue Exception => msg + STDERR.puts msg + STDERR.flush + return [] + end + end + + def create_booking(room_email:, start_param:, end_param:, subject:, description:nil, current_user:, attendees: nil, timezone:'Sydney', permission: 'impersonation', mailbox_location: 'user') + STDERR.puts "CREATING NEW BOOKING IN LIBRARY" + STDERR.puts "room_email is #{room_email}" + STDERR.puts "start_param is #{start_param}" + STDERR.puts "end_param is #{end_param}" + STDERR.puts "subject is #{subject}" + STDERR.puts "description is #{description}" + STDERR.puts "current_user is #{current_user}" + STDERR.puts "attendees is #{attendees}" + STDERR.puts "timezone is #{timezone}" + STDERR.flush + # description = String(description) + attendees = Array(attendees) + + + booking = {} + + # Allow for naming of subject or title + booking[:subject] = subject + booking[:title] = subject + booking[:location] = Orchestrator::ControlSystem.find_by_email(room_email).name + + + # Set the room email as a resource + booking[:resources] = [{ + attendee: { + mailbox: { + email_address: room_email + } + } + }] + + # start_object = Time.at(start_param.to_i) + # end_object = Time.at(end_param.to_i) + + start_object = ensure_ruby_date(start_param.to_i) + end_object = ensure_ruby_date(end_param.to_i) + + + # Add start and end times + booking[:start] = start_object.utc.iso8601.chop + booking[:end] = end_object.utc.iso8601.chop + + # Add the current user passed in as an attendee + mailbox = { email_address: current_user.email } + mailbox[:name] = current_user.name if current_user.name + booking[:required_attendees] = [{ + attendee: { mailbox: mailbox } + }] + + # Add the attendees + attendees.each do |attendee| + # If we don't have an array of emails then it's an object in the form {email: "a@b.com", name: "Blahman Blahson"} + if attendee.class != String + attendee = attendee['email'] + end + booking[:required_attendees].push({ + attendee: { mailbox: { email_address: attendee}} + }) + end + + # Add the room as an attendee (it seems as if some clients require this and others don't) + booking[:required_attendees].push({ attendee: { mailbox: { email_address: room_email}}}) + booking[:body] = description + + # A little debugging + STDERR.puts "MAKING REQUEST WITH" + STDERR.puts booking + STDERR.flush + + if mailbox_location == 'user' + mailbox = current_user.email + elsif mailbox_location == 'room' + mailbox = room_email + end + + # Determine whether to use delegation, impersonation or neither + if permission == 'delegation' + folder = @ews_client.get_folder(:calendar, { act_as: mailbox }) + elsif permission == 'impersonation' + @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], mailbox) + folder = @ews_client.get_folder(:calendar) + elsif permission == 'none' || permission.nil? + folder = @ews_client.get_folder(:calendar) + end + + # Create the booking and return data relating to it + appointment = folder.create_item(booking) + { + id: appointment.id, + start: start_param, + end: end_param, + attendees: attendees, + subject: subject + } + end + + def update_booking(booking_id:, room_email:nil, start_param:nil, end_param:nil, subject:nil, description:nil, current_user:nil, attendees: nil, timezone:'Sydney', permission: 'impersonation', mailbox_location: 'user') + + event = @ews_client.get_item(booking_id) + booking = {} + + # Add attendees if passed in + attendees = Array(attendees) + attendees.each do |attendee| + if attendee.class != String + attendee = attendee['email'] + end + booking[:required_attendees] ||= [] + booking[:required_attendees].push({ + attendee: { mailbox: { email_address: attendee}} + }) + end if attendees && !attendees.empty? + + # Add subject or title + booking[:subject] = subject if subject + booking[:title] = subject if subject + + # Add location + booking[:location] = Orchestrator::ControlSystem.find_by_email(room_email).name if room_email + + # Add new times if passed + booking[:start] = Time.at(start_param.to_i / 1000).utc.iso8601.chop if start_param + booking[:end] = Time.at(end_param.to_i / 1000).utc.iso8601.chop if end_param + + if mailbox_location == 'user' + mailbox = current_user.email + elsif mailbox_location == 'room' + mailbox = room_email + end + + if permission == 'impersonation' + @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], mailbox) + end + + new_booking = event.update_item!(booking) + + + { + id: new_booking.id, + start: new_booking.start, + end: new_booking.end, + attendees: new_booking.required_attendees, + subject: new_booking.subject + } + end + + def delete_booking(booking_id:, mailbox:) + @ews_client.set_impersonation(Viewpoint::EWS::ConnectingSID[:SMTP], mailbox) + booking = @ews_client.get_item(booking_id) + booking.delete!(:recycle, send_meeting_cancellations: "SendOnlyToAll") + 200 + end + + # Takes a date of any kind (epoch, string, time object) and returns a time object + def ensure_ruby_date(date) + if !(date.class == Time || date.class == DateTime) + if string_is_digits(date) + + # Convert to an integer + date = date.to_i + + # If JavaScript epoch remove milliseconds + if date.to_s.length == 13 + date /= 1000 + end + + # Convert to datetimes + date = Time.at(date) + else + date = Time.parse(date) + end + end + return date + end + + # Returns true if a string is all digits (used to check for an epoch) + def string_is_digits(string) + string = string.to_s + string.scan(/\D/).empty? + end + +end \ No newline at end of file diff --git a/modules/aca/findme_booking.rb b/modules/aca/findme_booking.rb deleted file mode 100644 index 3a13016a..00000000 --- a/modules/aca/findme_booking.rb +++ /dev/null @@ -1,602 +0,0 @@ -# For rounding up to the nearest 15min -# See: http://stackoverflow.com/questions/449271/how-to-round-a-time-down-to-the-nearest-15-minutes-in-ruby -class ActiveSupport::TimeWithZone - def ceil(seconds = 60) - return self if seconds.zero? - Time.at(((self - self.utc_offset).to_f / seconds).ceil * seconds).in_time_zone + self.utc_offset - end -end - - -module Aca; end - -# NOTE:: Requires Settings: -# ======================== -# room_alias: 'rs.au.syd.L16Aitken', -# building: 'DP3', -# level: '16' - -class Aca::FindmeBooking - include ::Orchestrator::Constants - EMAIL_CACHE = ::Concurrent::Map.new - CAN_LDAP = begin - require 'net/ldap' - true - rescue LoadError - false - end - CAN_EWS = begin - require 'viewpoint2' - true - rescue LoadError - begin - require 'viewpoint' - true - rescue LoadError - false - end - false - end - - - descriptive_name 'Findme Room Bookings' - generic_name :Bookings - implements :logic - - - # The room we are interested in - default_settings({ - update_every: '5m', - - # Moved to System or Zone Setting - # cancel_meeting_after: 900 - - # Card reader IDs if we want to listen for swipe events - card_readers: ['reader_id_1', 'reader_id_2'], - - # Optional LDAP creds for looking up emails - ldap_creds: { - host: 'ldap.org.com', - port: 636, - encryption: { - method: :simple_tls, - tls_options: { - verify_mode: 0 - } - }, - auth: { - method: :simple, - username: 'service account', - password: 'password' - } - }, - tree_base: "ou=User,ou=Accounts,dc=org,dc=com", - - # Optional EWS for creating and removing bookings - ews_creds: [ - 'https://company.com/EWS/Exchange.asmx', - 'service account', - 'password', - { http_opts: { ssl_verify_mode: 0 } } - ], - ews_room: 'room@email.address' - }) - - - def on_load - on_update - end - - def on_update - self[:swiped] ||= 0 - @last_swipe_at = 0 - @use_act_as = setting(:use_act_as) - - self[:building] = setting(:building) - self[:level] = setting(:level) - self[:room] = setting(:room) - self[:touch_enabled] = setting(:touch_enabled) || false - self[:room_name] = setting(:room_name) || system.name - - # Skype join button available 2min before the start of a meeting - @skype_start_offset = setting(:skype_start_offset) || 120 - - # Skype join button not available in the last 8min of a meeting - @skype_end_offset = setting(:skype_end_offset) || 480 - - # Because restarting the modules results in a 'swipe' of the last read card - ignore_first_swipe = true - - # Is there catering available for this room? - self[:catering] = setting(:catering_system_id) - if self[:catering] - self[:menu] = setting(:menu) - end - - # Do we want to look up the users email address? - if CAN_LDAP - @ldap_creds = setting(:ldap_creds) - if @ldap_creds - encrypt = @ldap_creds[:encryption] - encrypt[:method] = encrypt[:method].to_sym if encrypt && encrypt[:method] - @tree_base = setting(:tree_base) - @ldap_user = @ldap_creds.delete :auth - end - else - logger.warn "net/ldap gem not available" if setting(:ldap_creds) - end - - # Do we want to use exchange web services to manage bookings - if CAN_EWS - @ews_creds = setting(:ews_creds) - @ews_room = (setting(:ews_room) || system.email) if @ews_creds - # supports: SMTP, PSMTP, SID, UPN (user principle name) - # NOTE:: Using UPN we might be able to remove the LDAP requirement - @ews_connect_type = (setting(:ews_connect_type) || :SMTP).to_sym - @timezone = setting(:room_timezone) - else - logger.warn "viewpoint gem not available" if setting(:ews_creds) - end - - # Load the last known values (persisted to the DB) - self[:waiter_status] = (setting(:waiter_status) || :idle).to_sym - self[:waiter_call] = self[:waiter_status] != :idle - - self[:catering_status] = setting(:last_catering_status) || {} - self[:order_status] = :idle - - self[:last_meeting_started] = setting(:last_meeting_started) - self[:cancel_meeting_after] = setting(:cancel_meeting_after) - - - # unsubscribe to all swipe IDs if any are subscribed - if @subs.present? - @subs.each do |sub| - unsubscribe(sub) - end - - @subs = nil - end - - # Are there any swipe card integrations - if system.exists? :Security - readers = setting(:card_readers) - if readers.present? - security = system[:Security] - - readers = Array(readers) - sys = system - @subs = [] - readers.each do |id| - @subs << sys.subscribe(:Security, 1, id.to_s) do |notice| - if ignore_first_swipe - ignore_first_swipe = false - else - swipe_occured(notice.value) - end - end - end - end - end - - fetch_bookings - schedule.clear - schedule.every(setting(:update_every) || '5m') { fetch_bookings } - end - - - # ====================================== - # Waiter call information - # ====================================== - def waiter_call(state) - status = is_affirmative?(state) - - self[:waiter_call] = status - - # Used to highlight the service button - if status - self[:waiter_status] = :pending - else - self[:waiter_status] = :idle - end - - define_setting(:waiter_status, self[:waiter_status]) - end - - def call_acknowledged - self[:waiter_status] = :accepted - define_setting(:waiter_status, self[:waiter_status]) - end - - - # ====================================== - # Catering Management - # ====================================== - def catering_status(details) - self[:catering_status] = details - - # We'll turn off the green light on the waiter call button - if self[:waiter_status] != :idle && details[:progress] == 'visited' - self[:waiter_call] = false - self[:waiter_status] = :idle - define_setting(:waiter_status, self[:waiter_status]) - end - - define_setting(:last_catering_status, details) - end - - def commit_order(order_details) - self[:order_status] = :pending - status = self[:catering_status] - - if status && status[:progress] == 'visited' - status = status.dup - status[:progress] = 'cleaned' - self[:catering_status] = status - end - - if self[:catering] - sys = system - @oid ||= 1 - systems(self[:catering])[:Orders].add_order({ - id: "#{sys.id}_#{@oid}", - created_at: Time.now.to_i, - room_id: sys.id, - room_name: sys.name, - order: order_details - }) - end - end - - def order_accepted - self[:order_status] = :accepted - end - - def order_complete - self[:order_status] = :idle - end - - - - # ====================================== - # ROOM BOOKINGS: - # ====================================== - def fetch_bookings(*args) - logger.debug { "looking up todays emails for #{@ews_room}" } - task { - todays_bookings - }.then(proc { |bookings| - self[:today] = bookings - }, proc { |e| logger.print_error(e, 'error fetching bookings') }) - end - - - # ====================================== - # Meeting Helper Functions - # ====================================== - - def start_meeting(meeting_ref) - self[:last_meeting_started] = meeting_ref - self[:meeting_pending] = meeting_ref - self[:meeting_ending] = false - self[:meeting_pending_notice] = false - define_setting(:last_meeting_started, meeting_ref) - end - - def cancel_meeting(start_time, *args) - task { - if start_time.is_a?(String) - start_time = start_time.chop - start_time = Time.parse(start_time).to_i * 1000 - end - delete_ews_booking (start_time / 1000).to_i - }.then(proc { |count| - logger.debug { "successfully removed #{count} bookings" } - - self[:last_meeting_started] = start_time - self[:meeting_pending] = start_time - self[:meeting_ending] = false - self[:meeting_pending_notice] = false - }, proc { |error| - logger.print_error error, 'removing ews booking' - }) - end - - # If last meeting started !== meeting pending then - # we'll show a warning on the in room touch panel - def set_meeting_pending(meeting_ref) - self[:meeting_ending] = false - self[:meeting_pending] = meeting_ref - self[:meeting_pending_notice] = true - end - - # Meeting ending warning indicator - # (When meeting_ending !== last_meeting_started then the warning hasn't been cleared) - # The warning is only displayed when meeting_ending === true - def set_end_meeting_warning(meeting_ref = nil, extendable = false) - if self[:last_meeting_started].nil? || self[:meeting_ending] != (meeting_ref || self[:last_meeting_started]) - self[:meeting_ending] = true - - schedule.in('30s') do - clear_end_meeting_warning - end - - # Allows meeting ending warnings in all rooms - self[:last_meeting_started] = meeting_ref if meeting_ref - self[:meeting_canbe_extended] = extendable - end - end - - def clear_end_meeting_warning - self[:meeting_ending] = self[:last_meeting_started] - end - # --------- - - def create_meeting(duration, next_start = nil) - if next_start - if next_start.is_a? Integer - next_start = Time.at((next_start / 1000).to_i) - else - next_start = Time.parse(next_start.split(/z/i)[0]) - end - end - - start_time = Time.now - end_time = duration.to_i.minutes.from_now.ceil(15.minutes) - - # Make sure we don't overlap the next booking - if next_start && next_start < end_time - end_time = next_start - end - - task { - make_ews_booking start_time, end_time - }.then(proc { |id| - logger.debug { "successfully created booking: #{id}" } - # We want to start the meeting automatically - start_meeting(start_time.to_i * 1000) - }, proc { |error| - logger.print_error error, 'creating ad hoc booking' - }) - end - - - protected - - - def swipe_occured(info) - # Update the user details - @last_swipe_at = Time.now.to_i - self[:fullname] = "#{info[:firstname]} #{info[:lastname]}" - self[:username] = info[:staff_id] - email = nil - - if self[:username] && @ldap_creds - email = EMAIL_CACHE[self[:username]] - if email - set_email(email) - logger.debug { "email #{email} found in cache" } - else - # Cache username here as self[:username] might change while we - # looking up the previous username - username = self[:username] - - logger.debug { "looking up email for #{username} - #{self[:fullname]}" } - task { - ldap_lookup_email username - }.then do |email| - if email - logger.debug { "email #{email} found in LDAP" } - EMAIL_CACHE[username] = email - set_email(email) - else - logger.warn "no email found in LDAP for #{username}" - set_email nil - end - end - end - else - logger.warn "no staff ID for user #{self[:fullname]}" - set_email nil - end - rescue => e - logger.print_error(e, 'error handling card swipe') - end - - def set_email(email) - self[:email] = email - self[:swiped] += 1 - end - - # ==================================== - # LDAP lookup to occur in worker thread - # ==================================== - def ldap_lookup_email(username) - ldap = Net::LDAP.new @ldap_creds - ldap.authenticate @ldap_user[:username], @ldap_user[:password] if @ldap_user - - login_filter = Net::LDAP::Filter.eq('sAMAccountName', username) - object_filter = Net::LDAP::Filter.eq('objectClass', '*') - treebase = @tree_base - search_attributes = ['mail'] - - email = nil - ldap.bind - ldap.search({ - base: treebase, - filter: object_filter & login_filter, - attributes: search_attributes - }) do |entry| - email = get_attr(entry, 'mail') - end - - # Returns email as a promise - email - rescue => e - logger.print_error(e, 'error looking up email') - end - - def get_attr(entry, attr_name) - if attr_name != "" && attr_name != nil - entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] - end - end - # ==================================== - - - # ======================================= - # EWS Requests to occur in a worker thread - # ======================================= - def make_ews_booking(start_time, end_time) - subject = 'On the spot booking' - user_email = self[:email] - - booking = { - subject: subject, - start: start_time, - end: end_time - } - - if user_email - booking[:required_attendees] = [{ - attendee: { mailbox: { email_address: user_email } } - }] - end - - cli = Viewpoint::EWSClient.new(*@ews_creds) - opts = {} - - if @use_act_as - opts[:act_as] = @ews_room if @ews_room - else - cli.set_impersonation(Viewpoint::EWS::ConnectingSID[@ews_connect_type], @ews_room) if @ews_room - end - - folder = cli.get_folder(:calendar, opts) - appointment = folder.create_item(booking) - - # Return the booking IDs - appointment.item_id - end - - def delete_ews_booking(delete_at) - now = Time.now - if @timezone - start = now.in_time_zone(@timezone).midnight - ending = now.in_time_zone(@timezone).tomorrow.midnight - else - start = now.midnight - ending = now.tomorrow.midnight - end - - count = 0 - - cli = Viewpoint::EWSClient.new(*@ews_creds) - - if @use_act_as - # TODO:: think this line can be removed?? - delete_at = Time.parse(delete_at.to_s).to_i - - opts = {} - opts[:act_as] = @ews_room if @ews_room - - folder = cli.get_folder(:calendar, opts) - items = folder.items({:calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) - else - cli.set_impersonation(Viewpoint::EWS::ConnectingSID[@ews_connect_type], @ews_room) if @ews_room - items = cli.find_items({:folder_id => :calendar, :calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) - end - - items.each do |meeting| - meeting_time = Time.parse(meeting.ews_item[:start][:text]) - - # Remove any meetings that match the start time provided - if meeting_time.to_i == delete_at - meeting.delete!(:recycle, send_meeting_cancellations: 'SendOnlyToAll') - count += 1 - end - end - - # Return the number of meetings removed - count - end - - def todays_bookings - now = Time.now - if @timezone - start = now.in_time_zone(@timezone).midnight - ending = now.in_time_zone(@timezone).tomorrow.midnight - else - start = now.midnight - ending = now.tomorrow.midnight - end - - cli = Viewpoint::EWSClient.new(*@ews_creds) - - if @use_act_as - opts = {} - opts[:act_as] = @ews_room if @ews_room - - folder = cli.get_folder(:calendar, opts) - items = folder.items({:calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) - else - cli.set_impersonation(Viewpoint::EWS::ConnectingSID[@ews_connect_type], @ews_room) if @ews_room - items = cli.find_items({:folder_id => :calendar, :calendar_view => {:start_date => start.utc.iso8601, :end_date => ending.utc.iso8601}}) - end - - skype_exists = set_skype_url = system.exists?(:Skype) - now_int = now.to_i - - items.select! { |booking| !booking.cancelled? } - results = items.collect do |meeting| - item = meeting.ews_item - start = item[:start][:text] - ending = item[:end][:text] - - # Extract the skype meeting URL - if set_skype_url - real_start = Time.parse(start) - start_integer = real_start.to_i - @skype_start_offset - real_end = Time.parse(ending) - end_integer = real_end.to_i - @skype_end_offset - - if now_int > start_integer && now_int < end_integer - meeting.get_all_properties! - - if meeting.body - # Lync: - # Skype: - body_parts = meeting.body.split('OutJoinLink"') - if body_parts.length > 1 - links = body_parts[-1].split('"').select { |link| link.start_with?('https://') } - if links[0].present? - set_skype_url = false - system[:Skype].set_uri(links[0]) - end - end - end - end - - if @timezone - start = real_start.in_time_zone(@timezone).iso8601[0..18] - ending = real_end.in_time_zone(@timezone).iso8601[0..18] - end - elsif @timezone - start = Time.parse(start).in_time_zone(@timezone).iso8601[0..18] - ending = Time.parse(ending).in_time_zone(@timezone).iso8601[0..18] - end - - { - :Start => start, - :End => ending, - :Subject => item[:subject][:text], - :owner => item[:organizer][:elems][0][:mailbox][:elems][0][:name][:text] - } - end - - system[:Skype].set_uri(nil) if skype_exists && set_skype_url - - results - end - # ======================================= -end