calendar-assistant 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,233 +1,9 @@
1
- require "calendar_assistant/cli_helpers"
2
-
3
- class CalendarAssistant
4
- class CLI < Thor
5
- def self.will_create_a_service
6
- option CalendarAssistant::Config::Keys::Settings::PROFILE,
7
- type: :string,
8
- desc: "the profile you'd like to use (if different from default)",
9
- aliases: ["-p"]
10
-
11
- option CalendarAssistant::Config::Keys::Options::LOCAL_STORE,
12
- type: :string,
13
- banner: "FILENAME",
14
- desc: "Load events from a local file instead of Google Calendar"
15
- end
16
-
17
- def self.has_attendees
18
- option CalendarAssistant::Config::Keys::Options::ATTENDEES,
19
- type: :string,
20
- banner: "ATTENDEE1[,ATTENDEE2[,...]]",
21
- desc: "[default 'me'] people (email IDs) to whom this command will be applied",
22
- aliases: ["-a"]
23
- end
24
-
25
- default_config = CalendarAssistant::Config.new options: options # used in option descriptions
26
-
27
- class_option :help,
28
- type: :boolean,
29
- aliases: ["-h", "-?"]
30
- class_option CalendarAssistant::Config::Keys::Options::DEBUG,
31
- type: :boolean,
32
- desc: "how dare you suggest there are bugs"
33
-
34
-
35
- desc "version",
36
- "Display the version of calendar-assistant"
37
- def version
38
- return if handle_help_args
39
- out.puts CalendarAssistant::VERSION
40
- end
41
-
42
-
43
- desc "config",
44
- "Dump your configuration parameters (merge of defaults and overrides from #{CalendarAssistant::Config::CONFIG_FILE_PATH})"
45
- def config
46
- return if handle_help_args
47
- settings = CalendarAssistant::Config.new.settings
48
- out.puts TOML::Generator.new({CalendarAssistant::Config::Keys::SETTINGS => settings}).body
49
- end
50
-
51
-
52
- desc "setup",
53
- "Link your local calendar-assistant installation to a Google API Client"
54
- long_desc <<~EOD
55
- This command will walk you through setting up a Google Cloud
56
- Project, enabling the Google Calendar API, and saving the
57
- credentials necessary to access the API on behalf of users.
58
-
59
- If you already have downloaded client credentials, you don't
60
- need to run this command. Instead, rename the downloaded JSON
61
- file to `#{CalendarAssistant::Authorizer::CREDENTIALS_PATH}`
62
- EOD
63
- def setup
64
- # TODO ugh see #34 for advice on how to clean this up
65
- return if handle_help_args
66
- if File.exist? CalendarAssistant::Authorizer::CREDENTIALS_PATH
67
- out.puts sprintf("Credentials already exist in %s",
68
- CalendarAssistant::Authorizer::CREDENTIALS_PATH)
69
- return
70
- end
71
-
72
- out.launch "https://developers.google.com/calendar/quickstart/ruby"
73
- sleep 1
74
- out.puts <<~EOT
75
- Please click on "ENABLE THE GOOGLE CALENDAR API" and either create a new project or select an existing project.
76
-
77
- (If you create a new project, name it something like "yourname-calendar-assistant" so you remember why it exists.)
78
-
79
- Then click "DOWNLOAD CLIENT CONFIGURATION" to download the credentials to local disk.
80
-
81
- Finally, paste the contents of the downloaded file here (it should be a complete JSON object):
82
- EOT
83
-
84
- json = out.prompt "Paste JSON here"
85
- File.open(CalendarAssistant::Authorizer::CREDENTIALS_PATH, "w") do |f|
86
- f.write json
87
- end
88
- FileUtils.chmod 0600, CalendarAssistant::Authorizer::CREDENTIALS_PATH
89
-
90
- out.puts "\nOK! Your next step is to run `calendar-assistant authorize`."
91
- end
92
-
93
-
94
- desc "authorize PROFILE_NAME",
95
- "create (or validate) a profile named NAME with calendar access"
96
- long_desc <<~EOD
97
- Create and authorize a named profile (e.g., "work", "home",
98
- "flastname@company.tld") to access your calendar.
99
-
100
- When setting up a profile, you'll be asked to visit a URL to
101
- authenticate, grant authorization, and generate and persist an
102
- access token.
103
-
104
- In order for this to work, you'll need to have set up your API client
105
- credentials. Run `calendar-assistant help setup` for instructions.
106
- EOD
107
- def authorize profile_name=nil
108
- return if handle_help_args
109
- return help! if profile_name.nil?
110
-
111
- CalendarAssistant.authorize profile_name
112
- puts "\nYou're authorized!\n\n"
113
- end
114
-
115
-
116
- desc "show [DATE | DATERANGE | TIMERANGE]",
117
- "Show your events for a date or range of dates (default 'today')"
118
- option CalendarAssistant::Config::Keys::Options::COMMITMENTS,
119
- type: :boolean,
120
- desc: "only show events that you've accepted with another person",
121
- aliases: ["-c"]
122
- will_create_a_service
123
- has_attendees
124
- def show datespec="today"
125
- return if handle_help_args
126
- config = CalendarAssistant::Config.new(options: options)
127
- ca = CalendarAssistant.new config
128
- ca.in_env do
129
- event_set = ca.find_events CLIHelpers.parse_datespec(datespec)
130
- out.print_events ca, event_set
131
- end
132
- end
133
-
134
-
135
- desc "join [TIME]",
136
- "Open the URL for a video call attached to your meeting at time TIME (default 'now')"
137
- option CalendarAssistant::Config::Keys::Options::JOIN,
138
- type: :boolean, default: true,
139
- desc: "launch a browser to join the video call URL"
140
- will_create_a_service
141
- def join timespec="now"
142
- return if handle_help_args
143
- ca = CalendarAssistant.new CalendarAssistant::Config.new(options: options)
144
- ca.in_env do
145
- event_set, url = CLIHelpers.find_av_uri ca, timespec
146
- if ! event_set.empty?
147
- out.print_events ca, event_set
148
- out.puts url
149
- out.launch url if options[CalendarAssistant::Config::Keys::Options::JOIN]
150
- else
151
- out.puts "Could not find a meeting '#{timespec}' with a video call to join."
152
- end
153
- end
154
- end
155
-
156
-
157
- desc "location [DATE | DATERANGE]",
158
- "Show your location for a date or range of dates (default 'today')"
159
- will_create_a_service
160
- def location datespec="today"
161
- return if handle_help_args
162
- ca = CalendarAssistant.new CalendarAssistant::Config.new(options: options)
163
- ca.in_env do
164
- event_set = ca.find_location_events CLIHelpers.parse_datespec(datespec)
165
- out.print_events ca, event_set
166
- end
167
- end
168
-
169
-
170
- desc "location-set LOCATION [DATE | DATERANGE]",
171
- "Set your location to LOCATION for a date or range of dates (default 'today')"
172
- will_create_a_service
173
- def location_set location=nil, datespec="today"
174
- return if handle_help_args
175
- return help! if location.nil?
176
-
177
- ca = CalendarAssistant.new CalendarAssistant::Config.new(options: options)
178
- ca.in_env do
179
- event_set = ca.create_location_event CLIHelpers.parse_datespec(datespec), location
180
- out.print_events ca, event_set
181
- end
182
- end
183
-
184
-
185
- desc "availability [DATE | DATERANGE | TIMERANGE]",
186
- "Show your availability for a date or range of dates (default 'today')"
187
- option CalendarAssistant::Config::Keys::Settings::MEETING_LENGTH,
188
- type: :string,
189
- banner: "LENGTH",
190
- desc: sprintf("[default %s] find chunks of available time at least as long as LENGTH (which is a ChronicDuration string like '30m' or '2h')",
191
- default_config.setting(CalendarAssistant::Config::Keys::Settings::MEETING_LENGTH)),
192
- aliases: ["-l"]
193
- option CalendarAssistant::Config::Keys::Settings::START_OF_DAY,
194
- type: :string,
195
- banner: "TIME",
196
- desc: sprintf("[default %s] find chunks of available time after TIME (which is a BusinessTime string like '9am' or '14:30')",
197
- default_config.setting(CalendarAssistant::Config::Keys::Settings::START_OF_DAY)),
198
- aliases: ["-s"]
199
- option CalendarAssistant::Config::Keys::Settings::END_OF_DAY,
200
- type: :string,
201
- banner: "TIME",
202
- desc: sprintf("[default %s] find chunks of available time before TIME (which is a BusinessTime string like '9am' or '14:30')",
203
- default_config.setting(CalendarAssistant::Config::Keys::Settings::END_OF_DAY)),
204
- aliases: ["-e"]
205
- has_attendees
206
- will_create_a_service
207
- def availability datespec="today"
208
- return if handle_help_args
209
- ca = CalendarAssistant.new CalendarAssistant::Config.new(options: options)
210
- ca.in_env do
211
- event_set = ca.availability CLIHelpers.parse_datespec(datespec)
212
- out.print_available_blocks ca, event_set
213
- end
214
- end
215
-
216
- private
217
-
218
- def out
219
- @out ||= CLIHelpers::Out.new
220
- end
221
-
222
- def help!
223
- help(current_command_chain.first)
224
- end
225
-
226
- def handle_help_args
227
- if options[:help]
228
- help!
229
- return true
230
- end
231
- end
232
- end
233
- end
1
+ require_relative 'cli/config'
2
+ require_relative 'cli/helpers'
3
+ require_relative 'cli/printer'
4
+ require_relative 'cli/event_presenter'
5
+ require_relative 'cli/event_set_presenter'
6
+ require_relative 'cli/linter_event_presenter'
7
+ require_relative 'cli/linter_event_set_presenter'
8
+ require_relative 'cli/authorizer'
9
+ require_relative 'cli/commands'
@@ -0,0 +1,88 @@
1
+ #
2
+ # code in this file is inspired by
3
+ #
4
+ # https://github.com/gsuitedevs/ruby-samples/blob/master/calendar/quickstart/quickstart.rb
5
+ #
6
+ # Copyright 2018 Google LLC
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # https://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+
20
+ class CalendarAssistant
21
+ module CLI
22
+ class Authorizer
23
+ class NoCredentials < CalendarAssistant::BaseException ; end
24
+ class UnauthorizedError < CalendarAssistant::BaseException ; end
25
+
26
+ OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'.freeze
27
+ APPLICATION_NAME = "Flavorjones Calendar Assistant".freeze
28
+ CREDENTIALS_PATH = File.join (ENV['CA_HOME'] || ENV["HOME"]), ".calendar-assistant.client"
29
+ SCOPE = Google::Apis::CalendarV3::AUTH_CALENDAR
30
+
31
+ attr_reader :profile_name, :config_token_store
32
+
33
+ def initialize profile_name, config_token_store
34
+ @profile_name = profile_name
35
+ @config_token_store = config_token_store
36
+ end
37
+
38
+ def authorize
39
+ credentials || prompt_user_for_authorization
40
+ end
41
+
42
+ def service
43
+ if credentials.nil?
44
+ raise UnauthorizedError, "Not authorized. Please run `calendar-assistant authorize #{profile_name}`"
45
+ end
46
+
47
+ Google::Apis::CalendarV3::CalendarService.new.tap do |service|
48
+ service.client_options.application_name = APPLICATION_NAME
49
+ service.authorization = credentials
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def credentials
56
+ @credentials ||= authorizer.get_credentials profile_name
57
+ end
58
+
59
+ def prompt_user_for_authorization
60
+ url = authorizer.get_authorization_url(base_url: OOB_URI)
61
+
62
+ puts Rainbow("Please open this URL in your browser:").bold
63
+ puts
64
+ puts " " + url
65
+ puts
66
+
67
+ puts Rainbow("Then authorize '#{APPLICATION_NAME}' to manage your calendar and copy/paste the resulting code here:").bold
68
+ puts
69
+ print "> "
70
+ code = STDIN.gets
71
+
72
+ authorizer.get_and_store_credentials_from_code(user_id: profile_name, code: code, base_url: OOB_URI)
73
+ end
74
+
75
+ def authorizer
76
+ @authorizer ||= begin
77
+ if ! File.exists?(CREDENTIALS_PATH)
78
+ raise NoCredentials, "No credentials found. Please run `calendar-assistant help setup` for instructions"
79
+ end
80
+
81
+ FileUtils.chmod 0600, CREDENTIALS_PATH
82
+ client_id = Google::Auth::ClientId.from_file(CREDENTIALS_PATH)
83
+ Google::Auth::UserAuthorizer.new(client_id, SCOPE, config_token_store)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,284 @@
1
+ require "calendar_assistant/cli/helpers"
2
+
3
+ class CalendarAssistant
4
+ module CLI
5
+ class Commands < Thor
6
+ def self.will_create_a_service
7
+ option CalendarAssistant::Config::Keys::Settings::PROFILE,
8
+ type: :string,
9
+ desc: "the profile you'd like to use (if different from default)",
10
+ aliases: ["-p"]
11
+
12
+ option CalendarAssistant::Config::Keys::Options::LOCAL_STORE,
13
+ type: :string,
14
+ banner: "FILENAME",
15
+ desc: "Load events from a local file instead of Google Calendar"
16
+ end
17
+
18
+ def self.has_attendees
19
+ option CalendarAssistant::Config::Keys::Options::ATTENDEES,
20
+ type: :string,
21
+ banner: "ATTENDEE1[,ATTENDEE2[,...]]",
22
+ desc: "[default 'me'] people (email IDs) to whom this command will be applied",
23
+ aliases: ["-a"]
24
+ end
25
+
26
+ default_config = CalendarAssistant::CLI::Config.new options: options # used in option descriptions
27
+
28
+ class_option :help,
29
+ type: :boolean,
30
+ aliases: ["-h", "-?"]
31
+ class_option CalendarAssistant::Config::Keys::Options::DEBUG,
32
+ type: :boolean,
33
+ desc: "how dare you suggest there are bugs"
34
+
35
+ class_option CalendarAssistant::Config::Keys::Options::FORMATTING,
36
+ type: :boolean,
37
+ desc: "Enable Text Formatting",
38
+ default: CalendarAssistant::Config::DEFAULT_SETTINGS[CalendarAssistant::Config::Keys::Options::FORMATTING]
39
+
40
+ desc "version",
41
+ "Display the version of calendar-assistant"
42
+
43
+ def version
44
+ return if handle_help_args
45
+ out.puts CalendarAssistant::VERSION
46
+ end
47
+
48
+
49
+ desc "config",
50
+ "Dump your configuration parameters (merge of defaults and overrides from #{CalendarAssistant::CLI::Config::CONFIG_FILE_PATH})"
51
+
52
+ def config
53
+ return if handle_help_args
54
+ settings = CalendarAssistant::CLI::Config.new.settings
55
+ out.puts TOML::Generator.new({CalendarAssistant::Config::Keys::SETTINGS => settings}).body
56
+ end
57
+
58
+
59
+ desc "setup",
60
+ "Link your local calendar-assistant installation to a Google API Client"
61
+ long_desc <<~EOD
62
+ This command will walk you through setting up a Google Cloud
63
+ Project, enabling the Google Calendar API, and saving the
64
+ credentials necessary to access the API on behalf of users.
65
+
66
+ If you already have downloaded client credentials, you don't
67
+ need to run this command. Instead, rename the downloaded JSON
68
+ file to `#{CalendarAssistant::CLI::Authorizer::CREDENTIALS_PATH}`
69
+ EOD
70
+
71
+ def setup
72
+ # TODO ugh see #34 for advice on how to clean this up
73
+ return if handle_help_args
74
+ if File.exist? CalendarAssistant::CLI::Authorizer::CREDENTIALS_PATH
75
+ out.puts sprintf("Credentials already exist in %s",
76
+ CalendarAssistant::CLI::Authorizer::CREDENTIALS_PATH)
77
+ return
78
+ end
79
+
80
+ out.launch "https://developers.google.com/calendar/quickstart/ruby"
81
+ sleep 1
82
+ out.puts <<~EOT
83
+ Please click on "ENABLE THE GOOGLE CALENDAR API" and either create a new project or select an existing project.
84
+
85
+ (If you create a new project, name it something like "yourname-calendar-assistant" so you remember why it exists.)
86
+
87
+ Then click "DOWNLOAD CLIENT CONFIGURATION" to download the credentials to local disk.
88
+
89
+ Finally, paste the contents of the downloaded file here (it should be a complete JSON object):
90
+ EOT
91
+
92
+ json = out.prompt "Paste JSON here"
93
+ File.open(CalendarAssistant::CLI::Authorizer::CREDENTIALS_PATH, "w") do |f|
94
+ f.write json
95
+ end
96
+ FileUtils.chmod 0600, CalendarAssistant::CLI::Authorizer::CREDENTIALS_PATH
97
+
98
+ out.puts "\nOK! Your next step is to run `calendar-assistant authorize`."
99
+ end
100
+
101
+
102
+ desc "authorize PROFILE_NAME",
103
+ "create (or validate) a profile named NAME with calendar access"
104
+ long_desc <<~EOD
105
+ Create and authorize a named profile (e.g., "work", "home",
106
+ "flastname@company.tld") to access your calendar.
107
+
108
+ When setting up a profile, you'll be asked to visit a URL to
109
+ authenticate, grant authorization, and generate and persist an
110
+ access token.
111
+
112
+ In order for this to work, you'll need to have set up your API client
113
+ credentials. Run `calendar-assistant help setup` for instructions.
114
+ EOD
115
+
116
+ def authorize profile_name = nil
117
+ return if handle_help_args
118
+ return help! if profile_name.nil?
119
+
120
+ get_authorizer(profile_name: profile_name).authorize
121
+
122
+ puts "\nYou're authorized!\n\n"
123
+ end
124
+
125
+ desc "lint [DATE | DATERANGE | TIMERANGE]",
126
+ "Lint your events for a date or range of dates (default 'today')"
127
+ will_create_a_service
128
+ has_attendees
129
+
130
+ def lint datespec = "today"
131
+ calendar_assistant(datespec) do |ca, date|
132
+ event_set = ca.lint_events date
133
+ out.print_events ca, event_set, presenter_class: CalendarAssistant::CLI::LinterEventSetPresenter
134
+ end
135
+ end
136
+
137
+ desc "show [DATE | DATERANGE | TIMERANGE]",
138
+ "Show your events for a date or range of dates (default 'today')"
139
+ option CalendarAssistant::Config::Keys::Options::COMMITMENTS,
140
+ type: :boolean,
141
+ desc: "only show events that you've accepted with another person",
142
+ aliases: ["-c"]
143
+ will_create_a_service
144
+ has_attendees
145
+
146
+ def show datespec = "today"
147
+ calendar_assistant(datespec) do |ca, date|
148
+ event_set = ca.find_events date
149
+ out.print_events ca, event_set
150
+ end
151
+ end
152
+
153
+
154
+ desc "join [TIME]",
155
+ "Open the URL for a video call attached to your meeting at time TIME (default 'now')"
156
+ option CalendarAssistant::Config::Keys::Options::JOIN,
157
+ type: :boolean, default: true,
158
+ desc: "launch a browser to join the video call URL"
159
+ will_create_a_service
160
+
161
+ def join timespec = "now"
162
+ return if handle_help_args
163
+ set_formatting
164
+ ca = CalendarAssistant.new get_config, service: service
165
+ ca.in_env do
166
+ event_set, url = CalendarAssistant::CLI::Helpers.find_av_uri ca, timespec
167
+ if !event_set.empty?
168
+ out.print_events ca, event_set
169
+ out.puts url
170
+ out.launch url if options[CalendarAssistant::Config::Keys::Options::JOIN]
171
+ else
172
+ out.puts "Could not find a meeting '#{timespec}' with a video call to join."
173
+ end
174
+ end
175
+ end
176
+
177
+
178
+ desc "location [DATE | DATERANGE]",
179
+ "Show your location for a date or range of dates (default 'today')"
180
+ will_create_a_service
181
+
182
+ def location datespec = "today"
183
+ calendar_assistant(datespec) do |ca, date|
184
+ event_set = ca.find_location_events date
185
+ out.print_events ca, event_set
186
+ end
187
+ end
188
+
189
+
190
+ desc "location-set LOCATION [DATE | DATERANGE]",
191
+ "Set your location to LOCATION for a date or range of dates (default 'today')"
192
+ will_create_a_service
193
+
194
+ def location_set location = nil, datespec = "today"
195
+ return help! if location.nil?
196
+
197
+ calendar_assistant(datespec) do |ca, date|
198
+ event_set = ca.create_location_event date, location
199
+ out.print_events ca, event_set
200
+ end
201
+ end
202
+
203
+
204
+ desc "availability [DATE | DATERANGE | TIMERANGE]",
205
+ "Show your availability for a date or range of dates (default 'today')"
206
+ option CalendarAssistant::Config::Keys::Settings::MEETING_LENGTH,
207
+ type: :string,
208
+ banner: "LENGTH",
209
+ desc: sprintf("[default %s] find chunks of available time at least as long as LENGTH (which is a ChronicDuration string like '30m' or '2h')",
210
+ default_config.setting(CalendarAssistant::Config::Keys::Settings::MEETING_LENGTH)),
211
+ aliases: ["-l"]
212
+ option CalendarAssistant::Config::Keys::Settings::START_OF_DAY,
213
+ type: :string,
214
+ banner: "TIME",
215
+ desc: sprintf("[default %s] find chunks of available time after TIME (which is a BusinessTime string like '9am' or '14:30')",
216
+ default_config.setting(CalendarAssistant::Config::Keys::Settings::START_OF_DAY)),
217
+ aliases: ["-s"]
218
+ option CalendarAssistant::Config::Keys::Settings::END_OF_DAY,
219
+ type: :string,
220
+ banner: "TIME",
221
+ desc: sprintf("[default %s] find chunks of available time before TIME (which is a BusinessTime string like '9am' or '14:30')",
222
+ default_config.setting(CalendarAssistant::Config::Keys::Settings::END_OF_DAY)),
223
+ aliases: ["-e"]
224
+ has_attendees
225
+ will_create_a_service
226
+
227
+ def availability datespec = "today"
228
+ calendar_assistant(datespec) do |ca, date|
229
+ event_set = ca.availability date
230
+ out.print_available_blocks ca, event_set
231
+ end
232
+ end
233
+
234
+ private
235
+
236
+ def set_formatting
237
+ Rainbow.enabled = !!options[:formatting]
238
+ end
239
+
240
+ def service
241
+ @service ||= begin
242
+ if filename = get_config.setting(Config::Keys::Options::LOCAL_STORE)
243
+ CalendarAssistant::LocalService.new(file: filename)
244
+ else
245
+ get_authorizer.service
246
+ end
247
+ end
248
+ end
249
+
250
+ def get_authorizer(profile_name: get_config.profile_name, token_store: get_config.token_store)
251
+ @authorizer ||= {}
252
+ @authorizer[profile_name] ||= Authorizer.new(profile_name, token_store)
253
+ end
254
+
255
+ def calendar_assistant datespec = "today"
256
+ return if handle_help_args
257
+ set_formatting
258
+ ca = CalendarAssistant.new(get_config, service: service)
259
+ ca.in_env do
260
+ yield(ca, CalendarAssistant::CLI::Helpers.parse_datespec(datespec))
261
+ end
262
+ end
263
+
264
+ def get_config
265
+ @config ||= CalendarAssistant::CLI::Config.new(options: options)
266
+ end
267
+
268
+ def out
269
+ @out ||= CalendarAssistant::CLI::Printer.new
270
+ end
271
+
272
+ def help!
273
+ help(current_command_chain.first)
274
+ end
275
+
276
+ def handle_help_args
277
+ if options[:help]
278
+ help!
279
+ return true
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end