calendar-assistant 0.2.1 → 0.3.0

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.
@@ -20,28 +20,30 @@ class CalendarAssistant
20
20
  end
21
21
 
22
22
  def self.now
23
- CalendarAssistant::Event.new(GCal::Event.new start: GCal::EventDateTime.new(date_time: Time.now),
24
- end: GCal::EventDateTime.new(date_time: Time.now),
25
- summary: Rainbow(" now ").inverse.faint)
23
+ CalendarAssistant::Event.new(
24
+ Google::Apis::CalendarV3::Event.new(start: Google::Apis::CalendarV3::EventDateTime.new(date_time: Time.now),
25
+ end: Google::Apis::CalendarV3::EventDateTime.new(date_time: Time.now),
26
+ summary: Rainbow(" now ").inverse.faint)
27
+ )
26
28
  end
27
29
 
28
30
  def self.find_av_uri ca, timespec
29
31
  time = Chronic.parse timespec
30
32
  range = time..(time+5.minutes)
31
- events = ca.find_events range
33
+ event_set = ca.find_events range
32
34
 
33
- [Google::Apis::CalendarV3::Event::Response::ACCEPTED,
34
- Google::Apis::CalendarV3::Event::Response::TENTATIVE,
35
- Google::Apis::CalendarV3::Event::Response::NEEDS_ACTION,
35
+ [CalendarAssistant::Event::Response::ACCEPTED,
36
+ CalendarAssistant::Event::Response::TENTATIVE,
37
+ CalendarAssistant::Event::Response::NEEDS_ACTION,
36
38
  ].each do |response|
37
- events.reverse.select do |event|
39
+ event_set.events.reverse.select do |event|
38
40
  event.response_status == response
39
41
  end.each do |event|
40
- return [event, event.av_uri] if event.av_uri
42
+ return [event_set.new(event), event.av_uri] if event.av_uri
41
43
  end
42
44
  end
43
45
 
44
- nil
46
+ event_set.new(nil)
45
47
  end
46
48
 
47
49
  class Out
@@ -80,78 +82,139 @@ class CalendarAssistant
80
82
  return false if event.start_date != Date.today
81
83
 
82
84
  if event.start_time > Time.now
83
- puts ca.event_description(CLIHelpers.now)
85
+ puts event_description(CLIHelpers.now)
84
86
  return true
85
87
  end
86
88
 
87
89
  false
88
90
  end
89
91
 
90
- def print_events ca, events, options={}
91
- unless options[:omit_title]
92
- puts Rainbow("#{ca.calendar.id} (all times in #{ca.calendar.time_zone})\n").italic
93
- options = options.merge(omit_title: true)
92
+ def print_events ca, event_set, omit_title: false
93
+ unless omit_title
94
+ er = event_set.event_repository
95
+ puts Rainbow("#{er.calendar.id} (all times in #{er.calendar.time_zone})\n").italic
94
96
  end
95
97
 
96
- if events.is_a?(Hash)
97
- events.each do |key, value|
98
+ if event_set.is_a?(EventSet::Hash)
99
+ event_set.events.each do |key, value|
98
100
  puts Rainbow(key.to_s.capitalize + ":").bold.italic
99
- print_events ca, value, options
101
+ print_events ca, event_set.new(value), omit_title: true
100
102
  end
101
103
  return
102
104
  end
103
105
 
104
- events = Array(events)
106
+ events = Array(event_set.events)
105
107
  if events.empty?
106
108
  puts "No events in this time range."
107
109
  return
108
110
  end
109
111
 
110
112
  display_events = events.select do |event|
111
- ! options[:commitments] || event.commitment?
113
+ ! ca.config.setting(CalendarAssistant::Config::Keys::Options::COMMITMENTS) || event.commitment?
112
114
  end
113
115
 
114
116
  printed_now = false
115
117
  display_events.each do |event|
116
118
  printed_now = print_now! ca, event, printed_now
117
- puts ca.event_description(event)
118
- pp event if options[:debug]
119
+ puts event_description(event)
120
+ pp event if ca.config.debug?
119
121
  end
120
122
 
121
123
  puts
122
124
  end
123
125
 
124
- def print_available_blocks ca, events, options={}
125
- unless options[:omit_title]
126
- puts Rainbow(sprintf("%s\n- all times in %s\n- looking for blocks at least %s long\n",
127
- ca.calendar.id,
128
- ca.calendar.time_zone,
129
- ChronicDuration.output(ChronicDuration.parse(ca.config.setting(Config::Keys::Settings::MEETING_LENGTH))))
130
- ).italic
131
- options = options.merge(omit_title: true)
126
+ def print_available_blocks ca, event_set, omit_title: false
127
+ ers = ca.config.attendees.map { |calendar_id| ca.event_repository calendar_id }
128
+ time_zones = ers.map { |er| er.calendar.time_zone }.uniq
129
+
130
+ unless omit_title
131
+ puts Rainbow(ers.map { |er| er.calendar.id }.join(", ")).italic
132
+ puts Rainbow(sprintf("- looking for blocks at least %s long",
133
+ ChronicDuration.output(
134
+ ChronicDuration.parse(
135
+ ca.config.setting(Config::Keys::Settings::MEETING_LENGTH))))).italic
136
+ time_zones.each do |time_zone|
137
+ puts Rainbow(sprintf("- between %s and %s in %s",
138
+ ca.config.setting(Config::Keys::Settings::START_OF_DAY),
139
+ ca.config.setting(Config::Keys::Settings::END_OF_DAY),
140
+ time_zone,
141
+ )).italic
142
+ end
143
+ puts
132
144
  end
133
145
 
134
- if events.is_a?(Hash)
135
- events.each do |key, value|
146
+ if event_set.is_a?(EventSet::Hash)
147
+ event_set.events.each do |key, value|
136
148
  puts(sprintf(Rainbow("Availability on %s:\n").bold,
137
149
  key.strftime("%A, %B %-d")))
138
- print_available_blocks ca, value, options
150
+ print_available_blocks ca, event_set.new(value), omit_title: true
139
151
  puts
140
152
  end
141
153
  return
142
154
  end
143
155
 
144
- events = Array(events)
156
+ events = Array(event_set.events)
145
157
  if events.empty?
146
158
  puts " (No available blocks in this time range.)"
147
159
  return
148
160
  end
149
161
 
150
162
  events.each do |event|
151
- puts(sprintf(" %s - %s",
152
- event.start.date_time.strftime("%-l:%M%P"),
153
- event.end.date_time.strftime("%-l:%M%P")))
154
- pp event if options[:debug]
163
+ line = []
164
+ time_zones.each do |time_zone|
165
+ line << sprintf("%s - %s",
166
+ event.start_time.in_time_zone(time_zone).strftime("%l:%M%P"),
167
+ event.end_time.in_time_zone(time_zone).strftime("%l:%M%P %Z"))
168
+ end
169
+ line.uniq!
170
+ puts " • " + line.join(" / ") + Rainbow(" (" + event.duration + ")").italic
171
+ pp event if ca.config.debug?
172
+ end
173
+ end
174
+
175
+ def event_description event
176
+ s = sprintf("%-25.25s", event_date_description(event))
177
+
178
+ date_ansi_codes = []
179
+ date_ansi_codes << :bright if event.current?
180
+ date_ansi_codes << :faint if event.past?
181
+ s = date_ansi_codes.inject(Rainbow(s)) { |text, ansi| text.send ansi }
182
+
183
+ s += Rainbow(sprintf(" | %s", event.view_summary)).bold
184
+
185
+ attributes = []
186
+ unless event.private?
187
+ attributes << "recurring" if event.recurring?
188
+ attributes << "not-busy" unless event.busy?
189
+ attributes << "self" if event.self? && !event.private?
190
+ attributes << "1:1" if event.one_on_one?
191
+ attributes << "awaiting" if event.awaiting?
192
+ end
193
+
194
+ attributes << event.visibility if event.explicit_visibility?
195
+
196
+ s += Rainbow(sprintf(" (%s)", attributes.to_a.sort.join(", "))).italic unless attributes.empty?
197
+
198
+ s = Rainbow(Rainbow.uncolor(s)).faint.strike if event.declined?
199
+
200
+ s
201
+ end
202
+
203
+ def event_date_description event
204
+ if event.all_day?
205
+ start_date = event.start_date
206
+ end_date = event.end_date
207
+ if (end_date - start_date) <= 1
208
+ event.start.to_s
209
+ else
210
+ sprintf("%s - %s", start_date, end_date - 1.day)
211
+ end
212
+ else
213
+ if event.start_date == event.end_date
214
+ sprintf("%s - %s", event.start.date_time.strftime("%Y-%m-%d %H:%M"), event.end.date_time.strftime("%H:%M"))
215
+ else
216
+ sprintf("%s - %s", event.start.date_time.strftime("%Y-%m-%d %H:%M"), event.end.date_time.strftime("%Y-%m-%d %H:%M"))
217
+ end
155
218
  end
156
219
  end
157
220
  end
@@ -1,30 +1,48 @@
1
- require "toml"
2
-
3
1
  class CalendarAssistant
4
2
  class Config
3
+ autoload :TokenStore, "calendar_assistant/config/token_store"
4
+
5
5
  class TomlParseFailure < CalendarAssistant::BaseException ; end
6
6
  class NoConfigFileToPersist < CalendarAssistant::BaseException ; end
7
7
  class NoTokensAuthorized < CalendarAssistant::BaseException ; end
8
8
  class AccessingHashAsScalar < CalendarAssistant::BaseException ; end
9
9
 
10
10
  CONFIG_FILE_PATH = File.join ENV["HOME"], ".calendar-assistant"
11
+ DEFAULT_CALENDAR_ID = "primary"
11
12
 
12
13
  module Keys
13
14
  TOKENS = "tokens"
14
15
  SETTINGS = "settings"
15
16
 
17
+ #
18
+ # Settings are values that have a value in DEFAULT_SETTINGS below,
19
+ # and which can be overridden by entries in the user config file
20
+ #
16
21
  module Settings
17
- PROFILE = "profile"
18
- MEETING_LENGTH = "meeting-length"
19
- START_OF_DAY = "start-of-day"
20
- END_OF_DAY = "end-of-day"
22
+ PROFILE = "profile" # string
23
+ MEETING_LENGTH = "meeting-length" # ChronicDuration
24
+ START_OF_DAY = "start-of-day" # BusinessTime
25
+ END_OF_DAY = "end-of-day" # BusinessTime
26
+ end
27
+
28
+ #
29
+ # Options are ephemeral command-line flag settings which _may_
30
+ # have a value in DEFAULT_SETTINGS below
31
+ #
32
+ module Options
33
+ COMMITMENTS = "commitments" # bool
34
+ JOIN = "join" # bool
35
+ ATTENDEES = "attendees" # array of calendar ids (comma-delimited)
36
+ LOCAL_STORE = "local-store" # filename
37
+ DEBUG = "debug" # bool
21
38
  end
22
39
  end
23
40
 
24
41
  DEFAULT_SETTINGS = {
25
- Keys::Settings::MEETING_LENGTH => "30m", # ChronicDuration
26
- Keys::Settings::START_OF_DAY => "9am", # Chronic
27
- Keys::Settings::END_OF_DAY => "6pm", # Chronic
42
+ Keys::Settings::MEETING_LENGTH => "30m", # ChronicDuration
43
+ Keys::Settings::START_OF_DAY => "9am", # BusinessTime
44
+ Keys::Settings::END_OF_DAY => "6pm", # BusinessTime
45
+ Keys::Options::ATTENDEES => [DEFAULT_CALENDAR_ID], # array of calendar ids
28
46
  }
29
47
 
30
48
  attr_reader :config_file_path, :user_config, :options, :defaults
@@ -58,9 +76,23 @@ class CalendarAssistant
58
76
  @options = options
59
77
  end
60
78
 
79
+ def in_env &block
80
+ # this is totally not thread-safe
81
+ orig_b_o_d = BusinessTime::Config.beginning_of_workday
82
+ orig_e_o_d = BusinessTime::Config.end_of_workday
83
+ begin
84
+ BusinessTime::Config.beginning_of_workday = setting(Config::Keys::Settings::START_OF_DAY)
85
+ BusinessTime::Config.end_of_workday = setting(Config::Keys::Settings::END_OF_DAY)
86
+ yield
87
+ ensure
88
+ BusinessTime::Config.beginning_of_workday = orig_b_o_d
89
+ BusinessTime::Config.end_of_workday = orig_e_o_d
90
+ end
91
+ end
92
+
61
93
  def profile_name
62
94
  # CLI option takes precedence
63
- return options["profile"] if options["profile"]
95
+ return options[Keys::Settings::PROFILE] if options[Keys::Settings::PROFILE]
64
96
 
65
97
  # then a configured preference takes precedence
66
98
  default = get([Keys::SETTINGS, Keys::Settings::PROFILE])
@@ -91,12 +123,26 @@ class CalendarAssistant
91
123
  Config.set_in_hash user_config, keypath, value
92
124
  end
93
125
 
126
+ #
127
+ # note that, despite the name, this method returns both options
128
+ # and settings
129
+ #
94
130
  def setting setting_name
95
131
  Config.find_in_hash(options, setting_name) ||
96
132
  Config.find_in_hash(user_config, [Keys::SETTINGS, setting_name]) ||
97
133
  Config.find_in_hash(defaults, setting_name)
98
134
  end
99
135
 
136
+ def settings
137
+ setting_names = CalendarAssistant::Config::Keys::Settings.constants.map do |k|
138
+ CalendarAssistant::Config::Keys::Settings.const_get k
139
+ end
140
+ setting_names.inject({}) do |settings, key|
141
+ settings[key] = setting key
142
+ settings
143
+ end
144
+ end
145
+
100
146
  def tokens
101
147
  Config.find_in_hash(user_config, Keys::TOKENS) ||
102
148
  Config.set_in_hash(user_config, Keys::TOKENS, {})
@@ -118,6 +164,21 @@ class CalendarAssistant
118
164
  end
119
165
  end
120
166
 
167
+ #
168
+ # helper method for Keys::Options::ATTENDEES
169
+ #
170
+ def attendees
171
+ a = setting(Keys::Options::ATTENDEES)
172
+ if a.is_a?(String)
173
+ a = a.split(",")
174
+ end
175
+ a
176
+ end
177
+
178
+ def debug?
179
+ setting(Keys::Options::DEBUG)
180
+ end
181
+
121
182
  private
122
183
 
123
184
  def self.find_in_hash hash, keypath
@@ -150,5 +211,3 @@ class CalendarAssistant
150
211
  end
151
212
  end
152
213
  end
153
-
154
- require "calendar_assistant/config/token_store"
@@ -0,0 +1,16 @@
1
+ class CalendarAssistant
2
+ module DateHelpers
3
+ def self.cast_dates attributes
4
+ attributes.each_with_object({}) do |(key, value), object|
5
+ if value.is_a?(Time) || value.is_a?(DateTime)
6
+ object[key] = Google::Apis::CalendarV3::EventDateTime.new(date_time: value)
7
+ elsif value.is_a?(Date)
8
+ object[key] = Google::Apis::CalendarV3::EventDateTime.new(date: value.to_s)
9
+ else
10
+ object[key] = value
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -1,10 +1,42 @@
1
1
  class CalendarAssistant
2
2
  class Event < SimpleDelegator
3
+ #
4
+ # constants describing enumerated attribute values
5
+ # see https://developers.google.com/calendar/v3/reference/events
6
+ #
7
+ module RealResponse
8
+ DECLINED = "declined"
9
+ ACCEPTED = "accepted"
10
+ NEEDS_ACTION = "needsAction"
11
+ TENTATIVE = "tentative"
12
+ end
13
+
14
+ module Response
15
+ include RealResponse
16
+ SELF = "self" # not part of Google's API, but useful to represent meetings-for-myself
17
+ end
3
18
 
19
+ module Transparency
20
+ TRANSPARENT = "transparent"
21
+ OPAQUE = "opaque"
22
+ end
23
+
24
+ module Visibility
25
+ DEFAULT = "default"
26
+ PUBLIC = "public"
27
+ PRIVATE = "private"
28
+ end
29
+
30
+ #
31
+ # constants describing behavior
32
+ #
4
33
  LOCATION_EVENT_REGEX = /^#{CalendarAssistant::EMOJI_WORLDMAP}/
5
34
 
35
+ #
36
+ # methods
37
+ #
6
38
  def update **args
7
- super
39
+ update!(**args)
8
40
  self
9
41
  end
10
42
 
@@ -13,14 +45,14 @@ class CalendarAssistant
13
45
  end
14
46
 
15
47
  def all_day?
16
- !! start.to_date
48
+ start.try(:date) || self.end.try(:date)
17
49
  end
18
50
 
19
51
  def past?
20
52
  if all_day?
21
- Date.today >= self.end.to_date
53
+ Date.today >= end_date
22
54
  else
23
- Time.now >= self.end.date_time
55
+ Time.now >= end_time
24
56
  end
25
57
  end
26
58
 
@@ -30,18 +62,26 @@ class CalendarAssistant
30
62
 
31
63
  def future?
32
64
  if all_day?
33
- self.start.to_date > Date.today
65
+ start_date > Date.today
34
66
  else
35
- self.start.date_time > Time.now
67
+ start_time > Time.now
36
68
  end
37
69
  end
38
70
 
39
71
  def accepted?
40
- response_status == GCal::Event::Response::ACCEPTED
72
+ response_status == CalendarAssistant::Event::Response::ACCEPTED
41
73
  end
42
74
 
43
75
  def declined?
44
- response_status == GCal::Event::Response::DECLINED
76
+ response_status == CalendarAssistant::Event::Response::DECLINED
77
+ end
78
+
79
+ def awaiting?
80
+ response_status == CalendarAssistant::Event::Response::NEEDS_ACTION
81
+ end
82
+
83
+ def self?
84
+ response_status == CalendarAssistant::Event::Response::SELF
45
85
  end
46
86
 
47
87
  def one_on_one?
@@ -52,7 +92,7 @@ class CalendarAssistant
52
92
  end
53
93
 
54
94
  def busy?
55
- transparency != GCal::Event::Transparency::TRANSPARENT
95
+ transparency != CalendarAssistant::Event::Transparency::TRANSPARENT
56
96
  end
57
97
 
58
98
  def commitment?
@@ -62,20 +102,24 @@ class CalendarAssistant
62
102
  end
63
103
 
64
104
  def private?
65
- visibility == GCal::Event::Visibility::PRIVATE
105
+ visibility == CalendarAssistant::Event::Visibility::PRIVATE
66
106
  end
67
107
 
68
108
  def public?
69
- visibility == GCal::Event::Visibility::PUBLIC
109
+ visibility == CalendarAssistant::Event::Visibility::PUBLIC
70
110
  end
71
111
 
72
112
  def explicit_visibility?
73
113
  private? || public?
74
114
  end
75
115
 
116
+ def recurring?
117
+ !!recurring_event_id
118
+ end
119
+
76
120
  def start_time
77
121
  if all_day?
78
- start.to_date.beginning_of_day
122
+ start_date.beginning_of_day
79
123
  else
80
124
  start.date_time
81
125
  end
@@ -85,7 +129,23 @@ class CalendarAssistant
85
129
  if all_day?
86
130
  start.to_date
87
131
  else
88
- start.date_time.to_date
132
+ start_time.to_date
133
+ end
134
+ end
135
+
136
+ def end_time
137
+ if all_day?
138
+ end_date.beginning_of_day
139
+ else
140
+ self.end.date_time
141
+ end
142
+ end
143
+
144
+ def end_date
145
+ if all_day?
146
+ self.end.to_date
147
+ else
148
+ end_time.to_date
89
149
  end
90
150
  end
91
151
 
@@ -94,5 +154,55 @@ class CalendarAssistant
94
154
  return "(no title)" if summary.nil? || summary.blank?
95
155
  summary
96
156
  end
157
+
158
+ def duration
159
+ if all_day?
160
+ days = (end_date - start_date).to_i
161
+ return "#{days}d"
162
+ end
163
+
164
+ p = ActiveSupport::Duration.build(end_time - start_time).parts
165
+ s = []
166
+ s << "#{p[:hours]}h" if p.has_key?(:hours)
167
+ s << "#{p[:minutes]}m" if p.has_key?(:minutes)
168
+ s.join(" ")
169
+ end
170
+
171
+ def human_attendees
172
+ return nil if attendees.nil?
173
+ attendees.select { |a| ! a.resource }
174
+ end
175
+
176
+ def attendee id
177
+ return nil if attendees.nil?
178
+ attendees.find do |attendee|
179
+ attendee.email == id
180
+ end
181
+ end
182
+
183
+ def response_status
184
+ return CalendarAssistant::Event::Response::SELF if attendees.nil?
185
+ attendees.each do |attendee|
186
+ return attendee.response_status if attendee.self
187
+ end
188
+ nil
189
+ end
190
+
191
+ def av_uri
192
+ @av_uri ||= begin
193
+ description_link = CalendarAssistant::StringHelpers.find_uri_for_domain(description, "zoom.us")
194
+ return description_link if description_link
195
+
196
+ location_link = CalendarAssistant::StringHelpers.find_uri_for_domain(location, "zoom.us")
197
+ return location_link if location_link
198
+
199
+ return hangout_link if hangout_link
200
+ nil
201
+ end
202
+ end
203
+
204
+ def contains? time
205
+ start_time <= time && time < end_time
206
+ end
97
207
  end
98
- end
208
+ end