calendar-assistant 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72759e81d1f7e1a67ae2efcc7cbf1f3698c8d62262e68e7fc53cad7362f5311f
4
- data.tar.gz: 0e14e1c2793340d7ead49349e116fad4da8f67e9ca87e2c22d3ad88088d20cc6
3
+ metadata.gz: 57a695007c637201348e3d625d20ea076b33a08e5d00cf1a0c8dc2d7e09c68ec
4
+ data.tar.gz: 57a2575e0a149d1a9480bf7d3990a412cc8f23dc8409a3ccf33a38f9c044e81f
5
5
  SHA512:
6
- metadata.gz: 5e7ec4c110239ae51755851c694d421575ace9ab0c9c04ab3e79cde8729815bd65d87bedb4570f02ac751d91439db7dbce7b5af4800177ec30ab1c01d4f9b279
7
- data.tar.gz: b9a362d7a750b767fb39d2d9f76dd0f758d0de23b8ddaf3d4b7e4759c71624febc699c25617ec5b058b788433754bf07bc6e0931511a9d9504fcd4957f06da8e
6
+ metadata.gz: 5340ebdb6a3dd34bf39315debc91b7f5c1ba05a4915e5991d43aeb7046c949f39b7fb9a7391c8f20e76412f56e1b5de219816f093d2d8ed470d06d9f2304d3a9
7
+ data.tar.gz: c0322c182c45fe87d94199219a434db96a9f8b13d702e2b2832b5a846914afb98c29cb669081b6ae4324229412102f48196ce6dde969f331941bd188d4897007
data/Gemfile CHANGED
@@ -1,3 +1,11 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
+
5
+ group :optional do
6
+ # These gems are not required for a functioning test suite, so not listing them in the gemspec.
7
+ # I personally find them useful in the developer role, though.
8
+ gem "autotest"
9
+ gem "rspec-autotest"
10
+ gem "test_notifier"
11
+ end
data/README.md CHANGED
@@ -66,8 +66,6 @@ Options:
66
66
  -h, -?, [--help], [--no-help]
67
67
  -p, [--profile=PROFILE] # the profile you'd like to use (if different from default)
68
68
  [--debug], [--no-debug] # how dare you suggest there are bugs
69
-
70
-
71
69
  </pre>
72
70
 
73
71
 
@@ -86,7 +84,6 @@ Options:
86
84
  [--debug], [--no-debug] # how dare you suggest there are bugs
87
85
 
88
86
  Dump your configuration parameters (merge of defaults and overrides from /home/flavorjones/.calendar-assistant)
89
-
90
87
  </pre>
91
88
 
92
89
  The output is TOML, which is suitable for dumping into `~/.calendar-assistant` and editing.
@@ -99,7 +96,6 @@ end-of-day = "6pm"
99
96
  meeting-length = "30m"
100
97
  profile = "work"
101
98
  start-of-day = "9am"
102
-
103
99
  </pre>
104
100
 
105
101
 
@@ -116,7 +112,7 @@ Options:
116
112
  [--debug], [--no-debug] # how dare you suggest there are bugs
117
113
 
118
114
  Description:
119
- Create and authorize a named profile (e.g., "work", "home", "flastname@company.tld") to access your calendar.
115
+ Create and authorize a named profile (e.g., "work", "home", "me@example.com") to access your calendar.
120
116
 
121
117
  When setting up a profile, you'll be asked to visit a URL to authenticate, grant authorization, and generate and persist an access token.
122
118
 
@@ -128,7 +124,6 @@ Description:
128
124
  1. Turn on the Google API for your account
129
125
  2. Create a new Google API Project
130
126
  3. Download the configuration file for the Project, and name it as `credentials.json`
131
-
132
127
  </pre>
133
128
 
134
129
  This command will generate a URL which you should load in your browser while logged in as the Google account you wish to authorize. Generate a token, and paste the token back into `calendar-assistant`.
@@ -150,60 +145,55 @@ Options:
150
145
  [--debug], [--no-debug] # how dare you suggest there are bugs
151
146
 
152
147
  Show your events for a date or range of dates (default 'today')
153
-
154
148
  </pre>
155
149
 
156
150
  For example: display all events scheduled for tomorrow:
157
151
 
158
152
  <pre>
159
153
  <b>$</b> calendar-assistant show --profile=work 2018-10-01
160
- <i>mdalessio@pivotal.io (all times in America/New_York)
154
+ <i>me@example.com (all times in America/New_York)
161
155
  </i>
162
- 2018-10-01 <b> | 🗺 NJ</b><i> (not-busy, self)</i>
163
- <strike>2018-10-01 03:30 - 05:00 | INTERNATIONAL COFFEE DAYYYYYYYY</strike>
164
- <strike>2018-10-01 07:30 - 08:30 | Lunch and -GDPR</strike>
165
- <strike>2018-10-01 07:30 - 08:30 | Lunch & Learn</strike>
166
- 2018-10-01 08:00 - 09:00<b> | Commuting/Email</b><i> (recurring, self)</i>
167
- 2018-10-01 09:00 - 10:30<b> | None</b><i> (self)</i>
168
- 2018-10-01 10:30 - 10:55<b> | Mike D / Stev 1:1</b><i> (1:1, recurring)</i>
169
- 2018-10-01 11:00 - 11:30<b> | Dublin Office Status Meeting</b><i> (recurring)</i>
170
- 2018-10-01 11:30 - 12:00<b> | Mike/Rupa 1:1</b><i> (1:1, recurring)</i>
171
- <strike>2018-10-01 11:50 - 12:00 | Reminder: CF Standup prep (recurring)</strike>
172
- 2018-10-01 12:00 - 12:30<b> | Lunch</b><i> (self)</i>
173
- <strike>2018-10-01 12:15 - 12:30 | CF NYC Standup (recurring)</strike>
174
- <strike>2018-10-01 12:30 - 13:30 | Office Events Retro</strike>
175
- 2018-10-01 12:30 - 13:30<b> | Global Director's Check-In</b><i> (recurring)</i>
176
- 2018-10-01 13:30 - 14:50<b> | proactivity</b><i> (self)</i>
177
- <strike>2018-10-01 13:30 - 14:30 | Psychological Safety Workshop (Session 1)</strike>
178
- 2018-10-01 15:00 - 15:30<b> | Matthew/Mike</b><i> (1:1)</i>
179
- 2018-10-01 16:00 - 17:00<b> | Mike/Ryan T. 1:1</b><i> (1:1, recurring)</i>
180
- 2018-10-01 16:45 - 17:00<b> | Manager Initiative check-in</b><i> (recurring)</i>
181
- 2018-10-01 17:00 - 17:30<b> | CF Security Council Sync</b><i> (recurring)</i>
182
- 2018-10-01 17:30 - 17:55<b> | Mike / Dieu 1:1</b><i> (1:1, recurring)</i>
183
- <strike>2018-10-01 18:00 - 20:30 | Steak!</strike>
184
- <strike>2018-10-01 18:30 - 19:00 | SF CF Directors / HR Bi-weekly (recurring)</strike>
185
- <strike>2018-10-01 19:00 - 19:30 | CF SF Manager Sit Down (recurring)</strike>
186
-
187
-
156
+ 2018-10-01 <b> | 🗺 Mines of Moria </b><i> (not-busy, self)</i>
157
+ <strike>2018-10-01 03:30 - 05:00 | Generate enterprise portals </strike>
158
+ <strike>2018-10-01 07:30 - 08:30 | Incentivize wireless functionalities </strike>
159
+ <strike>2018-10-01 07:30 - 08:30 | Brand end-to-end action-items </strike>
160
+ 2018-10-01 08:00 - 09:00<b> | Grow web-enabled synergies </b><i> (recurring, self)</i>
161
+ 2018-10-01 09:00 - 10:30<b> | Disintermediate integrated web services </b><i> (self)</i>
162
+ 2018-10-01 10:30 - 10:55<b> | Architect sticky synergies </b><i> (1:1, recurring)</i>
163
+ 2018-10-01 11:00 - 11:30<b> | Leverage out-of-the-box supply-chains </b><i> (recurring)</i>
164
+ 2018-10-01 11:30 - 12:00<b> | Architect dot-com portals </b><i> (1:1, recurring)</i>
165
+ <strike>2018-10-01 11:50 - 12:00 | Grow out-of-the-box convergence </strike>
166
+ 2018-10-01 12:00 - 12:30<b> | Morph leading-edge experiences </b><i> (self)</i>
167
+ <strike>2018-10-01 12:15 - 12:30 | Revolutionize innovative communities </strike>
168
+ <strike>2018-10-01 12:30 - 13:30 | Evolve mission-critical e-services </strike>
169
+ 2018-10-01 12:30 - 13:30<b> | Embrace world-class e-tailers </b><i> (recurring)</i>
170
+ 2018-10-01 13:30 - 14:50<b> | Deliver out-of-the-box infomediaries </b><i> (self)</i>
171
+ <strike>2018-10-01 13:30 - 14:30 | Transform user-centric e-tailers </strike>
172
+ 2018-10-01 15:00 - 15:30<b> | Aggregate magnetic e-business </b><i> (1:1)</i>
173
+ 2018-10-01 16:00 - 17:00<b> | Leverage turn-key e-tailers </b><i> (1:1, recurring)</i>
174
+ 2018-10-01 16:45 - 17:00<b> | Monetize real-time interfaces </b><i> (recurring)</i>
175
+ 2018-10-01 17:00 - 17:30<b> | Generate out-of-the-box technologies </b><i> (recurring)</i>
176
+ 2018-10-01 17:30 - 17:55<b> | Strategize enterprise communities </b><i> (1:1, recurring)</i>
177
+ <strike>2018-10-01 18:00 - 20:30 | Redefine 24/365 infrastructures </strike>
178
+ <strike>2018-10-01 18:30 - 19:00 | Innovate vertical e-business </strike>
179
+ <strike>2018-10-01 19:00 - 19:30 | Repurpose seamless deliverables </strike>
188
180
  </pre>
189
181
 
190
182
  Display _only_ the commitments I have to other people using the `-c` option:
191
183
 
192
184
  <pre>
193
185
  <b>$</b> calendar-assistant show -c 2018-10-01
194
- <i>mdalessio@pivotal.io (all times in America/New_York)
186
+ <i>me@example.com (all times in America/New_York)
195
187
  </i>
196
- 2018-10-01 10:30 - 10:55<b> | Mike D / Stev 1:1</b><i> (1:1, recurring)</i>
197
- 2018-10-01 11:00 - 11:30<b> | Dublin Office Status Meeting</b><i> (recurring)</i>
198
- 2018-10-01 11:30 - 12:00<b> | Mike/Rupa 1:1</b><i> (1:1, recurring)</i>
199
- 2018-10-01 12:30 - 13:30<b> | Global Director's Check-In</b><i> (recurring)</i>
200
- 2018-10-01 15:00 - 15:30<b> | Matthew/Mike</b><i> (1:1)</i>
201
- 2018-10-01 16:00 - 17:00<b> | Mike/Ryan T. 1:1</b><i> (1:1, recurring)</i>
202
- 2018-10-01 16:45 - 17:00<b> | Manager Initiative check-in</b><i> (recurring)</i>
203
- 2018-10-01 17:00 - 17:30<b> | CF Security Council Sync</b><i> (recurring)</i>
204
- 2018-10-01 17:30 - 17:55<b> | Mike / Dieu 1:1</b><i> (1:1, recurring)</i>
205
-
206
-
188
+ 2018-10-01 10:30 - 10:55<b> | Exploit b2b synergies </b><i> (1:1, recurring)</i>
189
+ 2018-10-01 11:00 - 11:30<b> | Reinvent customized systems </b><i> (recurring)</i>
190
+ 2018-10-01 11:30 - 12:00<b> | Incentivize visionary users </b><i> (1:1, recurring)</i>
191
+ 2018-10-01 12:30 - 13:30<b> | Engineer robust roi </b><i> (recurring)</i>
192
+ 2018-10-01 15:00 - 15:30<b> | Exploit next-generation content </b><i> (1:1)</i>
193
+ 2018-10-01 16:00 - 17:00<b> | Integrate sticky platforms </b><i> (1:1, recurring)</i>
194
+ 2018-10-01 16:45 - 17:00<b> | Enhance intuitive portals </b><i> (recurring)</i>
195
+ 2018-10-01 17:00 - 17:30<b> | Reinvent viral platforms </b><i> (recurring)</i>
196
+ 2018-10-01 17:30 - 17:55<b> | Incubate magnetic interfaces </b><i> (1:1, recurring)</i>
207
197
  </pre>
208
198
 
209
199
 
@@ -222,7 +212,6 @@ Options:
222
212
  [--debug], [--no-debug] # how dare you suggest there are bugs
223
213
 
224
214
  Open the URL for a video call attached to your meeting at time TIME (default 'now')
225
-
226
215
  </pre>
227
216
 
228
217
  Some examples:
@@ -258,7 +247,6 @@ Options:
258
247
  [--debug], [--no-debug] # how dare you suggest there are bugs
259
248
 
260
249
  Show your availability for a date or range of dates (default 'today')
261
-
262
250
  </pre>
263
251
 
264
252
 
@@ -266,7 +254,7 @@ For example: show me my available time over a chunk of time:
266
254
 
267
255
  <pre>
268
256
  <b>$</b> calendar-assistant avail 2018-10-02..2018-10-04
269
- <i>mdalessio@pivotal.io
257
+ <i>me@example.com
270
258
  - all times in America/New_York
271
259
  - looking for blocks at least 30 mins long
272
260
  </i>
@@ -286,8 +274,6 @@ For example: show me my available time over a chunk of time:
286
274
  <b>Availability on Thursday, October 4:
287
275
  </b>
288
276
  • 10:55am - 1:00pm
289
-
290
-
291
277
  </pre>
292
278
 
293
279
 
@@ -295,7 +281,7 @@ You can also set start and end times for the search, which is useful when lookin
295
281
 
296
282
  <pre>
297
283
  <b>$</b> calendar-assistant avail 2018-10-02..2018-10-04 -s 12pm -e 7pm
298
- <i>mdalessio@pivotal.io
284
+ <i>me@example.com
299
285
  - all times in America/New_York
300
286
  - looking for blocks at least 30 mins long
301
287
  </i>
@@ -317,8 +303,6 @@ You can also set start and end times for the search, which is useful when lookin
317
303
  </b>
318
304
  • 10:55am - 1:00pm
319
305
  • 6:00pm - 7:00pm
320
-
321
-
322
306
  </pre>
323
307
 
324
308
 
@@ -337,7 +321,6 @@ Options:
337
321
  [--debug], [--no-debug] # how dare you suggest there are bugs
338
322
 
339
323
  Set your location to LOCATION for a date or range of dates (default 'today')
340
-
341
324
  </pre>
342
325
 
343
326
  **Note** that you can only be in one place at a time, so existing location events may be modified or deleted when new overlapping events are created.
@@ -380,19 +363,16 @@ Options:
380
363
  [--debug], [--no-debug] # how dare you suggest there are bugs
381
364
 
382
365
  Show your location for a date or range of dates (default 'today')
383
-
384
366
  </pre>
385
367
 
386
368
  For example:
387
369
 
388
370
  <pre>
389
371
  <b>$</b> calendar-assistant location "2018-09-24...2018-09-28"
390
- <i>mdalessio@pivotal.io (all times in America/New_York)
372
+ <i>me@example.com (all times in America/New_York)
391
373
  </i>
392
- 2018-09-24 - 2018-09-27 <b> | 🗺 Spring One @ DC</b><i> (not-busy, self)</i>
393
- 2018-09-28 <b> | 🗺 NJ</b><i> (not-busy, self)</i>
394
-
395
-
374
+ 2018-09-24 - 2018-09-27 <b> | 🗺 Grey Mountains </b><i> (not-busy, self)</i>
375
+ 2018-09-28 <b> | 🗺 Dorwinion </b><i> (not-busy, self)</i>
396
376
  </pre>
397
377
 
398
378
 
@@ -30,24 +30,15 @@ class CalendarAssistant
30
30
  time_range.first.to_date..(time_range.last + 1.day).to_date
31
31
  end
32
32
 
33
- def initialize config=CalendarAssistant::Config.new
33
+ def initialize config=CalendarAssistant::Config.new, event_repository: nil
34
34
  @config = config
35
35
  @service = Authorizer.new(config.profile_name, config.token_store).service
36
36
  @calendar = service.get_calendar DEFAULT_CALENDAR_ID
37
+ @event_repository = event_repository || EventRepository.new(@service, DEFAULT_CALENDAR_ID)
37
38
  end
38
39
 
39
40
  def find_events time_range
40
- events = service.list_events(DEFAULT_CALENDAR_ID,
41
- time_min: time_range.first.iso8601,
42
- time_max: time_range.last.iso8601,
43
- order_by: "startTime",
44
- single_events: true,
45
- max_results: 2000,
46
- )
47
- if events.nil? || events.items.nil?
48
- return []
49
- end
50
- events.items
41
+ @event_repository.find(time_range)
51
42
  end
52
43
 
53
44
  def availability time_range
@@ -100,7 +91,7 @@ class CalendarAssistant
100
91
  end
101
92
 
102
93
  def find_location_events time_range
103
- find_events(time_range).select { |e| e.location_event? }
94
+ @event_repository.find(time_range).select { |e| e.location_event? }
104
95
  end
105
96
 
106
97
  def create_location_event time_range, location
@@ -113,24 +104,17 @@ class CalendarAssistant
113
104
  deleted_events = []
114
105
  modified_events = []
115
106
 
116
- event = GCal::Event.new start: GCal::EventDateTime.new(date: range.first.iso8601),
117
- end: GCal::EventDateTime.new(date: range.last.iso8601),
118
- summary: "#{EMOJI_WORLDMAP} #{location}",
119
- transparency: GCal::Event::Transparency::TRANSPARENT
120
-
121
- event = service.insert_event DEFAULT_CALENDAR_ID, event
107
+ event = @event_repository.create(transparency: GCal::Event::Transparency::TRANSPARENT, start: range.first, end: range.last , summary: "#{EMOJI_WORLDMAP} #{location}")
122
108
 
123
109
  existing_events.each do |existing_event|
124
110
  if existing_event.start.date >= event.start.date && existing_event.end.date <= event.end.date
125
- service.delete_event DEFAULT_CALENDAR_ID, existing_event.id
111
+ @event_repository.delete existing_event
126
112
  deleted_events << existing_event
127
113
  elsif existing_event.start.date <= event.end.date && existing_event.end.date > event.end.date
128
- existing_event.update! start: GCal::EventDateTime.new(date: range.last)
129
- service.update_event DEFAULT_CALENDAR_ID, existing_event.id, existing_event
114
+ @event_repository.update existing_event, start: range.last
130
115
  modified_events << existing_event
131
116
  elsif existing_event.start.date < event.start.date && existing_event.end.date >= event.start.date
132
- existing_event.update! end: GCal::EventDateTime.new(date: range.first)
133
- service.update_event DEFAULT_CALENDAR_ID, existing_event.id, existing_event
117
+ @event_repository.update existing_event, end: range.first
134
118
  modified_events << existing_event
135
119
  end
136
120
  end
@@ -158,6 +142,9 @@ class CalendarAssistant
158
142
  attributes << "self" if event.human_attendees.nil? && event.visibility != "private"
159
143
  attributes << "1:1" if event.one_on_one?
160
144
  end
145
+
146
+ attributes << event.visibility if event.explicit_visibility?
147
+
161
148
  s += Rainbow(sprintf(" (%s)", attributes.to_a.sort.join(", "))).italic unless attributes.empty?
162
149
 
163
150
  s = Rainbow(Rainbow.uncolor(s)).faint.strike if event.declined?
@@ -198,5 +185,8 @@ require "calendar_assistant/config"
198
185
  require "calendar_assistant/authorizer"
199
186
  require "calendar_assistant/cli"
200
187
  require "calendar_assistant/string_helpers"
201
- require "calendar_assistant/event_extensions"
202
- require "calendar_assistant/rainbow_extensions"
188
+ require "calendar_assistant/extensions/event_date_time_extensions"
189
+ require "calendar_assistant/extensions/event_extensions"
190
+ require "calendar_assistant/event"
191
+ require "calendar_assistant/event_repository"
192
+ require "calendar_assistant/extensions/rainbow_extensions"
@@ -27,7 +27,7 @@ class CalendarAssistant
27
27
 
28
28
  OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'.freeze
29
29
  APPLICATION_NAME = "Flavorjones Calendar Assistant".freeze
30
- CREDENTIALS_PATH = 'credentials.json'.freeze
30
+ CREDENTIALS_PATH = File.join ENV["HOME"], ".calendar-assistant.client"
31
31
  SCOPE = Google::Apis::CalendarV3::AUTH_CALENDAR
32
32
 
33
33
  attr_reader :profile_name, :config_token_store
@@ -77,9 +77,10 @@ class CalendarAssistant
77
77
  def authorizer
78
78
  @authorizer ||= begin
79
79
  if ! File.exists?(CREDENTIALS_PATH)
80
- raise NoCredentials, "No credentials found. Please run `calendar-assistant help authorize` for help"
80
+ raise NoCredentials, "No credentials found. Please run `calendar-assistant help setup` for instructions"
81
81
  end
82
82
 
83
+ FileUtils.chmod 0600, CREDENTIALS_PATH
83
84
  client_id = Google::Auth::ClientId.from_file(CREDENTIALS_PATH)
84
85
  Google::Auth::UserAuthorizer.new(client_id, SCOPE, config_token_store)
85
86
  end
@@ -7,23 +7,78 @@ require "calendar_assistant/cli_helpers"
7
7
 
8
8
  class CalendarAssistant
9
9
  class CLI < Thor
10
+ def self.supports_profile_option
11
+ option :profile,
12
+ type: :string,
13
+ desc: "the profile you'd like to use (if different from default)",
14
+ aliases: ["-p"]
15
+ end
16
+
10
17
  default_config = CalendarAssistant::Config.new options: options # used in option descriptions
11
18
 
12
- # it's unfortunate that thor does not support this usage of help args
13
19
  class_option :help,
14
20
  type: :boolean,
15
21
  aliases: ["-h", "-?"]
16
-
17
- # note that these options are passed straight through to CLIHelpers.print_events
18
- class_option :profile,
19
- type: :string,
20
- desc: "the profile you'd like to use (if different from default)",
21
- aliases: ["-p"]
22
22
  class_option :debug,
23
23
  type: :boolean,
24
24
  desc: "how dare you suggest there are bugs"
25
25
 
26
26
 
27
+ desc "config",
28
+ "Dump your configuration parameters (merge of defaults and overrides from #{CalendarAssistant::Config::CONFIG_FILE_PATH})"
29
+ def config
30
+ return if handle_help_args
31
+ config = CalendarAssistant::Config.new
32
+ settings = {}
33
+ setting_names = CalendarAssistant::Config::Keys::Settings.constants.map { |k| CalendarAssistant::Config::Keys::Settings.const_get k }
34
+ setting_names.each do |key|
35
+ settings[key] = config.setting key
36
+ end
37
+ puts TOML::Generator.new({CalendarAssistant::Config::Keys::SETTINGS => settings}).body
38
+ end
39
+
40
+
41
+ desc "setup",
42
+ "Link your local calendar-assistant installation to a Google API Client"
43
+ long_desc <<~EOD
44
+ This command will walk you through setting up a Google Cloud
45
+ Project, enabling the Google Calendar API, and saving the
46
+ credentials necessary to access the API on behalf of users.
47
+
48
+ If you already have downloaded client credentials, you don't
49
+ need to run this command. Instead, rename the downloaded JSON
50
+ file to `#{CalendarAssistant::Authorizer::CREDENTIALS_PATH}`
51
+ EOD
52
+ def setup
53
+ out = CLIHelpers::Out.new
54
+ if File.exist? CalendarAssistant::Authorizer::CREDENTIALS_PATH
55
+ out.puts sprintf("Credentials already exist in %s",
56
+ CalendarAssistant::Authorizer::CREDENTIALS_PATH)
57
+ exit 0
58
+ end
59
+
60
+ out.launch "https://developers.google.com/calendar/quickstart/ruby"
61
+ sleep 1
62
+ out.puts <<~EOT
63
+ Please click on "ENABLE THE GOOGLE CALENDAR API" and either create a new project or select an existing project.
64
+
65
+ (If you create a new project, name it something like "yourname-calendar-assistant" so you remember why it exists.)
66
+
67
+ Then click "DOWNLOAD CLIENT CONFIGURATION" to download the credentials to local disk.
68
+
69
+ Finally, paste the contents of the downloaded file here (it should be a complete JSON object):
70
+ EOT
71
+
72
+ json = out.prompt "Paste JSON here"
73
+ File.open(CalendarAssistant::Authorizer::CREDENTIALS_PATH, "w") do |f|
74
+ f.write json
75
+ end
76
+ FileUtils.chmod 0600, CalendarAssistant::Authorizer::CREDENTIALS_PATH
77
+
78
+ out.puts "\nOK! Your next step is to run `calendar-assistant authorize`."
79
+ end
80
+
81
+
27
82
  desc "authorize PROFILE_NAME",
28
83
  "create (or validate) a profile named NAME with calendar access"
29
84
  long_desc <<~EOD
@@ -34,15 +89,8 @@ class CalendarAssistant
34
89
  authenticate, grant authorization, and generate and persist an
35
90
  access token.
36
91
 
37
- In order for this to work, you'll need to follow the
38
- instructions at this URL first:
39
-
40
- > https://developers.google.com/calendar/quickstart/ruby
41
-
42
- Namely, the prerequisites are:
43
- \x5 1. Turn on the Google API for your account
44
- \x5 2. Create a new Google API Project
45
- \x5 3. Download the configuration file for the Project, and name it as `credentials.json`
92
+ In order for this to work, you'll need to have set up your API client
93
+ credentials. Run `calendar-assistant help setup` for instructions.
46
94
  EOD
47
95
  def authorize profile_name
48
96
  return if handle_help_args
@@ -57,6 +105,7 @@ class CalendarAssistant
57
105
  type: :boolean,
58
106
  desc: "only show events that you've accepted with another person",
59
107
  aliases: ["-c"]
108
+ supports_profile_option
60
109
  def show datespec="today"
61
110
  return if handle_help_args
62
111
  config = CalendarAssistant::Config.new options: options
@@ -71,6 +120,7 @@ class CalendarAssistant
71
120
  option :join,
72
121
  type: :boolean, default: true,
73
122
  desc: "launch a browser to join the video call URL"
123
+ supports_profile_option
74
124
  def join timespec="now"
75
125
  return if handle_help_args
76
126
  config = CalendarAssistant::Config.new options: options
@@ -90,6 +140,7 @@ class CalendarAssistant
90
140
 
91
141
  desc "location [DATE | DATERANGE]",
92
142
  "Show your location for a date or range of dates (default 'today')"
143
+ supports_profile_option
93
144
  def location datespec="today"
94
145
  return if handle_help_args
95
146
  config = CalendarAssistant::Config.new options: options
@@ -101,6 +152,7 @@ class CalendarAssistant
101
152
 
102
153
  desc "location-set LOCATION [DATE | DATERANGE]",
103
154
  "Set your location to LOCATION for a date or range of dates (default 'today')"
155
+ supports_profile_option
104
156
  def location_set location, datespec="today"
105
157
  return if handle_help_args
106
158
  config = CalendarAssistant::Config.new options: options
@@ -109,6 +161,7 @@ class CalendarAssistant
109
161
  CLIHelpers::Out.new.print_events ca, events, options
110
162
  end
111
163
 
164
+
112
165
  desc "availability [DATE | DATERANGE | TIMERANGE]",
113
166
  "Show your availability for a date or range of dates (default 'today')"
114
167
  option CalendarAssistant::Config::Keys::Settings::MEETING_LENGTH,
@@ -129,6 +182,7 @@ class CalendarAssistant
129
182
  desc: sprintf("[default %s] find chunks of available time before TIME (which is a Chronic string like '9am' or '14:30')",
130
183
  default_config.setting(CalendarAssistant::Config::Keys::Settings::END_OF_DAY)),
131
184
  aliases: ["-e"]
185
+ supports_profile_option
132
186
  def availability datespec="today"
133
187
  return if handle_help_args
134
188
  config = CalendarAssistant::Config.new options: options
@@ -137,19 +191,6 @@ class CalendarAssistant
137
191
  CLIHelpers::Out.new.print_available_blocks ca, events, options
138
192
  end
139
193
 
140
- desc "config",
141
- "Dump your configuration parameters (merge of defaults and overrides from #{CalendarAssistant::Config::CONFIG_FILE_PATH})"
142
- def config
143
- return if handle_help_args
144
- config = CalendarAssistant::Config.new
145
- settings = {}
146
- setting_names = CalendarAssistant::Config::Keys::Settings.constants.map { |k| CalendarAssistant::Config::Keys::Settings.const_get k }
147
- setting_names.each do |key|
148
- settings[key] = config.setting key
149
- end
150
- puts TOML::Generator.new({CalendarAssistant::Config::Keys::SETTINGS => settings}).body
151
- end
152
-
153
194
  private
154
195
 
155
196
  def handle_help_args
@@ -20,9 +20,9 @@ class CalendarAssistant
20
20
  end
21
21
 
22
22
  def self.now
