flycal-cli 0.7.7 → 1.1
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/README.md +6 -181
- data/bin/flycal +2 -2
- data/config/defaults.yml +2 -28
- data/lib/flycal/cache_annotator.rb +83 -0
- data/lib/flycal/cache_key.rb +82 -0
- data/lib/flycal/calendar_query.rb +60 -0
- data/lib/{flycal_cli → flycal}/calendar_service.rb +48 -4
- data/lib/{flycal_cli/cli.rb → flycal/cli/app.rb} +246 -171
- data/lib/{flycal_cli → flycal/cli}/auth.rb +5 -3
- data/lib/{flycal_cli → flycal/cli}/clipboard.rb +3 -1
- data/lib/{flycal_cli → flycal/cli}/config.rb +6 -3
- data/lib/flycal/cli/file_cache.rb +44 -0
- data/lib/flycal/cli/locale.rb +58 -0
- data/lib/flycal/cli.rb +14 -0
- data/lib/flycal/client.rb +141 -0
- data/lib/flycal/credentials.rb +34 -0
- data/lib/{flycal_cli → flycal}/date_time_parser.rb +4 -4
- data/lib/flycal/default_slots.rb +77 -0
- data/lib/{flycal_cli → flycal}/description_query.rb +3 -3
- data/lib/{flycal_cli → flycal}/duration_parser.rb +3 -3
- data/lib/flycal/error.rb +5 -0
- data/lib/flycal/event_mapper.rb +58 -0
- data/lib/{flycal_cli → flycal}/locale.rb +35 -3
- data/lib/{flycal_cli → flycal}/mock/calendar_service.rb +1 -1
- data/lib/{flycal_cli → flycal}/mock/config.rb +14 -14
- data/lib/{flycal_cli → flycal}/mock/event_generator.rb +6 -4
- data/lib/flycal/mock/slot_scenario.rb +75 -0
- data/lib/flycal/mock.rb +6 -0
- data/lib/{flycal_cli → flycal}/pipeline/aggregator.rb +21 -24
- data/lib/{flycal_cli → flycal}/pipeline/json_renderer.rb +16 -17
- data/lib/{flycal_cli → flycal}/pipeline/params.rb +1 -1
- data/lib/{flycal_cli → flycal}/pipeline/renderer.rb +4 -4
- data/lib/{flycal_cli → flycal}/pipeline/retriever.rb +1 -1
- data/lib/{flycal_cli → flycal}/pipeline/search_pipeline.rb +1 -1
- data/lib/{flycal_cli → flycal}/pipeline/text_renderer.rb +14 -12
- data/lib/flycal/pipeline.rb +6 -0
- data/lib/{flycal_cli → flycal}/slot_finder.rb +12 -12
- data/lib/flycal/slot_formatter.rb +196 -0
- data/lib/flycal/slot_query.rb +74 -0
- data/lib/flycal/slot_templates.rb +129 -0
- data/lib/flycal/time_format.rb +36 -0
- data/lib/flycal/version.rb +11 -0
- data/lib/flycal.rb +12 -0
- data/lib/flycal_cli.rb +42 -17
- data/mcp/tools.json +21 -8
- metadata +41 -25
- data/lib/flycal_cli/mock.rb +0 -10
- data/lib/flycal_cli/pipeline.rb +0 -14
- data/lib/flycal_cli/slot_formatter.rb +0 -68
- data/lib/flycal_cli/version.rb +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fcbd8c2cfb6f90272cf8cc8cfa489bc99a7487efca11f68560b2d7f96ac92f34
|
|
4
|
+
data.tar.gz: 21e92239db0dc5b3f587abdd1e859b5e67e67293706f6ec1f50ca34ca4fe0e1d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2524fb58d1faecdf6ac6643761315f2586f6fe10b4535f2f50b87418f871665f13c84110c3d627ad2131bb9b2ef95aa171c0e6e1e41b52f87d7b76fd7261493c
|
|
7
|
+
data.tar.gz: 906c85b9dd3711aa16874a6f41be9851be1accbd07d131b09fb9f6173ed26d0e081f6607b457d606f538ea8861ab98dcff89b6cdab4f482b79ffe0c8af41eb7b
|
data/README.md
CHANGED
|
@@ -1,188 +1,13 @@
|
|
|
1
1
|
# flycal-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI wrapper around **Flycal** core (`lib/flycal` in the monorepo).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
gem install flycal-cli
|
|
11
|
-
flycal login
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
During the test phase the project already provides the Google API credentials.
|
|
15
|
-
Open the browser, approve access, done.
|
|
16
|
-
|
|
17
|
-
Optional but useful once:
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
flycal config # set default calendar + calendars that block free slots
|
|
21
|
-
flycal calendars # list calendars (name + id)
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Common examples
|
|
25
|
-
|
|
26
|
-
**Find free slots (most useful day-to-day)**
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
flycal slots
|
|
30
|
-
flycal slots --in "5 days"
|
|
31
|
-
flycal slots --from monday --in "5 days"
|
|
32
|
-
flycal slots --from "next monday" --in "2 weeks" --duration 1h
|
|
33
|
-
flycal slots --in "12 days" --template dinner
|
|
34
|
-
flycal slots --locale it --from lunedi --in "5 days"
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Output is a short header plus a clean list ready to paste (and copied to the clipboard when `pbcopy` / `clip` / `wl-copy` / `xclip` is available):
|
|
38
|
-
|
|
39
|
-
```
|
|
40
|
-
found 6 slots
|
|
41
|
-
from sat 1 August 2026 to sat 8 August 2026
|
|
42
|
-
with duration 45min, template dinner
|
|
43
|
-
considering calendars
|
|
44
|
-
- Work
|
|
45
|
-
- Personal
|
|
46
|
-
link: https://calendar.google.com/calendar/r/day/2026/8/1
|
|
47
|
-
|
|
48
|
-
saturday 1/8
|
|
49
|
-
19 - 23
|
|
50
|
-
|
|
51
|
-
monday 3/8
|
|
52
|
-
19 - 23
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
**Search events**
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
flycal search
|
|
59
|
-
flycal search --in 30days
|
|
60
|
-
flycal search --in 30days --description meeting
|
|
61
|
-
flycal search --from 2026-08-01 --to 2026-08-31 -c Work
|
|
62
|
-
flycal search --from monday --in "2 weeks" --locale it
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**Other commands**
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
flycal version
|
|
69
|
-
flycal update
|
|
70
|
-
flycal logout
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
All commands accept `--locale en|it`.
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## Configuration (`~/.flycal/config.yml`)
|
|
78
|
-
|
|
79
|
-
Defaults are created automatically on first run. Edit with `flycal config` → `edit config`, or by hand.
|
|
80
|
-
|
|
81
|
-
```yaml
|
|
82
|
-
calendar_default: ~
|
|
83
|
-
locale: en
|
|
84
|
-
slots:
|
|
85
|
-
templates:
|
|
86
|
-
work:
|
|
87
|
-
days: [1, 2, 3, 4, 5] # 1=Mon … 7=Sun
|
|
88
|
-
hours:
|
|
89
|
-
- 9:30-13:00
|
|
90
|
-
- 14:00-18:30
|
|
91
|
-
dinner:
|
|
92
|
-
days: [1, 2, 3, 4, 5, 6, 7]
|
|
93
|
-
hours:
|
|
94
|
-
- 19-23
|
|
95
|
-
exclude_calendars: [] # calendars whose events block free slots
|
|
96
|
-
defaults:
|
|
97
|
-
from: now
|
|
98
|
-
default_duration: 45min
|
|
99
|
-
free_before: 0m
|
|
100
|
-
free_after: 15m
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
| Key | Meaning |
|
|
104
|
-
|---|---|
|
|
105
|
-
| `calendar_default` | Default calendar (`~` = unset) |
|
|
106
|
-
| `locale` | `en` or `it` |
|
|
107
|
-
| `slots.templates` | Named schedules; `flycal slots` uses the first one unless `--template` is set |
|
|
108
|
-
| `slots.exclude_calendars` | Events here block free slots (fallback: `calendar_default`) |
|
|
109
|
-
| `slots.defaults.*` | Defaults for `--from` / `--duration` |
|
|
110
|
-
| `slots.free_before` / `free_after` | Buffers around busy events |
|
|
111
|
-
|
|
112
|
-
Interactive helpers:
|
|
5
|
+
- Core namespace: `Flycal`
|
|
6
|
+
- CLI namespace: `Flycal::Cli`
|
|
7
|
+
- Gem name: `flycal-cli`
|
|
113
8
|
|
|
114
9
|
```bash
|
|
115
|
-
flycal
|
|
10
|
+
ruby -Ilib -Iflycal_cli/lib flycal_cli/bin/flycal --help
|
|
116
11
|
```
|
|
117
12
|
|
|
118
|
-
|
|
119
|
-
- `exclude_calendars` — multi-select calendars that block slots
|
|
120
|
-
- `edit config` — open the YAML in `$EDITOR`
|
|
121
|
-
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
## Parameters (reference)
|
|
125
|
-
|
|
126
|
-
### `flycal slots`
|
|
127
|
-
|
|
128
|
-
| Option | Default | Notes |
|
|
129
|
-
|---|---|---|
|
|
130
|
-
| `--duration` | `45min` | Min free range length (`1h`, `90min`, …) |
|
|
131
|
-
| `--from` / `-f` | `now` | Absolute date or relative: `monday`, `next monday`, `tomorrow`, `lunedi`, … |
|
|
132
|
-
| `--in` / `-i` | `1 week` | Window from `--from` (`"5 days"`, `2weeks`, …) |
|
|
133
|
-
| `--template` / `-T` | first template (`work`) | e.g. `dinner` |
|
|
134
|
-
| `--calendar` / `-c` | — | Fallback if `exclude_calendars` is empty |
|
|
135
|
-
| `--locale` | config / `en` | `en` or `it` |
|
|
136
|
-
|
|
137
|
-
### `flycal search`
|
|
138
|
-
|
|
139
|
-
| Option | Default | Notes |
|
|
140
|
-
|---|---|---|
|
|
141
|
-
| `--from` / `-f` | today midnight | Absolute or relative date |
|
|
142
|
-
| `--to` / `-t` | +30 days | Ignored if `--in` is set |
|
|
143
|
-
| `--in` / `-i` | — | Duration from `--from` |
|
|
144
|
-
| `--description` / `-d` | — | Contains filter; OR terms with `\|` (e.g. `rui\|solver`) |
|
|
145
|
-
| `--groupBy` | auto | `day`, `week`, `month`, or `description` (split `--description` terms) |
|
|
146
|
-
| `--calendar` / `-c` | `calendar_default` | Name or ID |
|
|
147
|
-
| `--locale` | config / `en` | `en` or `it` |
|
|
148
|
-
| `--format` | `text` | Output format: `text` or `json` |
|
|
149
|
-
| `--mockTemplate` | — | Load mock defaults from `mocks/` or `mockTemplates/<name>.json` (no Google API) |
|
|
150
|
-
| `--mockCalendar` | from template | Generated mock calendar name/id (required in template or CLI) |
|
|
151
|
-
| `--mockSeed` | random | Reproducible mock distribution (printed in summary) |
|
|
152
|
-
|
|
153
|
-
Mock templates live in `mocks/` or `mockTemplates/` (example: `mock1.json` with `mockCalendar`). CLI mock flags override template values. Search `--from` / `--to` keep the normal defaults (today → +30 days), independent of the mock generation window.
|
|
154
|
-
|
|
155
|
-
Relative dates work in English and Italian: `today`/`oggi`, `tomorrow`/`domani`, `monday`/`lunedi`, `next monday`/`prossimo lunedi`, `last friday`/`scorso venerdi`.
|
|
156
|
-
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
## Optional: your own Google Cloud credentials
|
|
160
|
-
|
|
161
|
-
**Not needed** for the default test API — `flycal login` is enough.
|
|
162
|
-
|
|
163
|
-
Use this only if you want flycal to talk to **your own** Google Cloud OAuth client instead of the project-provided test credentials.
|
|
164
|
-
|
|
165
|
-
1. [Google Cloud Console → Credentials](https://console.cloud.google.com/apis/credentials)
|
|
166
|
-
2. Enable **Google Calendar API**
|
|
167
|
-
3. Create **Desktop app** OAuth credentials
|
|
168
|
-
4. Authorized redirect URI: `http://127.0.0.1:9292/oauth2callback`
|
|
169
|
-
5. Save the JSON as `~/.flycal/credentials.json`
|
|
170
|
-
6. Run `flycal login`
|
|
171
|
-
|
|
172
|
-
You can also ask a teammate who manages the Cloud project to share that JSON with you.
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
## Files
|
|
177
|
-
|
|
178
|
-
| Path | Purpose |
|
|
179
|
-
|---|---|
|
|
180
|
-
| `~/.flycal/config.yml` | Settings (calendars, templates, locale, …) |
|
|
181
|
-
| `~/.flycal/credentials.json` | Own OAuth client (optional) |
|
|
182
|
-
| `~/.flycal/tokens.yml` | Login tokens (managed automatically) |
|
|
183
|
-
|
|
184
|
-
MCP catalog for agents: [`mcp/tools.json`](mcp/tools.json)
|
|
185
|
-
|
|
186
|
-
## License
|
|
187
|
-
|
|
188
|
-
MIT
|
|
13
|
+
Release from the Rails app root: `bin/release-cli`
|
data/bin/flycal
CHANGED
data/config/defaults.yml
CHANGED
|
@@ -1,30 +1,4 @@
|
|
|
1
1
|
calendar_default: ~
|
|
2
2
|
locale: en
|
|
3
|
-
slots
|
|
4
|
-
|
|
5
|
-
work:
|
|
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
|
+
# `slots` is injected from Flycal::DefaultSlots at load/setup time.
|
|
4
|
+
# After first write, the CLI reads templates from this file on disk.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flycal
|
|
4
|
+
# Injects cache HIT/MISS metadata into text / JSON payloads.
|
|
5
|
+
module CacheAnnotator
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def line(status, _cache_key = nil)
|
|
9
|
+
"Cache: #{status}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def annotate_text(text, status, cache_key = nil)
|
|
13
|
+
lines = text.to_s.sub(/\n\z/, "").split("\n")
|
|
14
|
+
insert_cache_line!(lines, status, cache_key, fallback: :append)
|
|
15
|
+
"#{lines.join("\n")}\n"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def annotate_payload!(payload, status, cache_key)
|
|
19
|
+
payload = stringify_keys(payload)
|
|
20
|
+
info = payload["info"] ||= {}
|
|
21
|
+
info["cache"] = status
|
|
22
|
+
info.delete("cache_key")
|
|
23
|
+
|
|
24
|
+
if payload["text"].is_a?(Array)
|
|
25
|
+
lines = payload["text"].map(&:to_s)
|
|
26
|
+
insert_cache_line!(lines, status, cache_key, fallback: :unshift)
|
|
27
|
+
payload["text"] = lines
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
payload
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def strip_for_storage(payload)
|
|
34
|
+
copy = Marshal.load(Marshal.dump(stringify_keys(payload)))
|
|
35
|
+
if copy["info"].is_a?(Hash)
|
|
36
|
+
copy["info"] = copy["info"].dup
|
|
37
|
+
copy["info"].delete("cache")
|
|
38
|
+
copy["info"].delete("cache_key")
|
|
39
|
+
end
|
|
40
|
+
if copy["text"].is_a?(Array)
|
|
41
|
+
copy["text"] = copy["text"].reject { |l| l.to_s.start_with?("Cache:") }
|
|
42
|
+
end
|
|
43
|
+
copy
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def stringify_keys(value)
|
|
47
|
+
case value
|
|
48
|
+
when Hash
|
|
49
|
+
value.each_with_object({}) { |(k, v), out| out[k.to_s] = stringify_keys(v) }
|
|
50
|
+
when Array
|
|
51
|
+
value.map { |v| stringify_keys(v) }
|
|
52
|
+
else
|
|
53
|
+
value
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def insert_cache_line!(lines, status, cache_key = nil, fallback: :append)
|
|
58
|
+
lines.reject! { |l| l.to_s.start_with?("Cache:") }
|
|
59
|
+
marker = line(status, cache_key)
|
|
60
|
+
|
|
61
|
+
occupied_idx = lines.index { |l| l.to_s.start_with?("Total time occupied:") }
|
|
62
|
+
if occupied_idx
|
|
63
|
+
lines.insert(occupied_idx + 1, marker)
|
|
64
|
+
return lines
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
idx = lines.index("---")
|
|
68
|
+
if idx
|
|
69
|
+
lines.insert(idx + 1, marker)
|
|
70
|
+
else
|
|
71
|
+
blank_idx = lines.index { |l| l == "" }
|
|
72
|
+
if blank_idx
|
|
73
|
+
lines.insert(blank_idx, marker)
|
|
74
|
+
elsif fallback == :unshift
|
|
75
|
+
lines.unshift(marker)
|
|
76
|
+
else
|
|
77
|
+
lines << "---" << marker
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
lines
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Flycal
|
|
7
|
+
# Builds stable cache keys for search/slots from fully resolved params
|
|
8
|
+
# (defaults applied, just before Google API calls).
|
|
9
|
+
class CacheKey
|
|
10
|
+
DEFAULT_TTL_MINUTES = 5
|
|
11
|
+
|
|
12
|
+
# Commands / CLI verbs / API paths that use response caching.
|
|
13
|
+
# Everything else (version, calendars, login, config, …) is uncached.
|
|
14
|
+
CACHED_COMMANDS = %w[search slots].freeze
|
|
15
|
+
|
|
16
|
+
SKIP_KEYS = %w[
|
|
17
|
+
nocache cache_status cache_key events totals groups calendar_names raw
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def cached_commands
|
|
22
|
+
CACHED_COMMANDS
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def cached?(command)
|
|
26
|
+
CACHED_COMMANDS.include?(command.to_s)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def generate(command, params)
|
|
30
|
+
digest = Digest::SHA256.hexdigest(canonical_json(params))
|
|
31
|
+
"flycal-#{command}-#{digest}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def canonical_json(params)
|
|
35
|
+
JSON.generate(deep_sort(normalize(params)))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def normalize(value)
|
|
39
|
+
case value
|
|
40
|
+
when Hash
|
|
41
|
+
value.each_with_object({}) do |(k, v), out|
|
|
42
|
+
key = k.to_s
|
|
43
|
+
next if SKIP_KEYS.include?(key)
|
|
44
|
+
|
|
45
|
+
out[key] = normalize(v)
|
|
46
|
+
end
|
|
47
|
+
when Array
|
|
48
|
+
value.map { |v| normalize(v) }
|
|
49
|
+
when Time, DateTime
|
|
50
|
+
value.respond_to?(:iso8601) ? value.iso8601 : value.to_s
|
|
51
|
+
when Date
|
|
52
|
+
value.iso8601
|
|
53
|
+
when nil, TrueClass, FalseClass, Numeric, String
|
|
54
|
+
value
|
|
55
|
+
else
|
|
56
|
+
if value.respond_to?(:iso8601)
|
|
57
|
+
value.iso8601
|
|
58
|
+
elsif value.respond_to?(:to_h) && !value.is_a?(String)
|
|
59
|
+
normalize(value.to_h)
|
|
60
|
+
else
|
|
61
|
+
value.to_s
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def deep_sort(value)
|
|
67
|
+
case value
|
|
68
|
+
when Hash
|
|
69
|
+
value.keys.map(&:to_s).sort.each_with_object({}) do |k, out|
|
|
70
|
+
# Hash may have symbol keys after normalize we use strings
|
|
71
|
+
raw_key = value.key?(k) ? k : value.keys.find { |x| x.to_s == k }
|
|
72
|
+
out[k] = deep_sort(value[raw_key])
|
|
73
|
+
end
|
|
74
|
+
when Array
|
|
75
|
+
value.map { |v| deep_sort(v) }
|
|
76
|
+
else
|
|
77
|
+
value
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flycal
|
|
4
|
+
# Parses and resolves calendar selection options for CLI and HTTP API.
|
|
5
|
+
#
|
|
6
|
+
# Accepts:
|
|
7
|
+
# - a single string: "Work"
|
|
8
|
+
# - pipe/comma OR list: "Work|Personal" or "Work,Personal"
|
|
9
|
+
# - an array (e.g. repeated --calendar flags): ["Work", "Personal"]
|
|
10
|
+
# - mixed: ["Work|Gym", "Personal"]
|
|
11
|
+
module CalendarQuery
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Expand raw option value(s) into calendar reference strings.
|
|
15
|
+
def refs(value)
|
|
16
|
+
Array(value).flat_map { |entry| entry.to_s.split(/[|,]/) }
|
|
17
|
+
.map(&:strip)
|
|
18
|
+
.reject(&:empty?)
|
|
19
|
+
.uniq
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Resolve refs to calendar IDs against a list of calendar objects
|
|
23
|
+
# (must respond to #id / #summary, or hash with those keys).
|
|
24
|
+
def resolve_ids(calendars, value)
|
|
25
|
+
refs(value).filter_map { |ref| resolve_ref(calendars, ref) }.uniq
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resolve_ref(calendars, ref)
|
|
29
|
+
ref = ref.to_s.strip
|
|
30
|
+
return nil if ref.empty?
|
|
31
|
+
|
|
32
|
+
exact = calendars.find { |cal| calendar_id(cal) == ref }
|
|
33
|
+
return calendar_id(exact) if exact
|
|
34
|
+
|
|
35
|
+
exact_name = calendars.find { |cal| calendar_summary(cal).to_s.casecmp?(ref) }
|
|
36
|
+
return calendar_id(exact_name) if exact_name
|
|
37
|
+
|
|
38
|
+
needle = ref.downcase
|
|
39
|
+
partial = calendars.find do |cal|
|
|
40
|
+
summary = calendar_summary(cal).to_s
|
|
41
|
+
!summary.empty? && summary.downcase.include?(needle)
|
|
42
|
+
end
|
|
43
|
+
calendar_id(partial)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def calendar_id(cal)
|
|
47
|
+
return nil if cal.nil?
|
|
48
|
+
return cal.id if cal.respond_to?(:id)
|
|
49
|
+
|
|
50
|
+
cal[:id] || cal["id"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def calendar_summary(cal)
|
|
54
|
+
return nil if cal.nil?
|
|
55
|
+
return cal.summary if cal.respond_to?(:summary)
|
|
56
|
+
|
|
57
|
+
cal[:summary] || cal["summary"] || cal[:name] || cal["name"]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "google/apis/calendar_v3"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module Flycal
|
|
6
6
|
class CalendarService
|
|
7
7
|
def initialize(credentials)
|
|
8
8
|
@service = Google::Apis::CalendarV3::CalendarService.new
|
|
@@ -18,6 +18,10 @@ module FlycalCli
|
|
|
18
18
|
@service.get_calendar(calendar_id)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
def get_event(calendar_id, event_id)
|
|
22
|
+
@service.get_event(calendar_id, event_id)
|
|
23
|
+
end
|
|
24
|
+
|
|
21
25
|
def list_events(calendar_id, time_min:, time_max:, query: nil)
|
|
22
26
|
events = []
|
|
23
27
|
page_token = nil
|
|
@@ -25,8 +29,8 @@ module FlycalCli
|
|
|
25
29
|
loop do
|
|
26
30
|
result = @service.list_events(
|
|
27
31
|
calendar_id,
|
|
28
|
-
time_min: time_min
|
|
29
|
-
time_max: time_max
|
|
32
|
+
time_min: iso8601(time_min),
|
|
33
|
+
time_max: iso8601(time_max),
|
|
30
34
|
q: query,
|
|
31
35
|
single_events: true,
|
|
32
36
|
order_by: "startTime",
|
|
@@ -52,7 +56,7 @@ module FlycalCli
|
|
|
52
56
|
begin
|
|
53
57
|
events = list_events(cal_id, time_min: time_min, time_max: time_max, query: nil)
|
|
54
58
|
events.each { |e| all_events << { calendar_id: cal_id, event: e } }
|
|
55
|
-
rescue Google::Apis::
|
|
59
|
+
rescue Google::Apis::Error => e
|
|
56
60
|
warn "Error in calendar #{cal_id}: #{e.message}"
|
|
57
61
|
end
|
|
58
62
|
end
|
|
@@ -67,5 +71,45 @@ module FlycalCli
|
|
|
67
71
|
|
|
68
72
|
all_events
|
|
69
73
|
end
|
|
74
|
+
|
|
75
|
+
def create_event(calendar_id: "primary", summary:, start_time:, end_time:, all_day: false)
|
|
76
|
+
event = Google::Apis::CalendarV3::Event.new(summary: summary)
|
|
77
|
+
apply_event_times!(event, start_time: start_time, end_time: end_time, all_day: all_day)
|
|
78
|
+
@service.insert_event(calendar_id, event)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def update_event(calendar_id: "primary", event_id:, summary:, start_time:, end_time:, all_day: false)
|
|
82
|
+
event = get_event(calendar_id, event_id)
|
|
83
|
+
event.summary = summary if summary && !summary.to_s.empty?
|
|
84
|
+
apply_event_times!(event, start_time: start_time, end_time: end_time, all_day: all_day)
|
|
85
|
+
@service.update_event(calendar_id, event_id, event)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def iso8601(value)
|
|
91
|
+
return value.iso8601 if value.respond_to?(:iso8601)
|
|
92
|
+
return value.to_time.iso8601 if value.respond_to?(:to_time)
|
|
93
|
+
|
|
94
|
+
value.to_s
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def apply_event_times!(event, start_time:, end_time:, all_day:)
|
|
98
|
+
if all_day
|
|
99
|
+
event.start = Google::Apis::CalendarV3::EventDateTime.new(
|
|
100
|
+
date: Date.parse(start_time.to_s).to_s
|
|
101
|
+
)
|
|
102
|
+
event.end = Google::Apis::CalendarV3::EventDateTime.new(
|
|
103
|
+
date: Date.parse((end_time || start_time).to_s).to_s
|
|
104
|
+
)
|
|
105
|
+
else
|
|
106
|
+
event.start = Google::Apis::CalendarV3::EventDateTime.new(
|
|
107
|
+
date_time: DateTime.parse(start_time.to_s).rfc3339
|
|
108
|
+
)
|
|
109
|
+
event.end = Google::Apis::CalendarV3::EventDateTime.new(
|
|
110
|
+
date_time: DateTime.parse((end_time || start_time).to_s).rfc3339
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
70
114
|
end
|
|
71
115
|
end
|