Created
April 20, 2015 13:02
-
-
Save rubypirate/f12595336e51628c0510 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- encoding : utf-8 -*- | |
require 'ri_cal' | |
require 'icalendar' | |
require 'google/api_client' | |
module Icalendar | |
# add summary to freebusy - because Google use it! | |
class Freebusy < Component | |
ical_property :summary | |
end | |
end | |
class Cal < ActiveRecord::Base | |
belongs_to :company | |
attr_accessor :event_cache | |
ICAL = 0 | |
################################################################## | |
# ical stuff | |
def get_feed_data | |
return nil if !ical_link | |
return nil if !ical_link['http'] | |
begin | |
url = URI.split(ical_link) | |
if ical_link['https'] | |
http = Net::HTTP.new(url[2], 443, nil, nil) | |
http.use_ssl = true | |
http.verify_mode = OpenSSL::SSL::VERIFY_NONE | |
else | |
http = Net::HTTP.new(url[2], 80, nil, nil) | |
end | |
response = http.request_get(url[5], nil) | |
case response | |
when Net::HTTPRedirection | |
# follow a redirect | |
url = URI.split(response['location']) | |
if url[0] == 'https' | |
http = Net::HTTP.new(url[2], 443, nil, nil) | |
http.use_ssl = true | |
http.verify_mode = OpenSSL::SSL::VERIFY_NONE | |
else | |
http = Net::HTTP.new(url[2], 80, nil, nil) | |
end | |
response = http.request_get(url[5], nil) | |
case response | |
when Net::HTTPSuccess | |
return response.body.force_encoding("utf-8") | |
else | |
return nil | |
end | |
when Net::HTTPSuccess | |
return response.body.force_encoding("utf-8") | |
else | |
return nil | |
end | |
rescue | |
return nil | |
end | |
end | |
######################################################################################################################### | |
# links to external calenders ? | |
def hasExternalCal? | |
return true if self.google_cal | |
return false | |
end | |
def readOnly? | |
self.read_only ? true : false | |
end | |
def external_cal_update_event space | |
if self.google_cal && !readOnly? | |
return external_cal_add_event space if space.mset['google_event_id'].blank? | |
ical_info = space.slot.to_ical(true) | |
google_res = google_edit_event(space.mset['google_event_id'], ical_info.dtstart, ical_info.dtend, ical_info.summary, ical_info.location ) | |
if google_res && google_res['status'] == "confirmed" && google_res['id'] != '' | |
space.mset['google_event_id'] = google_res['id'] | |
space.save | |
end | |
end | |
end | |
def external_cal_add_event space | |
if self.google_cal && !readOnly? | |
ical_info = space.to_ical(true) | |
google_res = google_add_event(ical_info.dtstart, ical_info.dtend, ical_info.summary, ical_info.location, "http://#{get_domain}/view/dashboard/index?goto_space_id=#{space.id}" ) | |
if google_res && google_res['status'] && google_res['status'] == "confirmed" && google_res['id'] && google_res['id'] != '' | |
space.mset['google_event_id'] = google_res['id'] | |
space.save | |
end | |
end | |
end | |
# msg = response.response_messages.first.message | |
# if msg[:attribs] && msg[:attribs][:response_class] && msg[:attribs][:response_class] == "Success" && msg[:elems][:response_code][:text] == "NoError" | |
# space.mset['ews_change_key'] = ews_change_key(space.mset['ews_event_id']) if ews_change_key(space.mset['ews_event_id']) | |
# space.save | |
# end | |
def external_cal_delete_event space | |
if self.google_cal && !readOnly? | |
if space.mset['google_event_id'] && space.mset['google_event_id'] != '' | |
google_res = google_delete_event(space.mset['google_event_id']) | |
end | |
end | |
end | |
############################################################################################################################# | |
# google cal syncing | |
def google_update_cal | |
cal = nil | |
year = Time.now.year | |
self.cache = google_busy | |
self.cache_time = Time.now | |
cal = save! | |
#blank the timezone | |
Time.zone = nil | |
cal | |
end | |
def google_request_push | |
# first update the cal | |
google_update_cal | |
# now subscribe to updates | |
client = company.conf_connection.init_client | |
return [] unless client | |
service = client.discovered_api('calendar', 'v3') | |
result = client.execute( | |
:api_method => service.events.watch, | |
:parameters => {:calendarId=>self.google_cal}, | |
:body_object => {:id => self.id, :type => 'web_hook', :address => "https://#{get_domain()}/google_watch"}, | |
:headers => {'Content-Type' => 'application/json'}) | |
end | |
def google_busy | |
return nil if !google_cal | |
tz = company.time_zone.to_s.length > 0 ? company.time_zone : get_time_zone | |
client = company.conf_connection.init_client | |
if client | |
service = client.discovered_api('calendar', 'v3') | |
begin | |
result = client.execute( | |
:api_method => service.events.list, | |
:parameters => {'calendarId' => google_cal, 'timeZone' => tz, 'timeMin' => (Time.now - 10.days).utc.iso8601, 'timeMax' => 2.months.from_now.utc.iso8601 }, | |
:headers => {'Content-Type' => 'application/json'}) | |
rescue Exception => e | |
end | |
if result && result.body | |
body = JSON.parse(result.body.force_encoding("utf-8")) | |
items = (body['items'] && body['items'].any?) ? body['items'] : [] | |
out = "" | |
items.each_with_index do |item, i| | |
next if item['transparency'] && item['transparency'] == 'transparent' | |
if item['start'] && item['start']['dateTime'] && item['start']['dateTime'] != '' | |
d1 = item['start']['dateTime'].to_datetime | |
d2 = item['end']['dateTime'].to_datetime | |
out += d1.utc.strftime("%Y%m%dT%H%M%SZ") + "-" + d2.utc.strftime("%Y%m%dT%H%M%SZ") + "\n" | |
out += "-" + item['summary'].to_s.gsub(/\n/, " ") + "\n" | |
end | |
end # each end | |
end | |
return out | |
end | |
end | |
def google_freebusy | |
return nil if !google_cal | |
tz = company.time_zone.to_s.length > 0 ? company.time_zone : get_time_zone | |
client = company.conf_connection.init_client | |
if client | |
service = client.discovered_api('calendar', 'v3') | |
result = client.execute( | |
:api_method => service.freebusy.query, | |
:body_object => { :timeMin => (Time.now - 10.days).utc.iso8601, | |
:timeMax => 2.months.from_now.utc.iso8601, # 2 years at the moment - could make it configuarable later! ... had to limit it to 2 months as over than that google will error with message The requested time range is too long | |
:items => [{id: google_cal}] | |
}, | |
:headers => {'Content-Type' => 'application/json'}) | |
body = JSON.parse(result.body.force_encoding("utf-8")) | |
busy_days = body['calendars'][google_cal]['errors'] ? [] : body['calendars'][google_cal]['busy'] | |
out = "" | |
busy_days.each_with_index do |day, i| | |
d1 = day['start'].to_datetime | |
d2 = day['end'].to_datetime | |
out += d1.utc.strftime("%Y%m%dT%H%M%SZ") + "-" + d2.utc.strftime("%Y%m%dT%H%M%SZ") + "\n" | |
end | |
return out | |
end | |
end | |
def google_add_event start_date_time=(Time.now + 2.days).utc.iso8601, end_date_time=(Time.now + 2.days).utc.iso8601, summary='', location='', url=nil | |
client = company.conf_connection.init_client | |
return {} unless client | |
service = client.discovered_api('calendar', 'v3') | |
begin | |
result = client.execute( | |
:api_method => service.events.insert, | |
:parameters => {'calendarId' => self.google_cal}, | |
:body_object => { | |
:start => {:dateTime => DateTime.parse(start_date_time).utc.iso8601}, | |
:end => {:dateTime => DateTime.parse(end_date_time).utc.iso8601}, | |
#:recurrence => ['RRULE:FREQ=WEEKLY;UNTIL=20110701T100000-07:00'], | |
#'attendees' => [:email=>'[email protected]'], | |
:location => location, | |
:summary => summary, | |
:source =>{:url=>url, :title=>"Edit Appointment on BookingBug"} | |
}, | |
:headers => {'Content-Type' => 'application/json'}) | |
return body = JSON.parse(result.body.force_encoding("utf-8")) | |
rescue Exception => error | |
# Airbrake exception | |
Airbrake.notify(error, | |
:backtrace => error.backtrace, | |
:error_class => error.class, | |
:error_message => error.message, | |
:parameters => "#{start_date_time} - #{end_date_time} - #{summary} - #{location} - #{url} - #{self.google_cal}" | |
) | |
end | |
return {} | |
end | |
def google_edit_event event_id, start_date_time=(Time.now + 2.days).utc.iso8601, end_date_time=(Time.now + 2.days).utc.iso8601, summary='', location='' | |
client = company.conf_connection.init_client | |
return [] unless client | |
service = client.discovered_api('calendar', 'v3') | |
begin | |
result = client.execute( | |
:api_method => service.events.insert, | |
:parameters => {'calendarId' => self.google_cal, 'eventId' => event_id}, | |
:body_object => { | |
:start => {:dateTime => DateTime.parse(start_date_time).utc.iso8601}, | |
:end => {:dateTime => DateTime.parse(end_date_time).utc.iso8601}, | |
#:recurrence => ['RRULE:FREQ=WEEKLY;UNTIL=20110701T100000-07:00'], | |
#'attendees' => [:email=>'[email protected]'], | |
:location => location, | |
:summary => summary | |
}, | |
:headers => {'Content-Type' => 'application/json'}) | |
return JSON.parse result.body.force_encoding("utf-8") | |
rescue Exception => error | |
# Airbrake exception | |
Airbrake.notify(error, | |
:backtrace => error.backtrace, | |
:error_class => error.class, | |
:error_message => error.message | |
) | |
end | |
end | |
def google_delete_event event_id | |
client = company.conf_connection.init_client | |
return [] unless client | |
service = client.discovered_api('calendar', 'v3') | |
begin | |
result = client.execute( | |
:api_method => service.events.delete, | |
:parameters => {'calendarId' => self.google_cal, 'eventId' => event_id}, | |
:headers => {'Content-Type' => 'application/json'}) | |
return result.body.force_encoding("utf-8") if result && result.body | |
rescue Exception => error | |
# Airbrake exception | |
Airbrake.notify(error, | |
:backtrace => error.backtrace, | |
:error_class => error.class, | |
:error_message => error.message | |
) | |
end | |
end | |
def google_all_events calendar_id | |
client = init_client | |
return [] unless client | |
service = client.discovered_api('calendar', 'v3') | |
result = client.execute( | |
:api_method => service.events.list, | |
:parameters => {'calendarId' => calendar_id}, | |
:headers => {'Content-Type' => 'application/json'}) | |
return JSON.parse(result.body.force_encoding("utf-8")) if result && result.body | |
end | |
############################################################################################################ | |
# scan the cal parsing single and recurring events and freebusys | |
# keep future ones - and 7 days in the past | |
def custom_parse cals | |
raw = Array.new | |
iraw = Array.new | |
last_week = Date.today - 7 | |
future = Date.today + 3.years | |
out = "" | |
tz = company.time_zone.to_s.length > 0 ? company.time_zone : get_time_zone | |
for cal_store in cals | |
for event in cal_store.freebusys | |
begin | |
if event.dtend >= last_week && event.dtstart < future | |
d1 = event.dtstart.to_datetime | |
d2 = event.dtend.to_datetime | |
out += d1.utc.strftime("%Y%m%dT%H%M%SZ") + "-" + d2.utc.strftime("%Y%m%dT%H%M%SZ") + "\n" | |
end | |
rescue | |
logger.info "#{$!}" | |
logger.info "Invalid ical event" | |
break | |
end | |
end | |
for event in cal_store.events | |
begin | |
if event.valid? && event.start_time.future? && event.recurs? | |
# first for blank recurring rules | |
unless event.rrule_property.first.to_s.match(/:FREQ=YEARLY;.*BYMONTHDAY=30;BYMONTH=8/) | |
event.rrule_property.first.interval = 1 if event.rrule_property.first.interval == 0 | |
evs = event.occurrences(:starting => Time.now - 1.week, :before => Time.now + 12.months) | |
for ev in evs | |
if ev && ev.dtend && ev.dtend >= last_week && ev.dtstart < future && ev.transp().to_s != "TRANSPARENT" | |
d1 = ev.dtstart.to_datetime | |
d2 = ev.dtend.to_datetime | |
out += d1.utc.strftime("%Y%m%dT%H%M%SZ") + "-" + d2.utc.strftime("%Y%m%dT%H%M%SZ") + "\n" | |
out += "-" + event.summary.to_s.gsub(/\n/, " ") + "\n" | |
end | |
end | |
end | |
elsif event && event.dtend && event.dtend >= last_week && event.dtstart < future && event.transp().to_s != "TRANSPARENT" | |
d1 = event.start_time.to_datetime | |
d2 = event.finish_time.to_datetime | |
out += d1.utc.strftime("%Y%m%dT%H%M%SZ") + "-" + d2.utc.strftime("%Y%m%dT%H%M%SZ") + "\n" | |
out += "-" + event.summary.to_s.gsub(/\n/, " ") + "\n" | |
end | |
rescue | |
logger.info "#{$!}" | |
logger.info "Invalid ical event" | |
# break | |
end | |
end | |
end | |
return out | |
end | |
# parse a ical datetime | |
def parse_time str | |
return str if str[-1].to_s == "Z" | |
if str[0..3] == "TZID" | |
pos = str.index(":") | |
tz = str[5..(pos-1)] | |
orig = str[(pos+1)..-1] | |
dt = Time.zone.parse(orig, "%Y%m%dT%H%M%S").utc.strftime("%Y%m%dT%H%M%SZ"); | |
return dt | |
else | |
if Time.zone | |
return Time.zone.parse(str, "%Y%m%dT%H%M%S").utc.strftime("%Y%m%dT%H%M%SZ"); | |
else | |
return str | |
end | |
end | |
end | |
def ical_update_cal | |
cal = nil | |
year = Time.now.year | |
begin | |
data = get_feed_data | |
out = "" | |
if (data) | |
if data["RRULE"] && data["FREQ=YEARLY;INTERVAL=1;BYMONTH=12;BYMONTHDAY=-1"].nil? | |
# there's a repeating rule - use the full library | |
out = custom_parse(RiCal.parse_string(data)) | |
else | |
# quite parse | |
res = data.split(/\r\n/) | |
pos = 0 | |
len = res.length | |
while pos < res.length | |
str = res[pos] | |
pos+=1 | |
if str == "BEGIN:VTIMEZONE" | |
str = res[pos] | |
pos +=1 | |
Time.zone = str[5..-1] # set the timezone to use | |
end | |
if str == "BEGIN:VEVENT" || str == "BEGIN:VFREEBUSY" | |
st = nil | |
en = nil | |
while str && str != "END:VEVENT" && str != "END:VFREEBUSY" | |
str = res[pos] | |
pos +=1 | |
st = parse_time(str[8..-1]) if str[0..6] == "DTSTART" | |
en = parse_time(str[6..-1]) if str[0..4] == "DTEND" | |
end | |
if st && en | |
if en[0..3].to_i >= year && en[0..3].to_i <= (year+3) # only get events between current year and 3 years times | |
# only if at least 2012 | |
out += "#{st}-#{en}\n" | |
end | |
end | |
end | |
end | |
end | |
end | |
self.cache = out | |
self.cache_time = Time.now | |
save! | |
rescue Exception => e | |
raise "#{e} - cal_id: #{self.id}" if e.is_a?(Timeout::Error) || e.message == "execution expired" | |
return nil | |
end | |
#blank the timezone | |
Time.zone = nil | |
end | |
def update_cal | |
if !ical_link.blank? | |
ical_update_cal | |
elsif !google_cal.blank? | |
google_update_cal | |
end | |
end | |
# get a list of ical OR google cal events either from the cache for this cal - or from the ical feed itself | |
def get_ical_events | |
return self.event_cache if self.event_cache | |
return nil if (ical_link.blank? && google_cal.blank?) | |
cal = nil | |
# The cal cache only refreshes if it's older than 1 hour | |
if (!self.cache_time || cache_time < Time.now - 3600) | |
Timeout::timeout(240) do | |
update_cal | |
end | |
end | |
events = [] | |
if self.cache && !self.cache_time.blank? | |
tz = company.time_zone.to_s.length > 0 ? company.time_zone : get_time_zone | |
data = self.cache | |
dates = data.split(/\n/) | |
for date in dates | |
if date[0..0] == "-" && events.length > 0 && date.length > 0 | |
events[-1][2] = date[1..-1] | |
else | |
sp = date.split('-') | |
e1 = DateTime.parse(sp[0], "%Y%m%dT%H%M%SZ").in_time_zone(tz) | |
e2 = DateTime.parse(sp[1], "%Y%m%dT%H%M%SZ").in_time_zone(tz) | |
events << [e1,e2] | |
end | |
end | |
end | |
self.event_cache = events | |
return events | |
end | |
def get_bookings_for_date data, date, type, range, duration, start = 0 | |
return if !data | |
if (type == Schedule::DAY || type == Schedule::WEEK) | |
return | |
end | |
# keep the cals in memory if possible | |
if !@cal_store | |
@cal_store = get_ical_events | |
end | |
return if !@cal_store | |
jd = date.jd | |
base = ScheduleRange.starts(range)*12 + start | |
tstep = duration/5 | |
# TODO - we're not fully overlaying cross-midnight calendars | |
for event in @cal_store | |
d1 = event[0] | |
d2 = event[1] | |
if (d1.jd <= jd && d2.jd >= jd) # if start less than end and end greater than start! | |
if (d1.jd == d2.jd) | |
istart = d1.hour*12 + d1.min/5 | |
iend = d2.hour*12 + d2.min/5 | |
for a in istart..iend-1 | |
x = (a-base)/tstep # x = (a-base+(d1.jd-jd1)*288)/tstep | |
data[x] = 41 if (x >= 0 && x < data.length) #(+) | |
end | |
elsif (d1.jd == jd) # this is the first day | |
istart = d1.hour*12 + d1.min/5 | |
iend = 24*12 # end at midnight | |
for a in istart..iend-1 | |
x = (a-base)/tstep # x = (a-base+(d1.jd-jd1)*288)/tstep | |
data[x] = 40 if (x >= 0 && x < data.length) #(+) | |
end | |
elsif d2.jd == jd | |
istart = 0 # start at midnight | |
iend = d2.hour*12 + d2.min/5 | |
for a in istart..iend-1 | |
x = (a-base)/tstep # x = (a-base+(d1.jd-jd1)*288)/tstep | |
data[x] = 39 if (x >= 0 && x < data.length) #(+) | |
end | |
elsif d2.jd > jd | |
data.fill(10) | |
end | |
end | |
end | |
end | |
def get_bookings_for_day_range data, sdate, edate, type | |
if (type != Schedule::DAY) | |
return | |
end | |
# keep the cals in memory if possible | |
if !@cal_store | |
@cal_store = get_ical_events | |
end | |
return if !@cal_store | |
jd1 = sdate.jd | |
jd2 = edate.jd | |
# TODO - we're not fully overlaying cross-midnight calendars | |
for event in @cal_store | |
d1 = event[0] | |
d2 = event[1] | |
if (d1.jd <= jd2 && d2.jd >= jd1) # if start less than end and end greater than start! | |
# do single days for now.. | |
diff = d2.jd - d1.jd | |
date = d1 | |
if (diff > 0) | |
for d in 1..diff | |
data[date.yday-1] = 10 | |
date+=1; | |
end | |
end | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment