Skip to content

Instantly share code, notes, and snippets.

@rubypirate
Created April 20, 2015 13:02
Show Gist options
  • Save rubypirate/f12595336e51628c0510 to your computer and use it in GitHub Desktop.
Save rubypirate/f12595336e51628c0510 to your computer and use it in GitHub Desktop.
# -*- 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