23
- GCal::Event.new start: GCal::EventDateTime.new(date_time: Time.now),
23
+ CalendarAssistant::Event.new(GCal::Event.new start: GCal::EventDateTime.new(date_time: Time.now),
24
24
  end: GCal::EventDateTime.new(date_time: Time.now),
25
- summary: Rainbow(" now ").inverse.faint
25
+ summary: Rainbow(" now ").inverse.faint)
26
26
  end
27
27
 
28
28
  def self.find_av_uri ca, timespec
@@ -59,6 +59,22 @@ class CalendarAssistant
59
59
  io.puts(*args)
60
60
  end
61
61
 
62
+ def prompt query, default=nil
63
+ loop do
64
+ message = query
65
+ message += " [#{default}]" if default
66
+ message += ": "
67
+ print Rainbow(message).bold
68
+ answer = STDIN.gets.chomp.strip
69
+ if answer.empty?
70
+ return default if default
71
+ puts Rainbow("Please provide an answer.").red
72
+ else
73
+ return answer
74
+ end
75
+ end
76
+ end
77
+
62
78
  def print_now! ca, event, printed_now
63
79
  return true if printed_now
64
80
  return false if event.start_date != Date.today
@@ -45,6 +45,7 @@ class CalendarAssistant
45
45
  end
46
46
  elsif File.exist? config_file_path
47
47
  begin
48
+ FileUtils.chmod 0600, config_file_path
48
49
  TOML.load_file config_file_path
49
50
  rescue Exception => e
50
51
  raise TomlParseFailure, "could not parse #{config_file_path}: #{e}"
@@ -0,0 +1,98 @@
1
+ class CalendarAssistant
2
+ class Event < SimpleDelegator
3
+
4
+ LOCATION_EVENT_REGEX = /^#{CalendarAssistant::EMOJI_WORLDMAP}/
5
+
6
+ def update **args
7
+ super
8
+ self
9
+ end
10
+
11
+ def location_event?
12
+ !! (summary =~ LOCATION_EVENT_REGEX)
13
+ end
14
+
15
+ def all_day?
16
+ !! start.to_date
17
+ end
18
+
19
+ def past?
20
+ if all_day?
21
+ Date.today >= self.end.to_date
22
+ else
23
+ Time.now >= self.end.date_time
24
+ end
25
+ end
26
+
27
+ def current?
28
+ ! (past? || future?)
29
+ end
30
+
31
+ def future?
32
+ if all_day?
33
+ self.start.to_date > Date.today
34
+ else
35
+ self.start.date_time > Time.now
36
+ end
37
+ end
38
+
39
+ def accepted?
40
+ response_status == GCal::Event::Response::ACCEPTED
41
+ end
42
+
43
+ def declined?
44
+ response_status == GCal::Event::Response::DECLINED
45
+ end
46
+
47
+ def one_on_one?
48
+ return false if attendees.nil?
49
+ return false unless attendees.any? { |a| a.self }
50
+ return false if human_attendees.length != 2
51
+ true
52
+ end
53
+
54
+ def busy?
55
+ transparency != GCal::Event::Transparency::TRANSPARENT
56
+ end
57
+
58
+ def commitment?
59
+ return false if human_attendees.nil? || human_attendees.length < 2
60
+ return false if declined?
61
+ true
62
+ end
63
+
64
+ def private?
65
+ visibility == GCal::Event::Visibility::PRIVATE
66
+ end
67
+
68
+ def public?
69
+ visibility == GCal::Event::Visibility::PUBLIC
70
+ end
71
+
72
+ def explicit_visibility?
73
+ private? || public?
74
+ end
75
+
76
+ def start_time
77
+ if all_day?
78
+ start.to_date.beginning_of_day
79
+ else
80
+ start.date_time
81
+ end
82
+ end
83
+
84
+ def start_date
85
+ if all_day?
86
+ start.to_date
87
+ else
88
+ start.date_time.to_date
89
+ end
90
+ end
91
+
92
+ def view_summary
93
+ return "(private)" if private? && (summary.nil? || summary.blank?)
94
+ return "(no title)" if summary.nil? || summary.blank?
95
+ summary
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,50 @@
1
+ class CalendarAssistant
2
+ class EventRepository
3
+ def initialize(service, calendar_id)
4
+ @service = service
5
+ @calendar_id = calendar_id
6
+ end
7
+
8
+ def find time_range
9
+ events = @service.list_events(@calendar_id,
10
+ time_min: time_range.first.iso8601,
11
+ time_max: time_range.last.iso8601,
12
+ order_by: "startTime",
13
+ single_events: true,
14
+ max_results: 2000,
15
+ )
16
+ if events.nil? || events.items.nil?
17
+ return []
18
+ end
19
+ events.items.map { |e| CalendarAssistant::Event.new(e) }
20
+ end
21
+
22
+ def create event_attributes
23
+ event = GCal::Event.new cast_dates(event_attributes)
24
+ @service.insert_event @calendar_id, event
25
+ CalendarAssistant::Event.new(event)
26
+ end
27
+
28
+ def delete event
29
+ @service.delete_event @calendar_id, event.id
30
+ end
31
+
32
+ def update(event, attributes)
33
+ event.update! cast_dates(attributes)
34
+ @service.update_event @calendar_id, event.id, event
35
+ CalendarAssistant::Event.new(event)
36
+ end
37
+
38
+ private
39
+
40
+ def cast_dates attributes
41
+ attributes.each_with_object({}) do |(key, value), object|
42
+ if value.is_a?(Date)
43
+ object[key] = GCal::EventDateTime.new(date: value.iso8601)
44
+ else
45
+ object[key] = value
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,31 @@
1
+ #
2
+ # this file extends the Google::EventDateTime class found in the "google_calendar" rubygem
3
+ #
4
+
5
+ require "google/apis/calendar_v3"
6
+ require "time"
7
+
8
+ class Google::Apis::CalendarV3::EventDateTime
9
+ def to_date
10
+ return nil if @date.nil?
11
+ return Date.parse(@date) if @date.is_a?(String)
12
+ @date
13
+ end
14
+
15
+ def to_date!
16
+ return @date_time.to_date if @date.nil?
17
+ to_date
18
+ end
19
+
20
+ def to_s
21
+ return @date.to_s if @date
22
+ @date_time.strftime "%Y-%m-%d %H:%M"
23
+ end
24
+
25
+ def == lhs
26
+ if @date
27
+ return to_date == lhs.to_date
28
+ end
29
+ date_time == lhs.date_time
30
+ end
31
+ end
@@ -0,0 +1,71 @@
1
+ #
2
+ # this file extends the Google::Event class found in the "google_calendar" rubygem
3
+ #
4
+
5
+ require "google/apis/calendar_v3"
6
+ require "time"
7
+ require "calendar_assistant/extensions/event_date_time_extensions"
8
+
9
+ class Google::Apis::CalendarV3::Event
10
+ module RealResponse
11
+ DECLINED = "declined"
12
+ ACCEPTED = "accepted"
13
+ NEEDS_ACTION = "needsAction"
14
+ TENTATIVE = "tentative"
15
+ end
16
+
17
+ module Response
18
+ include RealResponse
19
+ SELF = "self" # not part of Google's API, but useful to represent meetings-for-myself
20
+ end
21
+
22
+ module Transparency
23
+ TRANSPARENT = "transparent"
24
+ OPAQUE = "opaque"
25
+ end
26
+
27
+ module Visibility
28
+ DEFAULT = "default"
29
+ PUBLIC = "public"
30
+ PRIVATE = "private"
31
+ end
32
+
33
+ def update **args
34
+ # this should be in the google API classes, IMHO
35
+ update!(**args)
36
+ self
37
+ end
38
+
39
+ def human_attendees
40
+ return nil if attendees.nil?
41
+ attendees.select { |a| ! a.resource }
42
+ end
43
+
44
+ def attendee id
45
+ return nil if attendees.nil?
46
+ attendees.find do |attendee|
47
+ attendee.email == id
48
+ end
49
+ end
50
+
51
+ def response_status
52
+ return Response::SELF if attendees.nil?
53
+ attendees.each do |attendee|
54
+ return attendee.response_status if attendee.self
55
+ end
56
+ nil
57
+ end
58
+
59
+ def av_uri
60
+ @av_uri ||= begin
61
+ description_link = CalendarAssistant::StringHelpers.find_uri_for_domain(description, "zoom.us")
62
+ return description_link if description_link
63
+
64
+ location_link = CalendarAssistant::StringHelpers.find_uri_for_domain(location, "zoom.us")
65
+ return location_link if location_link
66
+
67
+ return hangout_link if hangout_link
68
+ nil
69
+ end
70
+ end
71
+ end
@@ -3,6 +3,7 @@ require "uri"
3
3
  class CalendarAssistant
