r_cal 0.1.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.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +239 -0
- data/LICENSE +21 -0
- data/README.md +174 -0
- data/Rakefile +47 -0
- data/dev.yml +3 -0
- data/exe/rcal +17 -0
- data/lib/rcal/adapters/auth/base.rb +27 -0
- data/lib/rcal/adapters/auth/google.rb +71 -0
- data/lib/rcal/adapters/calendar/base.rb +35 -0
- data/lib/rcal/adapters/calendar/google.rb +161 -0
- data/lib/rcal/adapters/date_parser/base.rb +11 -0
- data/lib/rcal/adapters/date_parser/chronic.rb +86 -0
- data/lib/rcal/adapters/duration_parser/base.rb +15 -0
- data/lib/rcal/adapters/duration_parser/chronic_duration.rb +47 -0
- data/lib/rcal/adapters/ics_parser/base.rb +15 -0
- data/lib/rcal/adapters/ics_parser/icalendar.rb +68 -0
- data/lib/rcal/auth.rb +46 -0
- data/lib/rcal/calendar_service.rb +103 -0
- data/lib/rcal/command.rb +33 -0
- data/lib/rcal/commands/add.rb +147 -0
- data/lib/rcal/commands/agenda.rb +143 -0
- data/lib/rcal/commands/edit.rb +158 -0
- data/lib/rcal/commands/help.rb +21 -0
- data/lib/rcal/commands/import.rb +121 -0
- data/lib/rcal/commands/init.rb +158 -0
- data/lib/rcal/commands/list.rb +49 -0
- data/lib/rcal/commands/quick.rb +80 -0
- data/lib/rcal/commands.rb +21 -0
- data/lib/rcal/config.rb +68 -0
- data/lib/rcal/date_parser.rb +27 -0
- data/lib/rcal/duration_parser.rb +31 -0
- data/lib/rcal/entry_point.rb +10 -0
- data/lib/rcal/errors.rb +7 -0
- data/lib/rcal/formatters/agenda.rb +118 -0
- data/lib/rcal/ics_parser.rb +31 -0
- data/lib/rcal/models/calendar.rb +61 -0
- data/lib/rcal/models/event.rb +140 -0
- data/lib/rcal/predicate_collection.rb +65 -0
- data/lib/rcal/presenters/calendar_presenter.rb +34 -0
- data/lib/rcal/presenters/event_presenter.rb +98 -0
- data/lib/rcal/version.rb +5 -0
- data/lib/rcal.rb +27 -0
- data/mise.toml +2 -0
- metadata +328 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
require "google/apis/calendar_v3"
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../../models/calendar"
|
|
5
|
+
require_relative "../../models/event"
|
|
6
|
+
|
|
7
|
+
module Rcal
|
|
8
|
+
module Adapters
|
|
9
|
+
module Calendar
|
|
10
|
+
class Google < Base
|
|
11
|
+
def initialize(service:)
|
|
12
|
+
@service = service
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def list_calendars
|
|
16
|
+
response = @service.list_calendar_lists
|
|
17
|
+
items = response.items || []
|
|
18
|
+
|
|
19
|
+
items.map { |cal| build_calendar(cal) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def list_events(calendar_id:, time_min:, time_max:)
|
|
23
|
+
response = @service.list_events(
|
|
24
|
+
calendar_id,
|
|
25
|
+
time_min: time_min.iso8601,
|
|
26
|
+
time_max: time_max.iso8601,
|
|
27
|
+
single_events: true,
|
|
28
|
+
order_by: "startTime"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
items = response.items || []
|
|
32
|
+
|
|
33
|
+
items.map { |evt| build_event(evt, calendar_id: calendar_id) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def get_event(calendar_id:, event_id:)
|
|
37
|
+
google_event = @service.get_event(calendar_id, event_id)
|
|
38
|
+
build_event(google_event, calendar_id: calendar_id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def create_event(calendar_id:, event:)
|
|
42
|
+
google_event = build_google_event(event)
|
|
43
|
+
response = @service.insert_event(calendar_id, google_event)
|
|
44
|
+
build_event(response, calendar_id: calendar_id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def update_event(calendar_id:, event_id:, event:)
|
|
48
|
+
google_event = build_google_event(event)
|
|
49
|
+
response = @service.update_event(calendar_id, event_id, google_event)
|
|
50
|
+
build_event(response, calendar_id: calendar_id)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def delete_event(calendar_id:, event_id:)
|
|
54
|
+
@service.delete_event(calendar_id, event_id)
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def quick_add(calendar_id:, text:)
|
|
59
|
+
response = @service.quick_add_event(calendar_id, text)
|
|
60
|
+
build_event(response, calendar_id: calendar_id)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def build_calendar(google_calendar)
|
|
66
|
+
Rcal::Calendar.new(
|
|
67
|
+
id: google_calendar.id,
|
|
68
|
+
name: google_calendar.summary,
|
|
69
|
+
description: google_calendar.description,
|
|
70
|
+
timezone: google_calendar.time_zone,
|
|
71
|
+
color: google_calendar.background_color,
|
|
72
|
+
access_role: google_calendar.access_role,
|
|
73
|
+
primary: google_calendar.primary || false,
|
|
74
|
+
selected: google_calendar.selected || false
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_event(google_event, calendar_id:)
|
|
79
|
+
start_time, all_day = parse_event_time(google_event.start)
|
|
80
|
+
end_time, = parse_event_time(google_event.end)
|
|
81
|
+
|
|
82
|
+
Rcal::Event.new(
|
|
83
|
+
id: google_event.id,
|
|
84
|
+
summary: google_event.summary,
|
|
85
|
+
description: google_event.description,
|
|
86
|
+
location: google_event.location,
|
|
87
|
+
start_time: start_time,
|
|
88
|
+
end_time: end_time,
|
|
89
|
+
all_day: all_day,
|
|
90
|
+
calendar_id: calendar_id,
|
|
91
|
+
transparency: google_event.transparency,
|
|
92
|
+
recurrence: google_event.recurrence,
|
|
93
|
+
recurring_event_id: google_event.recurring_event_id,
|
|
94
|
+
response_status: extract_self_response_status(google_event.attendees),
|
|
95
|
+
attendees: build_attendees(google_event.attendees),
|
|
96
|
+
timezone: google_event.start&.time_zone
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_event_time(time_obj)
|
|
101
|
+
return [Time.now, false] if time_obj.nil?
|
|
102
|
+
|
|
103
|
+
if time_obj.date_time
|
|
104
|
+
# Google returns DateTime; convert to Time for consistent arithmetic
|
|
105
|
+
[time_obj.date_time.to_time, false]
|
|
106
|
+
elsif time_obj.date
|
|
107
|
+
[Date.parse(time_obj.date).to_time, true]
|
|
108
|
+
else
|
|
109
|
+
[Time.now, false]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def extract_self_response_status(attendees)
|
|
114
|
+
return nil if attendees.nil?
|
|
115
|
+
|
|
116
|
+
self_attendee = attendees.find { |a| a.self }
|
|
117
|
+
self_attendee&.response_status
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def build_attendees(google_attendees)
|
|
121
|
+
return nil if google_attendees.nil?
|
|
122
|
+
|
|
123
|
+
google_attendees.map do |attendee|
|
|
124
|
+
{
|
|
125
|
+
email: attendee.email,
|
|
126
|
+
response_status: attendee.response_status,
|
|
127
|
+
self: attendee.self || false,
|
|
128
|
+
resource: attendee.resource || false
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def build_google_event(event)
|
|
134
|
+
google_event = ::Google::Apis::CalendarV3::Event.new(
|
|
135
|
+
summary: event.summary,
|
|
136
|
+
description: event.description,
|
|
137
|
+
location: event.location
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if event.all_day?
|
|
141
|
+
google_event.start = ::Google::Apis::CalendarV3::EventDateTime.new(
|
|
142
|
+
date: event.start_time.to_date.to_s
|
|
143
|
+
)
|
|
144
|
+
google_event.end = ::Google::Apis::CalendarV3::EventDateTime.new(
|
|
145
|
+
date: event.end_time.to_date.to_s
|
|
146
|
+
)
|
|
147
|
+
else
|
|
148
|
+
google_event.start = ::Google::Apis::CalendarV3::EventDateTime.new(
|
|
149
|
+
date_time: event.start_time.iso8601
|
|
150
|
+
)
|
|
151
|
+
google_event.end = ::Google::Apis::CalendarV3::EventDateTime.new(
|
|
152
|
+
date_time: event.end_time.iso8601
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
google_event
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require "chronic"
|
|
2
|
+
require "date"
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../../errors"
|
|
5
|
+
|
|
6
|
+
module Rcal
|
|
7
|
+
module Adapters
|
|
8
|
+
module DateParser
|
|
9
|
+
class Chronic < Base
|
|
10
|
+
# Custom relative date pattern: +N, +Nd, +Nw
|
|
11
|
+
RELATIVE_PATTERN = /\A\+(\d+)(d|w)?\z/i
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def parse(input)
|
|
15
|
+
new.parse(input)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse(input)
|
|
20
|
+
normalized = normalize_input(input)
|
|
21
|
+
validate_input!(normalized)
|
|
22
|
+
|
|
23
|
+
result = parse_relative(normalized) ||
|
|
24
|
+
parse_with_chronic(normalized)
|
|
25
|
+
|
|
26
|
+
raise Rcal::ParseError, "Could not parse: #{input.inspect}" if result.nil?
|
|
27
|
+
|
|
28
|
+
normalize_result(result, has_time_component?(normalized))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def normalize_input(input)
|
|
34
|
+
input.to_s.strip
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_input!(input)
|
|
38
|
+
raise Rcal::ParseError, "Input cannot be empty" if input.empty?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse_relative(input)
|
|
42
|
+
match = input.match(RELATIVE_PATTERN)
|
|
43
|
+
return nil unless match
|
|
44
|
+
|
|
45
|
+
count = match[1].to_i
|
|
46
|
+
unit = match[2]&.downcase
|
|
47
|
+
|
|
48
|
+
case unit
|
|
49
|
+
when "w"
|
|
50
|
+
Date.today + (count * 7)
|
|
51
|
+
else
|
|
52
|
+
Date.today + count
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse_with_chronic(input)
|
|
57
|
+
::Chronic.parse(input)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def has_time_component?(input)
|
|
61
|
+
# Check if input contains time-related patterns
|
|
62
|
+
time_patterns = [
|
|
63
|
+
/\d{1,2}:\d{2}/, # 14:30, 2:30
|
|
64
|
+
/\d{1,2}\s*(am|pm)/i, # 3pm, 3 pm
|
|
65
|
+
/at\s+\d/i, # at 3, at 14
|
|
66
|
+
/noon/i,
|
|
67
|
+
/midnight/i
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
time_patterns.any? { |pattern| input.match?(pattern) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def normalize_result(result, preserve_time)
|
|
74
|
+
case result
|
|
75
|
+
when Time
|
|
76
|
+
preserve_time ? result : result.to_date
|
|
77
|
+
when Date
|
|
78
|
+
result
|
|
79
|
+
else
|
|
80
|
+
result.to_date
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Rcal
|
|
2
|
+
module Adapters
|
|
3
|
+
module DurationParser
|
|
4
|
+
class Base
|
|
5
|
+
def parse(input)
|
|
6
|
+
raise NotImplementedError, "#{self.class} must implement #parse"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def parse_minutes(input)
|
|
10
|
+
raise NotImplementedError, "#{self.class} must implement #parse_minutes"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "chronic_duration"
|
|
2
|
+
require_relative "base"
|
|
3
|
+
require_relative "../../errors"
|
|
4
|
+
|
|
5
|
+
module Rcal
|
|
6
|
+
module Adapters
|
|
7
|
+
module DurationParser
|
|
8
|
+
class ChronicDuration < Base
|
|
9
|
+
class << self
|
|
10
|
+
def parse(input)
|
|
11
|
+
new.parse(input)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def parse_minutes(input)
|
|
15
|
+
new.parse_minutes(input)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse(input)
|
|
20
|
+
normalized = normalize_input(input)
|
|
21
|
+
validate_input!(normalized)
|
|
22
|
+
|
|
23
|
+
result = ::ChronicDuration.parse(normalized)
|
|
24
|
+
|
|
25
|
+
raise Rcal::ParseError, "Could not parse duration: #{input.inspect}" if result.nil?
|
|
26
|
+
|
|
27
|
+
result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_minutes(input)
|
|
31
|
+
seconds = parse(input)
|
|
32
|
+
seconds / 60
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def normalize_input(input)
|
|
38
|
+
input.to_s.strip
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def validate_input!(input)
|
|
42
|
+
raise Rcal::ParseError, "Input cannot be empty" if input.empty?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Rcal
|
|
2
|
+
module Adapters
|
|
3
|
+
module IcsParser
|
|
4
|
+
class Base
|
|
5
|
+
def parse(content)
|
|
6
|
+
raise NotImplementedError, "#{self.class} must implement #parse"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def parse_file(path)
|
|
10
|
+
raise NotImplementedError, "#{self.class} must implement #parse_file"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require "icalendar"
|
|
2
|
+
require_relative "base"
|
|
3
|
+
require_relative "../../models/event"
|
|
4
|
+
require_relative "../../errors"
|
|
5
|
+
|
|
6
|
+
module Rcal
|
|
7
|
+
module Adapters
|
|
8
|
+
module IcsParser
|
|
9
|
+
class Icalendar < Base
|
|
10
|
+
def parse(content)
|
|
11
|
+
calendars = ::Icalendar::Calendar.parse(content)
|
|
12
|
+
raise Rcal::ParseError, "Invalid ICS content" if calendars.empty?
|
|
13
|
+
|
|
14
|
+
extract_events(calendars)
|
|
15
|
+
rescue ::Icalendar::Parser::ParseError => e
|
|
16
|
+
raise Rcal::ParseError, "Failed to parse ICS: #{e.message}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse_file(path)
|
|
20
|
+
raise Rcal::ParseError, "File not found: #{path}" unless File.exist?(path)
|
|
21
|
+
|
|
22
|
+
content = File.read(path)
|
|
23
|
+
parse(content)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def extract_events(calendars)
|
|
29
|
+
calendars.flat_map do |calendar|
|
|
30
|
+
calendar.events.map { |ics_event| build_event(ics_event) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_event(ics_event)
|
|
35
|
+
start_time, all_day = parse_datetime(ics_event.dtstart)
|
|
36
|
+
end_time, = parse_datetime(ics_event.dtend)
|
|
37
|
+
|
|
38
|
+
Rcal::Event.new(
|
|
39
|
+
summary: ics_event.summary&.to_s,
|
|
40
|
+
description: presence(ics_event.description&.to_s),
|
|
41
|
+
location: presence(ics_event.location&.to_s),
|
|
42
|
+
start_time: start_time,
|
|
43
|
+
end_time: end_time,
|
|
44
|
+
all_day: all_day
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def presence(value)
|
|
49
|
+
return nil if value.nil? || value.empty?
|
|
50
|
+
value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def parse_datetime(dt)
|
|
54
|
+
return [Time.now, false] if dt.nil?
|
|
55
|
+
|
|
56
|
+
case dt
|
|
57
|
+
when ::Icalendar::Values::Date
|
|
58
|
+
[dt.to_time, true]
|
|
59
|
+
when ::Icalendar::Values::DateTime
|
|
60
|
+
[dt.to_time, false]
|
|
61
|
+
else
|
|
62
|
+
[dt.to_time, false]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/rcal/auth.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require_relative "adapters/auth/google"
|
|
2
|
+
require_relative "config"
|
|
3
|
+
|
|
4
|
+
module Rcal
|
|
5
|
+
class Auth
|
|
6
|
+
class << self
|
|
7
|
+
def store_credentials(credentials)
|
|
8
|
+
adapter.store_credentials(credentials)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def load_credentials
|
|
12
|
+
adapter.load_credentials
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def authenticated?
|
|
16
|
+
adapter.authenticated?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def token_expired?
|
|
20
|
+
adapter.token_expired?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def clear_credentials
|
|
24
|
+
adapter.clear_credentials
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def adapter
|
|
28
|
+
@adapter ||= default_adapter
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
attr_writer :adapter
|
|
32
|
+
|
|
33
|
+
def reset_adapter!
|
|
34
|
+
@adapter = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def default_adapter
|
|
40
|
+
Adapters::Auth::Google.new(
|
|
41
|
+
token_path: File.join(Configuration.data_dir, "tokens.json")
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "yaml"
|
|
3
|
+
require "google/apis/calendar_v3"
|
|
4
|
+
require "googleauth"
|
|
5
|
+
require_relative "adapters/calendar/google"
|
|
6
|
+
require_relative "auth"
|
|
7
|
+
require_relative "config"
|
|
8
|
+
|
|
9
|
+
module Rcal
|
|
10
|
+
class CalendarService
|
|
11
|
+
class << self
|
|
12
|
+
def list_calendars
|
|
13
|
+
adapter.list_calendars
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def list_events(calendar_id:, time_min:, time_max:)
|
|
17
|
+
adapter.list_events(
|
|
18
|
+
calendar_id: calendar_id,
|
|
19
|
+
time_min: time_min,
|
|
20
|
+
time_max: time_max
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get_event(calendar_id:, event_id:)
|
|
25
|
+
adapter.get_event(calendar_id: calendar_id, event_id: event_id)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def create_event(calendar_id:, event:)
|
|
29
|
+
adapter.create_event(calendar_id: calendar_id, event: event)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update_event(calendar_id:, event_id:, event:)
|
|
33
|
+
adapter.update_event(calendar_id: calendar_id, event_id: event_id, event: event)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def delete_event(calendar_id:, event_id:)
|
|
37
|
+
adapter.delete_event(calendar_id: calendar_id, event_id: event_id)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def quick_add(calendar_id:, text:)
|
|
41
|
+
adapter.quick_add(calendar_id: calendar_id, text: text)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def adapter
|
|
45
|
+
@adapter ||= default_adapter
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
attr_writer :adapter
|
|
49
|
+
|
|
50
|
+
def reset_adapter!
|
|
51
|
+
@adapter = nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def default_adapter
|
|
57
|
+
Adapters::Calendar::Google.new(service: build_calendar_service)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_calendar_service
|
|
61
|
+
service = Google::Apis::CalendarV3::CalendarService.new
|
|
62
|
+
service.authorization = load_credentials
|
|
63
|
+
service
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load_credentials
|
|
67
|
+
client_creds = load_client_credentials
|
|
68
|
+
token_data = load_google_token_data
|
|
69
|
+
|
|
70
|
+
return nil if client_creds.nil? || token_data.nil?
|
|
71
|
+
|
|
72
|
+
Google::Auth::UserRefreshCredentials.new(
|
|
73
|
+
client_id: client_creds["client_id"],
|
|
74
|
+
client_secret: client_creds["client_secret"],
|
|
75
|
+
access_token: token_data["access_token"],
|
|
76
|
+
refresh_token: token_data["refresh_token"],
|
|
77
|
+
expires_at: token_data["expiration_time_millis"] ? Time.at(token_data["expiration_time_millis"] / 1000) : nil
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def load_client_credentials
|
|
82
|
+
creds_file = File.join(Configuration.data_dir, "client_credentials.json")
|
|
83
|
+
return nil unless File.exist?(creds_file)
|
|
84
|
+
|
|
85
|
+
JSON.parse(File.read(creds_file))
|
|
86
|
+
rescue JSON::ParserError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def load_google_token_data
|
|
91
|
+
token_file = File.join(Configuration.data_dir, "google_tokens.yaml")
|
|
92
|
+
return nil unless File.exist?(token_file)
|
|
93
|
+
|
|
94
|
+
yaml_data = YAML.safe_load_file(token_file)
|
|
95
|
+
return nil unless yaml_data&.key?("default")
|
|
96
|
+
|
|
97
|
+
JSON.parse(yaml_data["default"])
|
|
98
|
+
rescue JSON::ParserError, Psych::SyntaxError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/rcal/command.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "cli/kit"
|
|
2
|
+
|
|
3
|
+
module Rcal
|
|
4
|
+
class Command < CLI::Kit::BaseCommand
|
|
5
|
+
def call(args, command_name)
|
|
6
|
+
if help_requested?(args)
|
|
7
|
+
display_help
|
|
8
|
+
return
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
run(args, command_name)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Subclasses implement this instead of call
|
|
15
|
+
def run(args, command_name)
|
|
16
|
+
raise NotImplementedError, "#{self.class} must implement #run"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def help_requested?(args)
|
|
22
|
+
args.any? { |arg| arg == "help" || arg == "-h" || arg == "--help" }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def display_help
|
|
26
|
+
if self.class.respond_to?(:help) && self.class.help
|
|
27
|
+
puts self.class.help
|
|
28
|
+
else
|
|
29
|
+
puts "No help available for this command."
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|