flycal-cli 1.0 → 1.2
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 +4 -4
- data/config/defaults.yml +3 -28
- data/config/oauth_client.json +14 -0
- data/lib/flycal/cli/app.rb +112 -143
- data/lib/flycal/cli/auth.rb +113 -42
- data/lib/flycal/cli/config.rb +24 -1
- data/lib/flycal/client.rb +73 -98
- data/lib/flycal/date_time_parser.rb +11 -10
- data/lib/flycal/default_slots.rb +77 -0
- data/lib/flycal/mock/event_generator.rb +6 -4
- data/lib/flycal/mock/slot_scenario.rb +78 -0
- data/lib/flycal/pipeline/json_renderer.rb +1 -5
- data/lib/flycal/pipeline/text_renderer.rb +5 -4
- data/lib/flycal/slot_finder.rb +16 -16
- data/lib/flycal/slot_formatter.rb +8 -4
- data/lib/flycal/slot_query.rb +80 -0
- data/lib/flycal/slot_templates.rb +129 -0
- data/lib/flycal/time_format.rb +114 -0
- data/lib/flycal.rb +1 -1
- data/lib/flycal_cli.rb +5 -0
- data/locales/en.json +18 -0
- data/locales/it.json +18 -0
- data/mcp/tools.json +15 -3
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 55605292daf7d56f71dc210b4c607ebceeb0aa1be1bc464ba73d82ce26583c4a
|
|
4
|
+
data.tar.gz: 256c9dd132265a9d7b36ac7a144c516b06d5c298f9a8e8fb8ac29e1fe2a7bc53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7a93a60e2cfd438b9b9ef9b52bf1c1a06e07c329bf3e86d3e8ef2253a5bf96c8bbbb390c5258b5250d71e0f87d6669b1088c1b7d10a3bebac0ad9809dc4f638
|
|
7
|
+
data.tar.gz: 5fc7e9f8a1d202e84caf771498a5758023a8aa16a240e6490108eb31d73507889857ca40a2db00309a78f7c15de34f16db1a334037bc44cd684531bac8d73053
|
data/config/defaults.yml
CHANGED
|
@@ -1,30 +1,5 @@
|
|
|
1
1
|
calendar_default: ~
|
|
2
2
|
locale: en
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
days:
|
|
7
|
-
- 1
|
|
8
|
-
- 2
|
|
9
|
-
- 3
|
|
10
|
-
- 4
|
|
11
|
-
- 5
|
|
12
|
-
hours:
|
|
13
|
-
- 9:30-13:00
|
|
14
|
-
- 14:00-18:30
|
|
15
|
-
dinner:
|
|
16
|
-
days:
|
|
17
|
-
- 1
|
|
18
|
-
- 2
|
|
19
|
-
- 3
|
|
20
|
-
- 4
|
|
21
|
-
- 5
|
|
22
|
-
- 6
|
|
23
|
-
hours:
|
|
24
|
-
- 19-23
|
|
25
|
-
exclude_calendars: []
|
|
26
|
-
defaults:
|
|
27
|
-
from: now
|
|
28
|
-
default_duration: 45min
|
|
29
|
-
free_before: 0m
|
|
30
|
-
free_after: 15m
|
|
3
|
+
timezone: Europe/Rome
|
|
4
|
+
# `slots` is injected from Flycal::DefaultSlots at load/setup time.
|
|
5
|
+
# After first write, the CLI reads templates from this file on disk.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"installed": {
|
|
3
|
+
"client_id": "854938129342-tnf0miouj03b4vgi17aol1j8aann25mn.apps.googleusercontent.com",
|
|
4
|
+
"project_id": "flycal-387320",
|
|
5
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
6
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
7
|
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
8
|
+
"client_secret": "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",
|
|
9
|
+
"redirect_uris": [
|
|
10
|
+
"http://localhost",
|
|
11
|
+
"http://127.0.0.1:9292/oauth2callback"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
data/lib/flycal/cli/app.rb
CHANGED
|
@@ -22,29 +22,23 @@ module Cli
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
desc "login", "Connect to your Google account"
|
|
25
|
+
option :client, type: :string,
|
|
26
|
+
desc: "OAuth client: flycal (Flycal Desktop app) or json (~/.flycal/credentials.json)"
|
|
25
27
|
def login
|
|
26
28
|
apply_locale_override
|
|
27
29
|
if Auth.logged_in?
|
|
28
|
-
puts "
|
|
29
|
-
puts "\
|
|
30
|
+
puts Locale.t("login.already_connected")
|
|
31
|
+
puts "\n#{Locale.t('login.next_config')}"
|
|
30
32
|
return
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
puts "\nTo configure flycal:"
|
|
36
|
-
puts "1. Go to https://console.cloud.google.com/apis/credentials"
|
|
37
|
-
puts "2. Create 'Desktop app' credentials"
|
|
38
|
-
puts "3. Download the JSON and save it as: #{Config.credentials_path}"
|
|
39
|
-
puts "\nAlso add this URI as an authorized redirect:"
|
|
40
|
-
puts " http://127.0.0.1:9292/oauth2callback"
|
|
41
|
-
return
|
|
42
|
-
end
|
|
35
|
+
mode = resolve_login_mode
|
|
36
|
+
return unless mode
|
|
43
37
|
|
|
44
38
|
begin
|
|
45
|
-
Auth.login
|
|
46
|
-
puts "\n
|
|
47
|
-
puts "\
|
|
39
|
+
Auth.login(mode)
|
|
40
|
+
puts "\n#{Locale.t('login.success')}"
|
|
41
|
+
puts "\n#{Locale.t('login.next_config')}"
|
|
48
42
|
rescue Flycal::Error => e
|
|
49
43
|
puts "Error: #{e.message}"
|
|
50
44
|
exit 1
|
|
@@ -332,16 +326,28 @@ module Cli
|
|
|
332
326
|
from_value = options[:from].to_s.strip
|
|
333
327
|
from_value = defaults["from"].to_s.strip if from_value.empty?
|
|
334
328
|
from_value = "now" if from_value.empty?
|
|
329
|
+
timezone = Config.timezone
|
|
330
|
+
template_name = nil
|
|
331
|
+
hours = nil
|
|
332
|
+
days = nil
|
|
333
|
+
slot_duration = nil
|
|
334
|
+
free_before = nil
|
|
335
|
+
free_after = nil
|
|
336
|
+
time_min = nil
|
|
337
|
+
time_max = nil
|
|
335
338
|
|
|
336
339
|
begin
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
340
|
+
TimeFormat.with_zone(timezone) do
|
|
341
|
+
resolved = SlotTemplates.resolve(name: options[:template], catalog: slot_cfg["templates"])
|
|
342
|
+
template_name = resolved[:name]
|
|
343
|
+
hours = resolved[:hours]
|
|
344
|
+
days = resolved[:days]
|
|
345
|
+
slot_duration = DurationParser.to_seconds(duration_value)
|
|
346
|
+
free_before = DurationParser.to_seconds(slot_cfg["free_before"] || "0m")
|
|
347
|
+
free_after = DurationParser.to_seconds(slot_cfg["free_after"] || "0m")
|
|
348
|
+
time_min = parse_slots_from(from_value)
|
|
349
|
+
time_max = DurationParser.add_to_time(in_value, time_min)
|
|
350
|
+
end
|
|
345
351
|
rescue Flycal::Error => e
|
|
346
352
|
puts "Error: #{e.message}"
|
|
347
353
|
exit 1
|
|
@@ -379,6 +385,7 @@ module Cli
|
|
|
379
385
|
free_after: slot_cfg["free_after"] || "0m",
|
|
380
386
|
template: template_name,
|
|
381
387
|
locale: Locale.current_locale,
|
|
388
|
+
timezone: timezone,
|
|
382
389
|
calendar: options[:calendar],
|
|
383
390
|
hours: hours,
|
|
384
391
|
days: days,
|
|
@@ -403,55 +410,57 @@ module Cli
|
|
|
403
410
|
if output.nil?
|
|
404
411
|
FileCache.delete(cache_key) if nocache
|
|
405
412
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
fetch_from = time_min - free_before
|
|
414
|
-
events = exclude_calendar_ids.flat_map do |calendar_id|
|
|
415
|
-
client.list_events(
|
|
416
|
-
calendar_id,
|
|
417
|
-
time_min: fetch_from,
|
|
418
|
-
time_max: time_max
|
|
419
|
-
)
|
|
420
|
-
rescue Google::Apis::Error => e
|
|
421
|
-
warn Locale.t("errors.calendar_fetch", calendar: calendar_id, message: e.message)
|
|
422
|
-
[]
|
|
423
|
-
end
|
|
413
|
+
TimeFormat.with_zone(timezone) do
|
|
414
|
+
calendars = client.list_calendars
|
|
415
|
+
calendar_meta = exclude_calendar_ids.filter_map do |id|
|
|
416
|
+
cal = calendars.find { |c| c.id == id }
|
|
417
|
+
name = cal&.summary || id
|
|
418
|
+
{ id: id, name: name }
|
|
419
|
+
end
|
|
424
420
|
|
|
425
|
-
|
|
426
|
-
events
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
421
|
+
fetch_from = time_min - free_before
|
|
422
|
+
events = exclude_calendar_ids.flat_map do |calendar_id|
|
|
423
|
+
client.list_events(
|
|
424
|
+
calendar_id,
|
|
425
|
+
time_min: fetch_from,
|
|
426
|
+
time_max: time_max
|
|
427
|
+
)
|
|
428
|
+
rescue Google::Apis::Error => e
|
|
429
|
+
warn Locale.t("errors.calendar_fetch", calendar: calendar_id, message: e.message)
|
|
430
|
+
[]
|
|
431
|
+
end
|
|
435
432
|
|
|
436
|
-
|
|
433
|
+
finder = SlotFinder.new(
|
|
434
|
+
events: events,
|
|
435
|
+
time_min: time_min,
|
|
436
|
+
time_max: time_max,
|
|
437
|
+
slot_duration_seconds: slot_duration,
|
|
438
|
+
hours: hours,
|
|
439
|
+
days: days,
|
|
440
|
+
free_before_seconds: free_before,
|
|
441
|
+
free_after_seconds: free_after
|
|
442
|
+
)
|
|
437
443
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
444
|
+
slots_by_day = finder.slots_by_day
|
|
445
|
+
|
|
446
|
+
output =
|
|
447
|
+
if format == "json"
|
|
448
|
+
SlotFormatter.format_json(
|
|
449
|
+
slots_by_day: slots_by_day,
|
|
450
|
+
time_min: time_min,
|
|
451
|
+
time_max: time_max,
|
|
452
|
+
duration: duration_value,
|
|
453
|
+
template: template_name,
|
|
454
|
+
calendars: calendar_meta,
|
|
455
|
+
locale: Locale.current_locale,
|
|
456
|
+
timezone: timezone,
|
|
457
|
+
calendar_option: options[:calendar],
|
|
458
|
+
from_option: options[:from],
|
|
459
|
+
in_option: options[:in],
|
|
460
|
+
free_before: slot_cfg["free_before"],
|
|
461
|
+
free_after: slot_cfg["free_after"],
|
|
462
|
+
empty_message: Locale.t("slots.no_available")
|
|
463
|
+
)
|
|
455
464
|
else
|
|
456
465
|
SlotFormatter.format_text(
|
|
457
466
|
slots_by_day: slots_by_day,
|
|
@@ -464,8 +473,9 @@ module Cli
|
|
|
464
473
|
)
|
|
465
474
|
end
|
|
466
475
|
|
|
467
|
-
|
|
468
|
-
|
|
476
|
+
FileCache.write(cache_key, strip_cache_marker_from_output(output, format))
|
|
477
|
+
status = "MISS"
|
|
478
|
+
end
|
|
469
479
|
end
|
|
470
480
|
|
|
471
481
|
print annotate_cli_output(output, status, cache_key, format)
|
|
@@ -541,6 +551,37 @@ module Cli
|
|
|
541
551
|
lines[(blank_idx + 1)..].join("\n").strip
|
|
542
552
|
end
|
|
543
553
|
|
|
554
|
+
def resolve_login_mode
|
|
555
|
+
requested = options[:client].to_s.strip.downcase
|
|
556
|
+
unless requested.empty?
|
|
557
|
+
unless [ Auth::CLIENT_FLYCAL, Auth::CLIENT_JSON ].include?(requested)
|
|
558
|
+
puts Locale.t("login.unknown_client", client: options[:client])
|
|
559
|
+
exit 1
|
|
560
|
+
end
|
|
561
|
+
return requested
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
return Auth::CLIENT_FLYCAL unless $stdin.tty?
|
|
565
|
+
|
|
566
|
+
begin
|
|
567
|
+
prompt = TTY::Prompt.new
|
|
568
|
+
choices = {
|
|
569
|
+
Locale.t("login.options.flycal") => Auth::CLIENT_FLYCAL,
|
|
570
|
+
Locale.t("login.options.json") => Auth::CLIENT_JSON
|
|
571
|
+
}
|
|
572
|
+
default_label =
|
|
573
|
+
if Config.auth_client == Auth::CLIENT_JSON
|
|
574
|
+
Locale.t("login.options.json")
|
|
575
|
+
else
|
|
576
|
+
Locale.t("login.options.flycal")
|
|
577
|
+
end
|
|
578
|
+
prompt.select(Locale.t("login.prompt"), choices, default: default_label, per_page: 5)
|
|
579
|
+
rescue Interrupt
|
|
580
|
+
puts "\n#{Locale.t('login.cancelled')}"
|
|
581
|
+
exit 0
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
544
585
|
def with_config_interrupt_handling
|
|
545
586
|
yield
|
|
546
587
|
rescue Interrupt
|
|
@@ -681,37 +722,9 @@ module Cli
|
|
|
681
722
|
end
|
|
682
723
|
|
|
683
724
|
def parse_slots_from(value)
|
|
684
|
-
return Time.now if value.to_s.strip.downcase == "now"
|
|
685
|
-
|
|
686
725
|
DateTimeParser.parse(value)
|
|
687
726
|
end
|
|
688
727
|
|
|
689
|
-
def resolve_slots_template(slot_cfg, requested_name)
|
|
690
|
-
templates = slot_cfg["templates"]
|
|
691
|
-
raise Flycal::Error, "slots.templates is missing in config.yml." unless templates.is_a?(Hash) && !templates.empty?
|
|
692
|
-
|
|
693
|
-
name = requested_name.to_s.strip
|
|
694
|
-
name = templates.keys.first.to_s if name.empty?
|
|
695
|
-
|
|
696
|
-
template = templates[name]
|
|
697
|
-
unless template.is_a?(Hash)
|
|
698
|
-
available = templates.keys.join(", ")
|
|
699
|
-
raise Flycal::Error, "Unknown slots template #{name.inspect}. Available: #{available}"
|
|
700
|
-
end
|
|
701
|
-
|
|
702
|
-
[name, template]
|
|
703
|
-
end
|
|
704
|
-
|
|
705
|
-
def parse_template_days(values)
|
|
706
|
-
days = Array(values).map(&:to_i)
|
|
707
|
-
raise Flycal::Error, "slots template days cannot be empty." if days.empty?
|
|
708
|
-
|
|
709
|
-
invalid = days.reject { |d| d.between?(1, 7) }
|
|
710
|
-
raise Flycal::Error, "Invalid template days #{invalid.inspect}. Use 1 (Mon) .. 7 (Sun)." unless invalid.empty?
|
|
711
|
-
|
|
712
|
-
days.uniq
|
|
713
|
-
end
|
|
714
|
-
|
|
715
728
|
def copy_slots_to_clipboard(text)
|
|
716
729
|
tool = Clipboard.copy(SlotFormatter.strip_ansi(text))
|
|
717
730
|
return unless tool
|
|
@@ -720,50 +733,6 @@ module Cli
|
|
|
720
733
|
puts Locale.t("slots.copied_to_clipboard", tool: tool)
|
|
721
734
|
end
|
|
722
735
|
|
|
723
|
-
def parse_hour_minute(value)
|
|
724
|
-
match = value.to_s.strip.match(/\A(\d{1,2}):(\d{2})\z/)
|
|
725
|
-
raise Flycal::Error, "Invalid time format #{value.inspect}. Use HH:MM (e.g. 9:00, 18:30)." unless match
|
|
726
|
-
|
|
727
|
-
hour = match[1].to_i
|
|
728
|
-
minute = match[2].to_i
|
|
729
|
-
unless hour.between?(0, 23) && minute.between?(0, 59)
|
|
730
|
-
raise Flycal::Error, "Invalid time #{value.inspect}. Hour must be 0-23 and minute 0-59."
|
|
731
|
-
end
|
|
732
|
-
|
|
733
|
-
[hour, minute]
|
|
734
|
-
end
|
|
735
|
-
|
|
736
|
-
def parse_workhours(values)
|
|
737
|
-
ranges = Array(values).map do |item|
|
|
738
|
-
start_str, end_str = item.to_s.strip.split("-", 2)
|
|
739
|
-
raise Flycal::Error, "Invalid hours item #{item.inspect}. Use format like '9-13' or '14:30-18:00'." if start_str.nil? || end_str.nil?
|
|
740
|
-
|
|
741
|
-
sh, sm = parse_hour_minute_flexible(start_str)
|
|
742
|
-
eh, em = parse_hour_minute_flexible(end_str)
|
|
743
|
-
start_minutes = (sh * 60) + sm
|
|
744
|
-
end_minutes = (eh * 60) + em
|
|
745
|
-
raise Flycal::Error, "Invalid hours range #{item.inspect}: end must be after start." if end_minutes <= start_minutes
|
|
746
|
-
|
|
747
|
-
[sh, sm, eh, em]
|
|
748
|
-
end
|
|
749
|
-
|
|
750
|
-
raise Flycal::Error, "template hours cannot be empty in config.yml." if ranges.empty?
|
|
751
|
-
|
|
752
|
-
ranges.sort_by { |sh, sm, _eh, _em| (sh * 60) + sm }
|
|
753
|
-
end
|
|
754
|
-
|
|
755
|
-
def parse_hour_minute_flexible(value)
|
|
756
|
-
str = value.to_s.strip
|
|
757
|
-
if str.match?(/\A\d{1,2}\z/)
|
|
758
|
-
hour = str.to_i
|
|
759
|
-
raise Flycal::Error, "Invalid hour #{value.inspect}. Must be 0-23." unless hour.between?(0, 23)
|
|
760
|
-
|
|
761
|
-
return [hour, 0]
|
|
762
|
-
end
|
|
763
|
-
|
|
764
|
-
parse_hour_minute(str)
|
|
765
|
-
end
|
|
766
|
-
|
|
767
736
|
def resolve_calendar_ids(service, calendar_option)
|
|
768
737
|
calendar_lists = service.list_calendars
|
|
769
738
|
refs = Flycal::CalendarQuery.refs(calendar_option)
|
data/lib/flycal/cli/auth.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "webrick"
|
|
4
3
|
require "googleauth"
|
|
5
4
|
require "googleauth/stores/file_token_store"
|
|
6
5
|
require "fileutils"
|
|
6
|
+
require "rbconfig"
|
|
7
7
|
|
|
8
8
|
module Flycal
|
|
9
9
|
module Cli
|
|
@@ -11,23 +11,19 @@ module Cli
|
|
|
11
11
|
SCOPE = "https://www.googleapis.com/auth/calendar.readonly"
|
|
12
12
|
REDIRECT_PORT = 9292
|
|
13
13
|
REDIRECT_URI = "http://127.0.0.1:#{REDIRECT_PORT}/oauth2callback"
|
|
14
|
+
CLIENT_FLYCAL = "flycal"
|
|
15
|
+
CLIENT_JSON = "json"
|
|
16
|
+
USER_ID = "flycal_user"
|
|
17
|
+
OAUTH_CLIENT_FILE = File.expand_path("../../../config/oauth_client.json", __dir__)
|
|
14
18
|
|
|
15
19
|
class << self
|
|
16
20
|
def credentials
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
client_id = Google::Auth::ClientId.from_file(Config.credentials_path)
|
|
21
|
-
authorizer = Google::Auth::UserAuthorizer.new(
|
|
22
|
-
client_id,
|
|
23
|
-
SCOPE,
|
|
24
|
-
token_store,
|
|
25
|
-
"/oauth2callback"
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
creds = authorizer.get_credentials("flycal_user")
|
|
29
|
-
return creds if creds
|
|
21
|
+
mode = resolved_mode
|
|
22
|
+
return nil unless mode
|
|
23
|
+
return nil unless File.exist?(Config.tokens_path)
|
|
30
24
|
|
|
25
|
+
authorizer_for(mode).get_credentials(USER_ID)
|
|
26
|
+
rescue Signet::AuthorizationError, Flycal::Error
|
|
31
27
|
nil
|
|
32
28
|
end
|
|
33
29
|
|
|
@@ -41,52 +37,109 @@ module Cli
|
|
|
41
37
|
false
|
|
42
38
|
end
|
|
43
39
|
|
|
44
|
-
def login
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"Go to: https://console.cloud.google.com/apis/credentials\n" \
|
|
49
|
-
"Create 'Desktop app' credentials and download the JSON as credentials.json"
|
|
40
|
+
def login(mode)
|
|
41
|
+
mode = normalize_mode(mode)
|
|
42
|
+
if Config.auth_client && Config.auth_client != mode && File.exist?(Config.tokens_path)
|
|
43
|
+
logout
|
|
50
44
|
end
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
authorizer = Google::Auth::UserAuthorizer.new(
|
|
55
|
-
client_id,
|
|
56
|
-
SCOPE,
|
|
57
|
-
token_store,
|
|
58
|
-
"/oauth2callback"
|
|
59
|
-
)
|
|
46
|
+
Config.auth_client = mode
|
|
47
|
+
authorizer = authorizer_for(mode)
|
|
60
48
|
|
|
61
|
-
creds = authorizer.get_credentials(
|
|
49
|
+
creds = authorizer.get_credentials(USER_ID)
|
|
62
50
|
if creds
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
begin
|
|
52
|
+
creds.fetch_access_token!
|
|
53
|
+
return creds
|
|
54
|
+
rescue Signet::AuthorizationError
|
|
55
|
+
token_store = Google::Auth::Stores::FileTokenStore.new(file: Config.tokens_path)
|
|
56
|
+
token_store.delete(USER_ID)
|
|
57
|
+
File.delete(Config.tokens_path) if File.exist?(Config.tokens_path)
|
|
58
|
+
authorizer = authorizer_for(mode)
|
|
59
|
+
end
|
|
65
60
|
end
|
|
66
61
|
|
|
67
|
-
# Start local server to receive the auth code
|
|
68
62
|
code = start_redirect_server(authorizer)
|
|
69
|
-
raise Flycal::Error, "
|
|
63
|
+
raise Flycal::Error, Locale.t("login.cancelled") if code.nil? || code.empty?
|
|
70
64
|
|
|
71
65
|
authorizer.get_and_store_credentials_from_code(
|
|
72
|
-
user_id:
|
|
66
|
+
user_id: USER_ID,
|
|
73
67
|
code: code,
|
|
74
68
|
base_url: "http://127.0.0.1:#{REDIRECT_PORT}"
|
|
75
69
|
)
|
|
76
70
|
end
|
|
77
71
|
|
|
78
72
|
def logout
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
73
|
+
if File.exist?(Config.tokens_path)
|
|
74
|
+
token_store = Google::Auth::Stores::FileTokenStore.new(file: Config.tokens_path)
|
|
75
|
+
token_store.delete(USER_ID)
|
|
76
|
+
File.delete(Config.tokens_path) if File.exist?(Config.tokens_path)
|
|
77
|
+
end
|
|
78
|
+
Config.auth_client = nil
|
|
84
79
|
true
|
|
85
80
|
end
|
|
86
81
|
|
|
82
|
+
def client_id_for(mode)
|
|
83
|
+
mode = normalize_mode(mode)
|
|
84
|
+
path = client_id_path(mode)
|
|
85
|
+
unless File.exist?(path)
|
|
86
|
+
raise Flycal::Error, missing_client_message(mode, path)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
Google::Auth::ClientId.from_file(path)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def bundled_client_path
|
|
93
|
+
OAUTH_CLIENT_FILE
|
|
94
|
+
end
|
|
95
|
+
|
|
87
96
|
private
|
|
88
97
|
|
|
98
|
+
def resolved_mode
|
|
99
|
+
stored = Config.auth_client
|
|
100
|
+
return stored if stored
|
|
101
|
+
return CLIENT_JSON if Config.credentials_exist?
|
|
102
|
+
return CLIENT_FLYCAL if File.exist?(Config.tokens_path)
|
|
103
|
+
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def normalize_mode(mode)
|
|
108
|
+
value = mode.to_s.strip.downcase
|
|
109
|
+
return value if [ CLIENT_FLYCAL, CLIENT_JSON ].include?(value)
|
|
110
|
+
|
|
111
|
+
raise Flycal::Error, Locale.t("login.unknown_client", client: mode)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def client_id_path(mode)
|
|
115
|
+
mode == CLIENT_JSON ? Config.credentials_path : bundled_client_path
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def authorizer_for(mode)
|
|
119
|
+
token_store = Google::Auth::Stores::FileTokenStore.new(file: Config.tokens_path)
|
|
120
|
+
Google::Auth::UserAuthorizer.new(
|
|
121
|
+
client_id_for(mode),
|
|
122
|
+
SCOPE,
|
|
123
|
+
token_store,
|
|
124
|
+
"/oauth2callback"
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def missing_client_message(mode, path)
|
|
129
|
+
if mode == CLIENT_JSON
|
|
130
|
+
Locale.t(
|
|
131
|
+
"login.json_missing",
|
|
132
|
+
path: path,
|
|
133
|
+
redirect: REDIRECT_URI
|
|
134
|
+
)
|
|
135
|
+
else
|
|
136
|
+
Locale.t("login.flycal_missing", path: path)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
89
140
|
def start_redirect_server(authorizer)
|
|
141
|
+
require "webrick"
|
|
142
|
+
|
|
90
143
|
auth_code = nil
|
|
91
144
|
state = SecureRandom.hex(16)
|
|
92
145
|
code_verifier = Google::Auth::UserAuthorizer.generate_code_verifier
|
|
@@ -110,7 +163,7 @@ module Cli
|
|
|
110
163
|
elsif params["code"]
|
|
111
164
|
auth_code = params["code"]
|
|
112
165
|
res.status = 200
|
|
113
|
-
res.body = "<h1
|
|
166
|
+
res.body = "<h1>#{Locale.t('login.browser_done_title')}</h1><p>#{Locale.t('login.browser_done_body')}</p>"
|
|
114
167
|
end
|
|
115
168
|
|
|
116
169
|
Thread.new { server.shutdown }
|
|
@@ -121,13 +174,31 @@ module Cli
|
|
|
121
174
|
state: state
|
|
122
175
|
)
|
|
123
176
|
|
|
124
|
-
puts "\
|
|
177
|
+
puts "\n#{Locale.t('login.open_link')}\n\n"
|
|
125
178
|
puts " #{url}\n\n"
|
|
126
|
-
puts "
|
|
179
|
+
puts "#{Locale.t('login.waiting')}\n\n"
|
|
180
|
+
open_browser(url)
|
|
127
181
|
|
|
128
182
|
server.start
|
|
129
183
|
auth_code
|
|
130
184
|
end
|
|
185
|
+
|
|
186
|
+
def open_browser(url)
|
|
187
|
+
host_os = RbConfig::CONFIG["host_os"].to_s
|
|
188
|
+
cmd =
|
|
189
|
+
if host_os.match?(/darwin/)
|
|
190
|
+
[ "open", url ]
|
|
191
|
+
elsif host_os.match?(/linux/)
|
|
192
|
+
[ "xdg-open", url ]
|
|
193
|
+
elsif host_os.match?(/mswin|mingw|cygwin/)
|
|
194
|
+
[ "cmd", "/c", "start", "", url ]
|
|
195
|
+
end
|
|
196
|
+
return unless cmd
|
|
197
|
+
|
|
198
|
+
system(*cmd, out: File::NULL, err: File::NULL)
|
|
199
|
+
rescue StandardError
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
131
202
|
end
|
|
132
203
|
end
|
|
133
204
|
end
|
data/lib/flycal/cli/config.rb
CHANGED
|
@@ -53,6 +53,23 @@ module Cli
|
|
|
53
53
|
File.exist?(CREDENTIALS_FILE)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
def auth_client
|
|
57
|
+
value = load["auth_client"].to_s.strip.downcase
|
|
58
|
+
return value if %w[flycal json].include?(value)
|
|
59
|
+
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def auth_client=(value)
|
|
64
|
+
data = load
|
|
65
|
+
if value.to_s.strip.empty?
|
|
66
|
+
data.delete("auth_client")
|
|
67
|
+
else
|
|
68
|
+
data["auth_client"] = value.to_s.strip.downcase
|
|
69
|
+
end
|
|
70
|
+
save(data)
|
|
71
|
+
end
|
|
72
|
+
|
|
56
73
|
def tokens_path
|
|
57
74
|
TOKENS_FILE
|
|
58
75
|
end
|
|
@@ -149,6 +166,11 @@ module Cli
|
|
|
149
166
|
load["locale"].to_s
|
|
150
167
|
end
|
|
151
168
|
|
|
169
|
+
def timezone
|
|
170
|
+
value = load["timezone"].to_s.strip
|
|
171
|
+
value.empty? ? Flycal::TimeFormat::DEFAULT_ZONE : value
|
|
172
|
+
end
|
|
173
|
+
|
|
152
174
|
def locale=(value)
|
|
153
175
|
data = load
|
|
154
176
|
data["locale"] = value.to_s
|
|
@@ -159,7 +181,8 @@ module Cli
|
|
|
159
181
|
@default_values ||= begin
|
|
160
182
|
raise "Defaults file not found: #{DEFAULTS_FILE}" unless File.exist?(DEFAULTS_FILE)
|
|
161
183
|
|
|
162
|
-
YAML.load_file(DEFAULTS_FILE) || {}
|
|
184
|
+
file = YAML.load_file(DEFAULTS_FILE) || {}
|
|
185
|
+
file.merge("slots" => Flycal::DefaultSlots.config_fragment)
|
|
163
186
|
end
|
|
164
187
|
end
|
|
165
188
|
|