4
4
  module StringHelpers
5
5
  def self.find_uri_for_domain string, domain
6
+ return unless string
6
7
  URI::Parser.new.extract(string).each do |uri_string|
7
8
  uri = URI.parse uri_string
8
9
  return uri_string if uri.hostname =~ /\.#{domain}$/
@@ -1,3 +1,3 @@
1
1
  class CalendarAssistant
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: calendar-assistant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Dalessio
8
+ - Mik Freedman
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2018-10-25 00:00:00.000000000 Z
12
+ date: 2018-10-31 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: google-api-client
@@ -165,78 +166,65 @@ dependencies:
165
166
  - !ruby/object:Gem::Version
166
167
  version: '10.0'
167
168
  - !ruby/object:Gem::Dependency
168
- name: rspec
169
+ name: faker
169
170
  requirement: !ruby/object:Gem::Requirement
170
171
  requirements:
171
172
  - - "~>"
172
173
  - !ruby/object:Gem::Version
173
- version: '3.0'
174
+ version: 1.9.1
174
175
  type: :development
175
176
  prerelease: false
176
177
  version_requirements: !ruby/object:Gem::Requirement
177
178
  requirements:
178
179
  - - "~>"
179
180
  - !ruby/object:Gem::Version
180
- version: '3.0'
181
+ version: 1.9.1
181
182
  - !ruby/object:Gem::Dependency
182
- name: timecop
183
+ name: rspec
183
184
  requirement: !ruby/object:Gem::Requirement
184
185
  requirements:
185
186
  - - "~>"
186
187
  - !ruby/object:Gem::Version
187
- version: 0.9.0
188
+ version: '3.0'
188
189
  type: :development
189
190
  prerelease: false
190
191
  version_requirements: !ruby/object:Gem::Requirement
191
192
  requirements:
192
193
  - - "~>"
193
194
  - !ruby/object:Gem::Version
194
- version: 0.9.0
195
- - !ruby/object:Gem::Dependency
196
- name: autotest
197
- requirement: !ruby/object:Gem::Requirement
198
- requirements:
199
- - - ">="
200
- - !ruby/object:Gem::Version
201
- version: '0'
202
- type: :development
203
- prerelease: false
204
- version_requirements: !ruby/object:Gem::Requirement
205
- requirements:
206
- - - ">="
207
- - !ruby/object:Gem::Version
208
- version: '0'
195
+ version: '3.0'
209
196
  - !ruby/object:Gem::Dependency
210
- name: rspec-autotest
197
+ name: timecop
211
198
  requirement: !ruby/object:Gem::Requirement
212
199
  requirements:
213
- - - ">="
200
+ - - "~>"
214
201
  - !ruby/object:Gem::Version
215
- version: '0'
202
+ version: 0.9.0
216
203
  type: :development
217
204
  prerelease: false
218
205
  version_requirements: !ruby/object:Gem::Requirement
219
206
  requirements:
220
- - - ">="
207
+ - - "~>"
221
208
  - !ruby/object:Gem::Version
222
- version: '0'
209
+ version: 0.9.0
223
210
  - !ruby/object:Gem::Dependency
224
- name: test_notifier
211
+ name: license_finder
225
212
  requirement: !ruby/object:Gem::Requirement
226
213
  requirements:
