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 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
@@ -0,0 +1,3 @@
1
+ module I2GCal
2
+ VERSION = '0.0.1'.freeze
3
+ end
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'
@@ -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: