calendar-assistant 0.1.0 → 0.2.1

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