i2gcal 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/i2gcal/version.rb +3 -0
- data/lib/i2gcal.rb +1 -0
- data/lib/synchrograph/api_client_builder.rb +24 -0
- data/lib/synchrograph/filters/base.rb +16 -0
- data/lib/synchrograph/filters/categories_to_colours.rb +14 -0
- data/lib/synchrograph/gcalendar.rb +130 -0
- data/lib/synchrograph/icalendar_monkey_patches/better_timezones.rb +30 -0
- data/lib/synchrograph/icalendar_monkey_patches/dates_and_datetimes.rb +28 -0
- data/lib/synchrograph/icalendar_monkey_patches/event.rb +39 -0
- data/lib/synchrograph/icalendar_monkey_patches.rb +12 -0
- data/lib/synchrograph.rb +60 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e816956ee9ffd5bbeecc454dfa1f9141d285e38c
|
4
|
+
data.tar.gz: 26adbf650a050e5e4f5e97c62480700480b332bc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a06fe005b4292998eeee5eea41f3bac0456c58e4dff15c0388297753ea6f4214832b4529752d6bfdc4d6127976e872b0d5e45bf38c055e2acd7e4f5637613ebb
|
7
|
+
data.tar.gz: c21768e42977f4ad315dacb18bb78ddd97925af866e954ec10974f4e7f2752fb4a3243df86a58d975bdc00bcec7c436c40ff83255fd6543178d61970a55a2bdc
|
data/lib/i2gcal.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'synchrograph'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'google/api_client'
|
2
|
+
|
3
|
+
class APIClientBuilder
|
4
|
+
|
5
|
+
def self.new_client(client_secrets)
|
6
|
+
client = Google::APIClient.new(application_name:'stupidpupil_icalendar', application_version:'0.0.1')
|
7
|
+
client.authorization.scope = 'https://www.googleapis.com/auth/calendar'
|
8
|
+
|
9
|
+
client.authorization.client_id = client_secrets.client_id
|
10
|
+
client.authorization.client_secret = client_secrets.client_secret
|
11
|
+
client.authorization.redirect_uri = client_secrets.redirect_uris.first
|
12
|
+
|
13
|
+
return client
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.new_client_with_refresh_token(refresh_token, client_secrets)
|
17
|
+
client = new_client(client_secrets)
|
18
|
+
client.authorization.refresh_token = refresh_token
|
19
|
+
client.authorization.fetch_access_token!
|
20
|
+
|
21
|
+
return client
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class BaseFilter
|
2
|
+
|
3
|
+
def self.call(ical_event, representation)
|
4
|
+
{
|
5
|
+
'summary' => ical_event.summary,
|
6
|
+
'start' => ical_event.dtstart.google_calendar_representation,
|
7
|
+
'end' => ical_event.dtend.google_calendar_representation,
|
8
|
+
'location' => ical_event.location,
|
9
|
+
'description' => ical_event.description,
|
10
|
+
'id' => ical_event.google_calendar_id,
|
11
|
+
'status' => 'confirmed',
|
12
|
+
'recurrence' => ical_event.rrule.map {|r| "RRULE:#{r.value_ical}"} + ical_event.rdates + ical_event.exdates
|
13
|
+
}.delete_if {|k,v| v.nil? }
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CatToColFilter
|
2
|
+
|
3
|
+
def initialize(mapping)
|
4
|
+
@mapping = mapping
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(ical_event, representation)
|
8
|
+
@mapping.each_pair do |cat, col|
|
9
|
+
representation['colorId'] = col.to_s if ical_event.categories.any? {|c| c.to_sym == cat}
|
10
|
+
end
|
11
|
+
representation
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'synchrograph/filters/base'
|
2
|
+
require 'synchrograph/filters/categories_to_colours'
|
3
|
+
|
4
|
+
# Handles generating fetching the existing list of events in GCal
|
5
|
+
# and generating requests to correct this list
|
6
|
+
class GCalendar
|
7
|
+
|
8
|
+
attr_reader :client, :gcal_id
|
9
|
+
|
10
|
+
def initialize(api_client, gcal_identifier)
|
11
|
+
@client = api_client
|
12
|
+
@gcal_id = gcal_identifier
|
13
|
+
@pending_requests = []
|
14
|
+
|
15
|
+
#categories = icalendar.first.events.map {|e| e.categories}.flatten.uniq { |u| u.to_s}
|
16
|
+
#cat_to_col_mapping = {}
|
17
|
+
#categories.each_with_index do |c, i|
|
18
|
+
# cat_to_col_mapping[c] = i%10
|
19
|
+
#end
|
20
|
+
|
21
|
+
@filters = [
|
22
|
+
BaseFilter,
|
23
|
+
CatToColFilter.new({
|
24
|
+
"Protected time for Specific Task & Finish Work": 6,
|
25
|
+
"Work Related Travel (mark as Out of Office)": 3,
|
26
|
+
"Just for Information (mark as Free)": 8,
|
27
|
+
"Authorised Leave (annual maternity etc)": 5,
|
28
|
+
"Formal Event or Workshop": 10
|
29
|
+
})
|
30
|
+
]
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
def gcal_api
|
35
|
+
@gcal_api ||= client.discovered_api('calendar', 'v3')
|
36
|
+
end
|
37
|
+
|
38
|
+
###
|
39
|
+
|
40
|
+
def events
|
41
|
+
result = client.execute(api_method:gcal_api.events.list, parameters: {'calendarId' => gcal_id, 'showDeleted' => true})
|
42
|
+
gcal_events = result.data.items
|
43
|
+
|
44
|
+
while result.next_page_token
|
45
|
+
result = client.execute(result.next_page)
|
46
|
+
gcal_events += result.data.items
|
47
|
+
end
|
48
|
+
|
49
|
+
return gcal_events
|
50
|
+
end
|
51
|
+
|
52
|
+
def event_for_id(id)
|
53
|
+
result = client.execute(api_method:gcal_api.events.get, parameters: {'calendarId' => gcal_id, 'eventId' => id})
|
54
|
+
return result.data
|
55
|
+
end
|
56
|
+
|
57
|
+
def instances_for_event_id(event_id)
|
58
|
+
result = client.execute(api_method:gcal_api.events.instances, parameters: {'calendarId' => gcal_id, 'eventId' => event_id})
|
59
|
+
gcal_instances = result.data.items
|
60
|
+
|
61
|
+
while result.next_page_token
|
62
|
+
result = client.execute(result.next_page)
|
63
|
+
gcal_instances += result.data.items
|
64
|
+
end
|
65
|
+
|
66
|
+
return gcal_instances
|
67
|
+
end
|
68
|
+
|
69
|
+
###
|
70
|
+
|
71
|
+
def add_request_for_gcal_event_and_ical_event(gcal_event, ical_event)
|
72
|
+
@pending_requests << insert(ical_event) if gcal_event.nil?
|
73
|
+
@pending_requests << delete(gcal_event) if ical_event.nil? and gcal_event['recurring_event_id'].nil? and (gcal_event['status'] != 'cancelled')
|
74
|
+
@pending_requests << update(gcal_event, ical_event) if gcal_event and ical_event
|
75
|
+
end
|
76
|
+
|
77
|
+
def google_calendar_representation_of_ical_event(ical_event)
|
78
|
+
representation = {}
|
79
|
+
|
80
|
+
@filters.each do |f|
|
81
|
+
representation = f.call(ical_event, representation)
|
82
|
+
end
|
83
|
+
|
84
|
+
return representation
|
85
|
+
end
|
86
|
+
|
87
|
+
def insert(ical_event)
|
88
|
+
{
|
89
|
+
api_method: gcal_api.events.insert,
|
90
|
+
headers: {'Content-Type' => 'application/json'},
|
91
|
+
parameters: {'calendarId' => gcal_id},
|
92
|
+
body:JSON.dump(google_calendar_representation_of_ical_event(ical_event))
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
def delete(gcal_event)
|
97
|
+
{
|
98
|
+
api_method: gcal_api.events.delete,
|
99
|
+
parameters: {'calendarId' => gcal_id, 'eventId' => gcal_event['id']},
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def update(gcal_event, ical_event)
|
104
|
+
new_rep = google_calendar_representation_of_ical_event(ical_event)
|
105
|
+
|
106
|
+
{
|
107
|
+
api_method: gcal_api.events.update,
|
108
|
+
headers: {'Content-Type' => 'application/json'},
|
109
|
+
parameters: {'calendarId' => gcal_id, 'eventId' => ical_event.google_calendar_id},
|
110
|
+
body:JSON.dump(new_rep)
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
###
|
115
|
+
|
116
|
+
def execute_requests
|
117
|
+
while @pending_requests.any?
|
118
|
+
batch = Google::APIClient::BatchRequest.new
|
119
|
+
|
120
|
+
@pending_requests.slice!(0,950).each do |r|
|
121
|
+
batch.add r
|
122
|
+
end
|
123
|
+
|
124
|
+
@pending_requests = []
|
125
|
+
return @client.execute(batch)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Icalendar
|
2
|
+
module Values
|
3
|
+
module BetterTimezones
|
4
|
+
|
5
|
+
regex = /<!-- (\(UTC.+?)\s+-->[\s|$]+<mapZone .+? type="(.+?)"\/>/
|
6
|
+
wz_path = File.expand_path('../../../../data/windowsZones.xml', __FILE__)
|
7
|
+
wz = File.read(wz_path)
|
8
|
+
WINDOWS_TZID_MAP = Hash[wz.scan(regex)]
|
9
|
+
|
10
|
+
def tzinfo
|
11
|
+
return time_zone.tzinfo if self.respond_to? :time_zone
|
12
|
+
|
13
|
+
tzid = ical_params['tzid']
|
14
|
+
tzid = tzid.first if tzid and tzid.any?
|
15
|
+
|
16
|
+
if tzid
|
17
|
+
return TZInfo::Timezone.get(WINDOWS_TZID_MAP[tzid]) if WINDOWS_TZID_MAP.has_key? tzid
|
18
|
+
return TZInfo::Timezone.get(tzid) if TZInfo::Timezone.all_identifiers.include? tzid
|
19
|
+
end
|
20
|
+
|
21
|
+
return TZInfo::Timezone.get('Etc/UTC')
|
22
|
+
end
|
23
|
+
|
24
|
+
def value_with_tz
|
25
|
+
ActiveSupportTimeWithZoneAdapter.new nil, tzinfo, value
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Icalendar
|
2
|
+
module Values
|
3
|
+
|
4
|
+
class Date
|
5
|
+
include BetterTimezones
|
6
|
+
|
7
|
+
def google_calendar_representation
|
8
|
+
{
|
9
|
+
'date' => self.strftime('%F'),
|
10
|
+
'timeZone' => tzinfo.name
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
class DateTime
|
17
|
+
include BetterTimezones
|
18
|
+
|
19
|
+
def google_calendar_representation
|
20
|
+
{
|
21
|
+
'dateTime' => self.strftime('%FT%T'),
|
22
|
+
'timeZone' => tzinfo.name
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Icalendar
|
2
|
+
class Event
|
3
|
+
|
4
|
+
def google_calendar_id=(gcal_id)
|
5
|
+
@gcal_id = gcal_id
|
6
|
+
end
|
7
|
+
|
8
|
+
def google_calendar_id
|
9
|
+
@gcal_id ||= UUIDTools::UUID.sha1_create(UUID_ICAL_TO_GCAL_NAMESPACE,uid).hexdigest
|
10
|
+
end
|
11
|
+
|
12
|
+
def exdates
|
13
|
+
exdate.map {|r| "EXDATE:#{[r].flatten.map {|s| s.value_with_tz.utc.strftime('%Y%m%dT%H%M%S')+'Z'}.join(',')}"}
|
14
|
+
end
|
15
|
+
|
16
|
+
def rdates
|
17
|
+
rdate.map {|r| "RDATE:#{[r].flatten.map {|s| s.value_with_tz.utc.strftime('%Y%m%dT%H%M%S')+'Z'}.join(',')}"}
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
###
|
23
|
+
|
24
|
+
def parent_component
|
25
|
+
return nil if recurrence_id.nil?
|
26
|
+
@parent_component ||= parent.events.find {|p| p.recurrence_id.nil? and (p.uid == self.uid)}
|
27
|
+
end
|
28
|
+
|
29
|
+
def update_from_parent_component!
|
30
|
+
return if parent_component.nil?
|
31
|
+
|
32
|
+
[:summary, :description, :location, :geo].each do |desc_prop|
|
33
|
+
self.send("#{desc_prop}=".to_sym, parent_component.send(desc_prop)) if self.send(desc_prop).nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'icalendar'
|
2
|
+
require 'uuidtools'
|
3
|
+
|
4
|
+
module Icalendar
|
5
|
+
|
6
|
+
UUID_ICAL_TO_GCAL_NAMESPACE = UUIDTools::UUID.parse('88cc3c53-51c9-4550-a469-7aefb4d066dc')
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'synchrograph/icalendar_monkey_patches/better_timezones'
|
11
|
+
require 'synchrograph/icalendar_monkey_patches/dates_and_datetimes'
|
12
|
+
require 'synchrograph/icalendar_monkey_patches/event'
|
data/lib/synchrograph.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'icalendar'
|
2
|
+
|
3
|
+
require 'synchrograph/icalendar_monkey_patches'
|
4
|
+
require 'synchrograph/api_client_builder'
|
5
|
+
require 'synchrograph/gcalendar'
|
6
|
+
|
7
|
+
# Glues together the API Client, the GCalendar and the ICalendar
|
8
|
+
# to create the right requests, in the right order and submit them
|
9
|
+
class Synchrograph
|
10
|
+
|
11
|
+
def initialize(api_client)
|
12
|
+
@client = api_client
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def synchronise(gcalendar, icalendar)
|
17
|
+
|
18
|
+
ical_events = icalendar.first.events.find_all {|ie| ie.recurrence_id.nil?}
|
19
|
+
gcal_events = gcalendar.events
|
20
|
+
|
21
|
+
response_body = ""
|
22
|
+
|
23
|
+
#Insert or update
|
24
|
+
ical_events.each do |ie|
|
25
|
+
ge = gcal_events.find {|ge| ge['id'] == ie.google_calendar_id}
|
26
|
+
|
27
|
+
gcalendar.add_request_for_gcal_event_and_ical_event ge, ie
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
(gcal_events.find_all {|ge| ge['recurring_event_id'].nil? and not ical_events.map {|ie| ie.google_calendar_id}.include?(ge['id'])}).each do |ge|
|
32
|
+
gcalendar.add_request_for_gcal_event_and_ical_event ge, nil
|
33
|
+
end
|
34
|
+
|
35
|
+
response_body << gcalendar.execute_requests.body
|
36
|
+
|
37
|
+
#Now deal with recurrence exceptions
|
38
|
+
ical_events = icalendar.first.events.find_all {|ie| not ie.recurrence_id.nil?}
|
39
|
+
|
40
|
+
ical_events.group_by {|ie| ie.google_calendar_id}.each_pair do |ge_id, re_e|
|
41
|
+
gcal_instances = gcalendar.instances_for_event_id ge_id
|
42
|
+
|
43
|
+
re_e.each do |ie|
|
44
|
+
instance = gcal_instances.find {|i| i.start.date_time == ie.recurrence_id}
|
45
|
+
next if instance.nil?
|
46
|
+
|
47
|
+
ie.update_from_parent_component!
|
48
|
+
|
49
|
+
ie.google_calendar_id = instance.id
|
50
|
+
gcalendar.add_request_for_gcal_event_and_ical_event(instance, ie)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
er = gcalendar.execute_requests
|
55
|
+
response_body << er.body if er
|
56
|
+
|
57
|
+
return response_body
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: i2gcal
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Watkins
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-07-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: google-api-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.8'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: icalendar
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.4'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: uuidtools
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.1'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bacon
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.2'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.2'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
executables: []
|
86
|
+
extensions: []
|
87
|
+
extra_rdoc_files: []
|
88
|
+
files:
|
89
|
+
- lib/i2gcal.rb
|
90
|
+
- lib/i2gcal/version.rb
|
91
|
+
- lib/synchrograph.rb
|
92
|
+
- lib/synchrograph/api_client_builder.rb
|
93
|
+
- lib/synchrograph/filters/base.rb
|
94
|
+
- lib/synchrograph/filters/categories_to_colours.rb
|
95
|
+
- lib/synchrograph/gcalendar.rb
|
96
|
+
- lib/synchrograph/icalendar_monkey_patches.rb
|
97
|
+
- lib/synchrograph/icalendar_monkey_patches/better_timezones.rb
|
98
|
+
- lib/synchrograph/icalendar_monkey_patches/dates_and_datetimes.rb
|
99
|
+
- lib/synchrograph/icalendar_monkey_patches/event.rb
|
100
|
+
homepage: https://github.com/stupidpupil/i2gcal
|
101
|
+
licenses:
|
102
|
+
- AGPL-3.0
|
103
|
+
metadata: {}
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 2.5.1
|
121
|
+
signing_key:
|
122
|
+
specification_version: 4
|
123
|
+
summary: One-way sync an iCalendar file to Google Calendar
|
124
|
+
test_files: []
|
125
|
+
has_rdoc:
|