calendar-assistant 0.6.0 → 0.7.0

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