calendar-assistant 0.9.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -1
- data/README.md +44 -22
- data/Rakefile +37 -6
- data/bin/calendar-assistant +3 -1
- data/lib/calendar_assistant.rb +14 -16
- data/lib/calendar_assistant/available_block.rb +1 -1
- data/lib/calendar_assistant/calendar_assistant.rb +21 -25
- data/lib/calendar_assistant/cli.rb +10 -10
- data/lib/calendar_assistant/cli/authorizer.rb +14 -14
- data/lib/calendar_assistant/cli/command_service.rb +2 -2
- data/lib/calendar_assistant/cli/commands.rb +47 -40
- data/lib/calendar_assistant/cli/config.rb +16 -17
- data/lib/calendar_assistant/cli/event_presenter.rb +2 -2
- data/lib/calendar_assistant/cli/event_set_presenter.rb +3 -3
- data/lib/calendar_assistant/cli/helpers.rb +7 -8
- data/lib/calendar_assistant/cli/linter_event_presenter.rb +3 -3
- data/lib/calendar_assistant/cli/linter_event_set_presenter.rb +4 -4
- data/lib/calendar_assistant/cli/printer.rb +15 -15
- data/lib/calendar_assistant/config.rb +11 -11
- data/lib/calendar_assistant/config/token_store.rb +4 -4
- data/lib/calendar_assistant/date_helpers.rb +1 -2
- data/lib/calendar_assistant/event.rb +54 -50
- data/lib/calendar_assistant/event_repository.rb +14 -15
- data/lib/calendar_assistant/event_repository_factory.rb +1 -1
- data/lib/calendar_assistant/event_set.rb +14 -14
- data/lib/calendar_assistant/extensions/google_apis_extensions.rb +1 -1
- data/lib/calendar_assistant/extensions/launchy_extensions.rb +7 -4
- data/lib/calendar_assistant/has_duration.rb +4 -5
- data/lib/calendar_assistant/lint_event_repository.rb +3 -3
- data/lib/calendar_assistant/location_config_validator.rb +3 -3
- data/lib/calendar_assistant/location_event_repository.rb +7 -8
- data/lib/calendar_assistant/predicate_collection.rb +1 -2
- data/lib/calendar_assistant/scheduler.rb +4 -5
- data/lib/calendar_assistant/string_helpers.rb +1 -1
- data/lib/calendar_assistant/version.rb +1 -1
- metadata +38 -30
@@ -3,9 +3,10 @@ class CalendarAssistant
|
|
3
3
|
class Config
|
4
4
|
autoload :TokenStore, "calendar_assistant/config/token_store"
|
5
5
|
|
6
|
-
class NoTokensAuthorized < CalendarAssistant::BaseException
|
6
|
+
class NoTokensAuthorized < CalendarAssistant::BaseException
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
|
+
class AccessingHashAsScalar < CalendarAssistant::BaseException
|
9
10
|
end
|
10
11
|
|
11
12
|
module Keys
|
@@ -57,16 +58,15 @@ class CalendarAssistant
|
|
57
58
|
|
58
59
|
attr_reader :user_config, :options, :defaults
|
59
60
|
|
60
|
-
def initialize
|
61
|
+
def initialize(options: {},
|
61
62
|
user_config: {},
|
62
|
-
defaults: DEFAULT_SETTINGS
|
63
|
-
|
63
|
+
defaults: DEFAULT_SETTINGS)
|
64
64
|
@defaults = defaults
|
65
65
|
@options = options
|
66
66
|
@user_config = user_config
|
67
67
|
end
|
68
68
|
|
69
|
-
def in_env
|
69
|
+
def in_env(&block)
|
70
70
|
# this is totally not thread-safe
|
71
71
|
orig_b_o_d = BusinessTime::Config.beginning_of_workday
|
72
72
|
orig_e_o_d = BusinessTime::Config.end_of_workday
|
@@ -98,7 +98,7 @@ class CalendarAssistant
|
|
98
98
|
end
|
99
99
|
end
|
100
100
|
|
101
|
-
def get
|
101
|
+
def get(keypath)
|
102
102
|
rval = Config.find_in_hash(user_config, keypath)
|
103
103
|
|
104
104
|
if rval.is_a?(Hash)
|
@@ -108,7 +108,7 @@ class CalendarAssistant
|
|
108
108
|
rval
|
109
109
|
end
|
110
110
|
|
111
|
-
def set
|
111
|
+
def set(keypath, value)
|
112
112
|
Config.set_in_hash user_config, keypath, value
|
113
113
|
end
|
114
114
|
|
@@ -116,7 +116,7 @@ class CalendarAssistant
|
|
116
116
|
# note that, despite the name, this method returns both options
|
117
117
|
# and settings
|
118
118
|
#
|
119
|
-
def setting
|
119
|
+
def setting(setting_name)
|
120
120
|
context = Config.find_in_hash(options, Keys::Options::CONTEXT)
|
121
121
|
Config.find_in_hash(options, setting_name) ||
|
122
122
|
Config.find_in_hash(user_config, [Keys::SETTINGS, context, setting_name]) ||
|
@@ -190,14 +190,14 @@ class CalendarAssistant
|
|
190
190
|
a
|
191
191
|
end
|
192
192
|
|
193
|
-
def self.find_in_hash
|
193
|
+
def self.find_in_hash(hash, keypath)
|
194
194
|
split_keypath(keypath).inject(hash) do |current_val, key|
|
195
195
|
break unless current_val.has_key?(key)
|
196
196
|
current_val[key]
|
197
197
|
end
|
198
198
|
end
|
199
199
|
|
200
|
-
def self.set_in_hash
|
200
|
+
def self.set_in_hash(hash, keypath, new_value)
|
201
201
|
*path_parts, key = *split_keypath(keypath)
|
202
202
|
|
203
203
|
current_hash = path_parts.inject(hash) do |current_val, path|
|
@@ -3,20 +3,20 @@ class CalendarAssistant
|
|
3
3
|
class TokenStore
|
4
4
|
attr_reader :config
|
5
5
|
|
6
|
-
def initialize
|
6
|
+
def initialize(config)
|
7
7
|
@config = config
|
8
8
|
end
|
9
9
|
|
10
|
-
def delete
|
10
|
+
def delete(id)
|
11
11
|
config.tokens.delete(id)
|
12
12
|
config.persist!
|
13
13
|
end
|
14
14
|
|
15
|
-
def load
|
15
|
+
def load(id)
|
16
16
|
config.tokens[id]
|
17
17
|
end
|
18
18
|
|
19
|
-
def store
|
19
|
+
def store(id, token)
|
20
20
|
config.tokens[id] = token
|
21
21
|
config.persist!
|
22
22
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
class CalendarAssistant
|
2
2
|
module DateHelpers
|
3
|
-
def self.cast_dates
|
3
|
+
def self.cast_dates(attributes)
|
4
4
|
attributes.each_with_object({}) do |(key, value), object|
|
5
5
|
if value.is_a?(Time) || value.is_a?(DateTime)
|
6
6
|
object[key] = Google::Apis::CalendarV3::EventDateTime.new(date_time: value)
|
@@ -13,4 +13,3 @@ class CalendarAssistant
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
16
|
-
|
@@ -29,49 +29,49 @@ class CalendarAssistant
|
|
29
29
|
end
|
30
30
|
|
31
31
|
PREDICATES = {
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
]
|
32
|
+
"response": %I[
|
33
|
+
accepted?
|
34
|
+
declined?
|
35
|
+
awaiting?
|
36
|
+
tentative?
|
37
|
+
],
|
38
|
+
"temporal": %I[
|
39
|
+
all_day?
|
40
|
+
past?
|
41
|
+
current?
|
42
|
+
future?
|
43
|
+
],
|
44
|
+
"visibility": %I[
|
45
|
+
private?
|
46
|
+
public?
|
47
|
+
explicitly_visible?
|
48
|
+
visible_guestlist?
|
49
|
+
],
|
50
|
+
"attributes": %I[
|
51
|
+
location_event?
|
52
|
+
self?
|
53
|
+
one_on_one?
|
54
|
+
busy?
|
55
|
+
commitment?
|
56
|
+
recurring?
|
57
|
+
abandoned?
|
58
|
+
anyone_can_add_self?
|
59
|
+
attendees_omitted?
|
60
|
+
end_time_unspecified?
|
61
|
+
guests_can_invite_others?
|
62
|
+
guests_can_modify?
|
63
|
+
guests_can_see_other_guests?
|
64
|
+
private_copy?
|
65
|
+
locked?
|
66
|
+
needs_action?
|
67
|
+
],
|
68
68
|
}
|
69
69
|
|
70
70
|
#
|
71
71
|
# class methods
|
72
72
|
#
|
73
73
|
|
74
|
-
def self.location_event_prefix
|
74
|
+
def self.location_event_prefix(config)
|
75
75
|
icon = config[CalendarAssistant::Config::Keys::Settings::LOCATION_ICON]
|
76
76
|
if nickname = config[CalendarAssistant::Config::Keys::Settings::NICKNAME]
|
77
77
|
return "#{icon} #{nickname} @ "
|
@@ -87,13 +87,13 @@ class CalendarAssistant
|
|
87
87
|
@config = config
|
88
88
|
end
|
89
89
|
|
90
|
-
def update
|
90
|
+
def update(**args)
|
91
91
|
update!(**args)
|
92
92
|
self
|
93
93
|
end
|
94
94
|
|
95
95
|
def location_event?
|
96
|
-
!!
|
96
|
+
!!summary.try(:starts_with?, Event.location_event_prefix(@config))
|
97
97
|
end
|
98
98
|
|
99
99
|
def accepted?
|
@@ -167,15 +167,15 @@ class CalendarAssistant
|
|
167
167
|
|
168
168
|
def other_human_attendees
|
169
169
|
return nil if attendees.nil?
|
170
|
-
attendees.select { |a| !
|
170
|
+
attendees.select { |a| !a.resource && !a.self }
|
171
171
|
end
|
172
172
|
|
173
173
|
def human_attendees
|
174
174
|
return nil if attendees.nil?
|
175
|
-
attendees.select { |a| !
|
175
|
+
attendees.select { |a| !a.resource }
|
176
176
|
end
|
177
177
|
|
178
|
-
def attendee
|
178
|
+
def attendee(id)
|
179
179
|
return nil if attendees.nil?
|
180
180
|
attendees.find do |attendee|
|
181
181
|
attendee.email == id
|
@@ -192,15 +192,19 @@ class CalendarAssistant
|
|
192
192
|
|
193
193
|
def av_uri
|
194
194
|
@av_uri ||= begin
|
195
|
-
|
196
|
-
|
195
|
+
if conference_data && conference_data.conference_solution.name == "Zoom Meeting"
|
196
|
+
return conference_data.entry_points.detect{|d| d.entry_point_type == "video" }.uri
|
197
|
+
end
|
197
198
|
|
198
|
-
|
199
|
-
|
199
|
+
description_link = CalendarAssistant::StringHelpers.find_uri_for_domain(description, "zoom.us")
|
200
|
+
return description_link if description_link
|
200
201
|
|
201
|
-
|
202
|
-
|
203
|
-
|
202
|
+
location_link = CalendarAssistant::StringHelpers.find_uri_for_domain(location, "zoom.us")
|
203
|
+
return location_link if location_link
|
204
|
+
|
205
|
+
return hangout_link if hangout_link
|
206
|
+
nil
|
207
|
+
end
|
204
208
|
end
|
205
209
|
end
|
206
|
-
end
|
210
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
class CalendarAssistant
|
2
2
|
class EventRepository
|
3
|
-
class CalendarNotFoundException < CalendarAssistant::BaseException
|
3
|
+
class CalendarNotFoundException < CalendarAssistant::BaseException
|
4
4
|
end
|
5
5
|
|
6
6
|
attr_reader :calendar, :calendar_id, :config
|
@@ -15,45 +15,44 @@ class CalendarAssistant
|
|
15
15
|
raise
|
16
16
|
end
|
17
17
|
|
18
|
-
def in_tz
|
18
|
+
def in_tz(&block)
|
19
19
|
CalendarAssistant.in_tz calendar.time_zone do
|
20
20
|
yield
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
def find
|
24
|
+
def find(time_range, predicates: {})
|
25
25
|
events = @service.list_events(@calendar_id,
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
)
|
26
|
+
time_min: time_range.first.iso8601,
|
27
|
+
time_max: time_range.last.iso8601,
|
28
|
+
order_by: "startTime",
|
29
|
+
single_events: true,
|
30
|
+
max_results: 2000)
|
32
31
|
events = events.items.map { |e| CalendarAssistant::Event.new(e, config: config) }
|
33
32
|
|
34
33
|
events = filter_by_predicates(events, predicates) unless predicates.empty?
|
35
34
|
CalendarAssistant::EventSet.new self, events
|
36
35
|
end
|
37
36
|
|
38
|
-
def new
|
39
|
-
event = Google::Apis::CalendarV3::Event.new
|
37
|
+
def new(event_attributes)
|
38
|
+
event = Google::Apis::CalendarV3::Event.new(**DateHelpers.cast_dates(event_attributes))
|
40
39
|
event.visibility ||= config.event_visibility
|
41
40
|
CalendarAssistant::Event.new(event, config: config)
|
42
41
|
end
|
43
42
|
|
44
|
-
def create
|
43
|
+
def create(event_attributes)
|
45
44
|
new(event_attributes).tap do |event|
|
46
45
|
@service.insert_event @calendar_id, event.__getobj__
|
47
46
|
end
|
48
47
|
end
|
49
48
|
|
50
|
-
def delete
|
51
|
-
@service.delete_event @calendar_id,
|
49
|
+
def delete(event)
|
50
|
+
@service.delete_event @calendar_id, event.id
|
52
51
|
event
|
53
52
|
end
|
54
53
|
|
55
54
|
def update(event, attributes)
|
56
|
-
event.update!
|
55
|
+
event.update!(**DateHelpers.cast_dates(attributes))
|
57
56
|
updated_event = @service.update_event @calendar_id, event.id, event
|
58
57
|
CalendarAssistant::Event.new(updated_event, config: config)
|
59
58
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
class CalendarAssistant
|
2
2
|
class EventRepositoryFactory
|
3
|
-
def self.new_event_repository
|
3
|
+
def self.new_event_repository(service, calendar_id, config: CalendarAssistant::Config.new, type: :base)
|
4
4
|
klass = case type
|
5
5
|
when :location
|
6
6
|
LocationEventRepository
|
@@ -7,7 +7,7 @@ class CalendarAssistant
|
|
7
7
|
# - it could be a bare Event
|
8
8
|
#
|
9
9
|
class EventSet
|
10
|
-
def self.new
|
10
|
+
def self.new(event_repository, events = nil)
|
11
11
|
if events.is_a?(EventSet::Hash)
|
12
12
|
return EventSet::Hash.new event_repository, events.try(:events)
|
13
13
|
end
|
@@ -23,17 +23,17 @@ class CalendarAssistant
|
|
23
23
|
class Base
|
24
24
|
attr_reader :event_repository, :events
|
25
25
|
|
26
|
-
def initialize
|
26
|
+
def initialize(event_repository, events)
|
27
27
|
@event_repository = event_repository
|
28
28
|
@events = events
|
29
29
|
end
|
30
30
|
|
31
|
-
def ==
|
31
|
+
def ==(rhs)
|
32
32
|
return false unless rhs.is_a?(self.class)
|
33
33
|
self.event_repository == rhs.event_repository && self.events == rhs.events
|
34
34
|
end
|
35
35
|
|
36
|
-
def new
|
36
|
+
def new(new_events)
|
37
37
|
EventSet.new self.event_repository, new_events
|
38
38
|
end
|
39
39
|
|
@@ -45,28 +45,28 @@ class CalendarAssistant
|
|
45
45
|
end
|
46
46
|
|
47
47
|
class Hash < EventSet::Base
|
48
|
-
def ensure_keys
|
48
|
+
def ensure_keys(keys, only: false)
|
49
49
|
keys.each do |key|
|
50
50
|
events[key] = [] unless events.has_key?(key)
|
51
51
|
end
|
52
52
|
if only
|
53
53
|
events.keys.each do |key|
|
54
|
-
if !
|
54
|
+
if !keys.include? key
|
55
55
|
events.delete(key)
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
-
def []
|
61
|
+
def [](key)
|
62
62
|
events[key] ||= []
|
63
63
|
end
|
64
64
|
|
65
|
-
def []=
|
65
|
+
def []=(key, value)
|
66
66
|
events[key] = value
|
67
67
|
end
|
68
68
|
|
69
|
-
def available_blocks
|
69
|
+
def available_blocks(length: 1)
|
70
70
|
event_repository.in_tz do
|
71
71
|
dates = events.keys.sort
|
72
72
|
|
@@ -91,7 +91,7 @@ class CalendarAssistant
|
|
91
91
|
avail_time[date] << AvailableBlock.new(start: start_time, end: e.start_time)
|
92
92
|
end
|
93
93
|
start_time = [e.end_time, start_time].max
|
94
|
-
break if !
|
94
|
+
break if !start_time.during_business_hours?
|
95
95
|
end
|
96
96
|
|
97
97
|
if HasDuration.duration_in_seconds(start_time, end_time) >= length
|
@@ -105,18 +105,18 @@ class CalendarAssistant
|
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
108
|
-
def intersection
|
108
|
+
def intersection(other, length: 1)
|
109
109
|
set = new({})
|
110
110
|
set.ensure_keys(events.keys + other.events.keys)
|
111
111
|
set.events.keys.each do |date|
|
112
112
|
events[date].each do |event_a|
|
113
113
|
other.events[date].each do |event_b|
|
114
114
|
if event_a.contains?(event_b.start_time) ||
|
115
|
-
event_a.contains?(event_b.end_time-1) ||
|
115
|
+
event_a.contains?(event_b.end_time - 1) ||
|
116
116
|
event_b.contains?(event_a.start_time) ||
|
117
|
-
event_b.contains?(event_a.end_time-1)
|
117
|
+
event_b.contains?(event_a.end_time - 1)
|
118
118
|
start_time = [event_a.start_time, event_b.start_time].max
|
119
|
-
end_time
|
119
|
+
end_time = [event_a.end_time, event_b.end_time].min
|
120
120
|
if HasDuration.duration_in_seconds(start_time, end_time) >= length
|
121
121
|
set.events[date] << AvailableBlock.new(start: start_time, end: end_time)
|
122
122
|
end
|
@@ -12,9 +12,9 @@ require "launchy"
|
|
12
12
|
#
|
13
13
|
class CalendarAssistant
|
14
14
|
class ZoomLaunchy < Launchy::Application::Browser
|
15
|
-
ZOOM_URI_REGEXP = %r(https?://\w+.zoom.us/j/(\d+))
|
15
|
+
ZOOM_URI_REGEXP = %r(https?://\w+.zoom.us/j/(\d+)(\?(.*))?)
|
16
16
|
|
17
|
-
def self.handles?
|
17
|
+
def self.handles?(uri)
|
18
18
|
return true if ZOOM_URI_REGEXP.match(uri)
|
19
19
|
end
|
20
20
|
|
@@ -26,13 +26,16 @@ class CalendarAssistant
|
|
26
26
|
[find_executable("xdg-open")]
|
27
27
|
end
|
28
28
|
|
29
|
-
def open
|
29
|
+
def open(uri, options = {})
|
30
30
|
command = host_os_family.app_list(self).compact.first
|
31
31
|
if command.nil?
|
32
32
|
super uri, options
|
33
33
|
else
|
34
|
-
|
34
|
+
matches = ZOOM_URI_REGEXP.match(uri)
|
35
|
+
confno = matches[1]
|
36
|
+
params = matches[3]
|
35
37
|
url = "zoommtg://zoom.us/join?confno=#{confno}"
|
38
|
+
url += "&#{params}" if params
|
36
39
|
run command, [url]
|
37
40
|
end
|
38
41
|
end
|