227
- - - ">="
214
+ - - "~>"
228
215
  - !ruby/object:Gem::Version
229
- version: '0'
216
+ version: 5.5.0
230
217
  type: :development
231
218
  prerelease: false
232
219
  version_requirements: !ruby/object:Gem::Requirement
233
220
  requirements:
234
- - - ">="
221
+ - - "~>"
235
222
  - !ruby/object:Gem::Version
236
- version: '0'
223
+ version: 5.5.0
237
224
  description: A command-line tool to help manage your Google Calendar.
238
225
  email:
239
226
  - mike.dalessio@gmail.com
227
+ - github@michael-freedman.com
240
228
  executables:
241
229
  - calendar-assistant
242
230
  extensions: []
@@ -254,8 +242,11 @@ files:
254
242
  - lib/calendar_assistant/cli_helpers.rb
255
243
  - lib/calendar_assistant/config.rb
256
244
  - lib/calendar_assistant/config/token_store.rb
257
- - lib/calendar_assistant/event_extensions.rb
258
- - lib/calendar_assistant/rainbow_extensions.rb
245
+ - lib/calendar_assistant/event.rb
246
+ - lib/calendar_assistant/event_repository.rb
247
+ - lib/calendar_assistant/extensions/event_date_time_extensions.rb
248
+ - lib/calendar_assistant/extensions/event_extensions.rb
249
+ - lib/calendar_assistant/extensions/rainbow_extensions.rb
259
250
  - lib/calendar_assistant/string_helpers.rb
260
251
  - lib/calendar_assistant/version.rb
261
252
  homepage: https://github.com/flavorjones/calendar-assistant
@@ -1,173 +0,0 @@
1
- #
2
- # this file extends the Google::Event class found in the "google_calendar" rubygem
3
- #
4
-
5
- require "google/apis/calendar_v3"
6
- require "time"
7
-
8
- class Google::Apis::CalendarV3::Event
9
- module RealResponse
10
- DECLINED = "declined"
11
- ACCEPTED = "accepted"
12
- NEEDS_ACTION = "needsAction"
13
- TENTATIVE = "tentative"
14
- end
15
-
16
- module Response
17
- include RealResponse
18
- SELF = "self" # not part of Google's API, but useful to represent meetings-for-myself
19
- end
20
-
21
- module Transparency
22
- TRANSPARENT = "transparent"
23
- OPAQUE = "opaque"
24
- end
25
-
26
- module Visibility
27
- DEFAULT = "default"
28
- PUBLIC = "public"
29
- PRIVATE = "private"
30
- end
31
-
32
- LOCATION_EVENT_REGEX = /^#{CalendarAssistant::EMOJI_WORLDMAP}/
33
-
34
- def update **args
35
- # this should be in the google API classes, IMHO
36
- update!(**args)
37
- self
38
- end
39
-
40
- def location_event?
41
- !! (summary =~ LOCATION_EVENT_REGEX)
42
- end
43
-
44
- def all_day?
45
- !! @start.to_date
46
- end
47
-
48
- def past?
49
- if all_day?
50
- Date.today >= self.end.to_date
51
- else
52
- Time.now >= self.end.date_time
53
- end
54
- end
55
-
56
- def current?
57
- ! (past? || future?)
58
- end
59
-
60
- def future?
61
- if all_day?
62
- self.start.to_date > Date.today
63
- else
64
- self.start.date_time > Time.now
65
- end
66
- end
67
-
68
- def accepted?
69
- response_status == Response::ACCEPTED
70
- end
71
-
72
- def declined?
73
- response_status == Response::DECLINED
74
- end
75
-
76
- def one_on_one?
77
- return false if attendees.nil?
78
- return false unless attendees.any? { |a| a.self }
79
- return false if human_attendees.length != 2
80
- true
81
- end
82
-
83
- def busy?
84
- transparency != Transparency::TRANSPARENT
85
- end
86
-
87
- def commitment?
88
- return false if human_attendees.nil? || human_attendees.length < 2
89
- return false if declined?
90
- true
91
- end
92
-
93
- def private?
94
- visibility == Visibility::PRIVATE
95
- end
96
-
97
- def start_time
98
- if all_day?
99
- self.start.to_date.beginning_of_day
100
- else
101
- self.start.date_time
102
- end
103
- end
104
-
105
- def start_date
106
- if all_day?
107
- self.start.to_date
108
- else
109
- self.start.date_time.to_date
110
- end
111
- end
112
-
113
- def human_attendees
114
- return nil if attendees.nil?
115
- attendees.select { |a| ! a.resource }
116
- end
117
-
118
- def attendee id
119
- return nil if attendees.nil?
120
- attendees.find do |attendee|
121
- attendee.email == id
122
- end
123
- end
124
-
125
- def response_status
126
- return Response::SELF if attendees.nil?
127
- attendees.each do |attendee|
128
- return attendee.response_status if attendee.self
129
- end
130
- nil
131
- end
132
-
133
- def av_uri
134
- @av_uri ||= begin
135
- zoom = CalendarAssistant::StringHelpers.find_uri_for_domain(description, "zoom.us")
136
- return zoom if zoom
137
-
138
- return hangout_link if hangout_link
139
- nil
140
- end
141
- end
142
-
143
- def view_summary
144
- return "(private)" if private? && (summary.nil? || summary.blank?)
145
- return "(no title)" if summary.nil? || summary.blank?
146
- summary
147
- end
148
- end
149
-
150
- class Google::Apis::CalendarV3::EventDateTime
151
- def to_date
152
- return nil if @date.nil?
153
- return Date.parse(@date) if @date.is_a?(String)
154
- @date
155
- end
156
-
157
- def to_date!
158
- return @date_time.to_date if @date.nil?
159
- to_date
160
- end
161
-
162
- def to_s
163
- return @date.to_s if @date
164
- @date_time.strftime "%Y-%m-%d %H:%M"
165
- end
166
-
167
- def == lhs
168
- if @date
169
- return to_date == lhs.to_date
170
- end
171
- date_time == lhs.date_time
172
- end
173
- end