calendar-assistant 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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