flycal-cli 0.7.3 → 0.7.7
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 +105 -227
- data/lib/flycal_cli/calendar_service.rb +2 -5
- data/lib/flycal_cli/cli.rb +74 -148
- data/lib/flycal_cli/description_query.rb +32 -0
- data/lib/flycal_cli/mock/calendar_service.rb +43 -0
- data/lib/flycal_cli/mock/config.rb +171 -0
- data/lib/flycal_cli/mock/event_generator.rb +71 -0
- data/lib/flycal_cli/mock.rb +10 -0
- data/lib/flycal_cli/pipeline/aggregator.rb +229 -0
- data/lib/flycal_cli/pipeline/json_renderer.rb +142 -0
- data/lib/flycal_cli/pipeline/params.rb +46 -0
- data/lib/flycal_cli/pipeline/renderer.rb +27 -0
- data/lib/flycal_cli/pipeline/retriever.rb +72 -0
- data/lib/flycal_cli/pipeline/search_pipeline.rb +18 -0
- data/lib/flycal_cli/pipeline/text_renderer.rb +133 -0
- data/lib/flycal_cli/pipeline.rb +14 -0
- data/lib/flycal_cli/version.rb +1 -1
- data/lib/flycal_cli.rb +3 -0
- data/mcp/README.md +48 -9
- data/mcp/tools.json +322 -53
- data/mockTemplates/mock1.json +11 -0
- data/mocks/mock1.json +11 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db8d0ab49b0872b92c469825aff6a0a944f9012eed76c8bc0768bc82156de1f9
|
|
4
|
+
data.tar.gz: 16e758564b7adee8f7ea53b79c856865105afc26aa8489abe73313906f35ceb5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9485b33d254189f6f96d4c96f9e8867b047b35354fc60f63da86b8bd84612686afb559b6d5d937b98f41ac091777fd32a25ef1d0827a0cc6f4eb7f11be2287e5
|
|
7
|
+
data.tar.gz: c9b338282c8fa1ecc7a6b979d6c7c8632fcfe03ae598affdb27b98e603545dfb85e688d9d1180569bea2106094322fb9b664f48db89a731dade13d6c80ed2fe9
|
data/README.md
CHANGED
|
@@ -1,188 +1,48 @@
|
|
|
1
1
|
# flycal-cli
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/flycal-cli)
|
|
3
|
+
[](https://badge.fury.io/rb/flycal-cli)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Read your Google Calendar from the terminal: search events, find free slots, copy them into an email. **Read-only** — nothing is modified on the calendar.
|
|
6
6
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
- Ruby 3.1 or later
|
|
10
|
-
- A Google Cloud project with Calendar API enabled
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
7
|
+
## Quick start
|
|
13
8
|
|
|
14
9
|
```bash
|
|
15
10
|
gem install flycal-cli
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Or add to your Gemfile:
|
|
19
|
-
|
|
20
|
-
```ruby
|
|
21
|
-
gem "flycal-cli"
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Then run `bundle install`.
|
|
25
|
-
|
|
26
|
-
## Setup
|
|
27
|
-
|
|
28
|
-
Before using flycal, you need OAuth credentials from Google Cloud Console:
|
|
29
|
-
|
|
30
|
-
1. Go to [Google Cloud Console - Credentials](https://console.cloud.google.com/apis/credentials)
|
|
31
|
-
2. Create a project or select an existing one
|
|
32
|
-
3. Enable the **Google Calendar API** (APIs & Services → Library → search for "Google Calendar API")
|
|
33
|
-
4. Create **Desktop app** credentials (OAuth 2.0 Client IDs)
|
|
34
|
-
5. Add this URI as an authorized redirect:
|
|
35
|
-
```
|
|
36
|
-
http://127.0.0.1:9292/oauth2callback
|
|
37
|
-
```
|
|
38
|
-
6. Download the JSON file and save it as `~/.flycal/credentials.json`
|
|
39
|
-
|
|
40
|
-
## Commands
|
|
41
|
-
|
|
42
|
-
All commands support a per-invocation locale override:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
flycal search --locale it --in 7days
|
|
46
|
-
flycal slots --locale en --in "3 days" --duration 1h
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### login
|
|
50
|
-
|
|
51
|
-
Connect to your Google account. Opens a browser for OAuth authentication when not yet connected.
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
11
|
flycal login
|
|
55
12
|
```
|
|
56
13
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
### logout
|
|
60
|
-
|
|
61
|
-
Disconnect from your Google account and remove stored tokens.
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
flycal logout
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### update
|
|
68
|
-
|
|
69
|
-
Update `flycal-cli` to the latest published gem version.
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
flycal update
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### version
|
|
76
|
-
|
|
77
|
-
Show the current installed `flycal-cli` version.
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
flycal version
|
|
81
|
-
flycal --version
|
|
82
|
-
flycal -v
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### calendars
|
|
86
|
-
|
|
87
|
-
List available calendars with name and ID.
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
flycal calendars
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Example output:
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
Work user@example.com
|
|
97
|
-
Personal user@gmail.com
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### config
|
|
101
|
-
|
|
102
|
-
Interactive configuration menu.
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
flycal config
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Options:
|
|
109
|
-
|
|
110
|
-
- `calendar_default` — choose the default calendar and save it to `~/.flycal/config.yml`
|
|
111
|
-
- `exclude_calendars` — multi-select one or more calendars whose events block free slots (default selection includes the current default calendar)
|
|
112
|
-
- `edit config` — open `~/.flycal/config.yml` with `$EDITOR` (or `vi`)
|
|
14
|
+
During the test phase the project already provides the Google API credentials.
|
|
15
|
+
Open the browser, approve access, done.
|
|
113
16
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
The default calendar is used by the search command when no calendar is specified.
|
|
117
|
-
|
|
118
|
-
### search
|
|
119
|
-
|
|
120
|
-
Search for events in your calendar(s). Supports flexible date ranges and text filtering.
|
|
17
|
+
Optional but useful once:
|
|
121
18
|
|
|
122
19
|
```bash
|
|
123
|
-
flycal
|
|
124
|
-
flycal
|
|
125
|
-
flycal search -f 2025-03-01 -t 2025-03-31 -c "Work"
|
|
126
|
-
flycal search -i 2months -d placeholder
|
|
20
|
+
flycal config # set default calendar + calendars that block free slots
|
|
21
|
+
flycal calendars # list calendars (name + id)
|
|
127
22
|
```
|
|
128
23
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
- `--from` / `-f` — Start date/time. Default: midnight of current day. Formats: `YYYY-MM-DD`, locale forms (`DD-MM-YYYY` for `it`, `MM-DD-YYYY` for `en`, `/` or `-`), or with time `YYYY-MM-DDTHH:MM`
|
|
132
|
-
- `--to` / `-t` — End date/time. Default: 23:59 of the 30th day from today. Same formats as `--from`
|
|
133
|
-
- `--in` / `-i` — Duration from `--from`, overrides `--to`. Format: `30days`, `48hours`, `2months`, `1year` (no space). With space use quotes: `--in "30 days"`
|
|
134
|
-
- `--calendar` / `-c` — Calendar name or ID. Default: calendar set via `flycal config`
|
|
135
|
-
- `--description` / `-d` — Filter events by text. Matches events where the string appears in title or description (case-insensitive, contains)
|
|
136
|
-
|
|
137
|
-
**Time range behavior:**
|
|
24
|
+
## Common examples
|
|
138
25
|
|
|
139
|
-
|
|
140
|
-
- If `--in` is given: `--to` is ignored; the end time is computed from `--from` plus the duration
|
|
141
|
-
- Examples: `--in 30days`, `--in 48hours`, `--in 1months`, `--in 1year`
|
|
142
|
-
|
|
143
|
-
**Output:**
|
|
144
|
-
|
|
145
|
-
Each event is printed as: `Calendar | Start | End | Title`
|
|
146
|
-
|
|
147
|
-
The summary shows:
|
|
148
|
-
- From and To dates used
|
|
149
|
-
- Number of events found
|
|
150
|
-
- Total time occupied (hours, minutes, and working days based on 8-hour days)
|
|
151
|
-
|
|
152
|
-
For time frames longer than 7 days, a weekly breakdown is added (week number, start/end dates, hours, working days per week). For time frames longer than 30 days, a monthly breakdown is shown instead (month number, month name, hours, working days per month).
|
|
153
|
-
|
|
154
|
-
### slots
|
|
155
|
-
|
|
156
|
-
Find free time slots in your calendar. Output is a simple list for copy/paste into email or other tools.
|
|
26
|
+
**Find free slots (most useful day-to-day)**
|
|
157
27
|
|
|
158
28
|
```bash
|
|
159
29
|
flycal slots
|
|
30
|
+
flycal slots --in "5 days"
|
|
160
31
|
flycal slots --from monday --in "5 days"
|
|
161
|
-
flycal slots --from "next monday" --in "
|
|
162
|
-
flycal slots --from lunedi --in "5 days" --locale it
|
|
32
|
+
flycal slots --from "next monday" --in "2 weeks" --duration 1h
|
|
163
33
|
flycal slots --in "12 days" --template dinner
|
|
34
|
+
flycal slots --locale it --from lunedi --in "5 days"
|
|
164
35
|
```
|
|
165
36
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
- `--duration` — Slot length. Default: `slots.defaults.default_duration` in config (`45min`). Examples: `1h`, `30 minutes`
|
|
169
|
-
- `--from` / `-f` — Start date/time. Default: `slots.defaults.from` in config (`now`). Accepts:
|
|
170
|
-
- absolute: `YYYY-MM-DD`, locale dates (`DD-MM-YYYY` / `MM-DD-YYYY`), optional time
|
|
171
|
-
- relative: `now`, `today`/`oggi`, `tomorrow`/`domani`, `monday`/`lunedi`, `next monday`/`prossimo lunedi`, `last friday`/`scorso venerdi`
|
|
172
|
-
- `--in` / `-i` — Search window from `--from`. Default: `1 week`.
|
|
173
|
-
- `--template` / `-T` — Template from `slots.templates` in config. Default: first template (`work`)
|
|
174
|
-
- `--calendar` / `-c` — Calendar name or ID used as fallback when `exclude_calendars` is not configured
|
|
175
|
-
|
|
176
|
-
**Output:**
|
|
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):
|
|
177
38
|
|
|
178
39
|
```
|
|
179
40
|
found 6 slots
|
|
180
41
|
from sat 1 August 2026 to sat 8 August 2026
|
|
181
42
|
with duration 45min, template dinner
|
|
182
43
|
considering calendars
|
|
183
|
-
-
|
|
184
|
-
-
|
|
185
|
-
- antoniomolinari1977@gmail.com
|
|
44
|
+
- Work
|
|
45
|
+
- Personal
|
|
186
46
|
link: https://calendar.google.com/calendar/r/day/2026/8/1
|
|
187
47
|
|
|
188
48
|
saturday 1/8
|
|
@@ -192,9 +52,31 @@ monday 3/8
|
|
|
192
52
|
19 - 23
|
|
193
53
|
```
|
|
194
54
|
|
|
195
|
-
|
|
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
|
+
---
|
|
196
76
|
|
|
197
|
-
|
|
77
|
+
## Configuration (`~/.flycal/config.yml`)
|
|
78
|
+
|
|
79
|
+
Defaults are created automatically on first run. Edit with `flycal config` → `edit config`, or by hand.
|
|
198
80
|
|
|
199
81
|
```yaml
|
|
200
82
|
calendar_default: ~
|
|
@@ -202,27 +84,15 @@ locale: en
|
|
|
202
84
|
slots:
|
|
203
85
|
templates:
|
|
204
86
|
work:
|
|
205
|
-
days:
|
|
206
|
-
- 1
|
|
207
|
-
- 2
|
|
208
|
-
- 3
|
|
209
|
-
- 4
|
|
210
|
-
- 5
|
|
87
|
+
days: [1, 2, 3, 4, 5] # 1=Mon … 7=Sun
|
|
211
88
|
hours:
|
|
212
89
|
- 9:30-13:00
|
|
213
90
|
- 14:00-18:30
|
|
214
91
|
dinner:
|
|
215
|
-
days:
|
|
216
|
-
- 1
|
|
217
|
-
- 2
|
|
218
|
-
- 3
|
|
219
|
-
- 4
|
|
220
|
-
- 5
|
|
221
|
-
- 6
|
|
222
|
-
- 7
|
|
92
|
+
days: [1, 2, 3, 4, 5, 6, 7]
|
|
223
93
|
hours:
|
|
224
94
|
- 19-23
|
|
225
|
-
exclude_calendars: []
|
|
95
|
+
exclude_calendars: [] # calendars whose events block free slots
|
|
226
96
|
defaults:
|
|
227
97
|
from: now
|
|
228
98
|
default_duration: 45min
|
|
@@ -230,80 +100,88 @@ slots:
|
|
|
230
100
|
free_after: 15m
|
|
231
101
|
```
|
|
232
102
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
- `free_before` — buffer before each slot
|
|
242
|
-
- `free_after` — buffer after each slot; gaps must fit `duration + free_after`, output shows continuous free ranges
|
|
243
|
-
- `exclude_calendars` — calendars whose events block free slots; if empty, `calendar_default` is used
|
|
244
|
-
- `locale` supports `en` and `it` (default is `en`)
|
|
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 |
|
|
245
111
|
|
|
246
|
-
|
|
112
|
+
Interactive helpers:
|
|
247
113
|
|
|
114
|
+
```bash
|
|
115
|
+
flycal config
|
|
248
116
|
```
|
|
249
|
-
friday 15/7
|
|
250
|
-
10 - 12
|
|
251
|
-
13 - 15.30
|
|
252
117
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
```
|
|
118
|
+
- `calendar_default` — pick the default calendar
|
|
119
|
+
- `exclude_calendars` — multi-select calendars that block slots
|
|
120
|
+
- `edit config` — open the YAML in `$EDITOR`
|
|
257
121
|
|
|
258
|
-
|
|
122
|
+
---
|
|
259
123
|
|
|
260
|
-
|
|
124
|
+
## Parameters (reference)
|
|
261
125
|
|
|
262
|
-
|
|
263
|
-
|:-----|---------|
|
|
264
|
-
| `config.yml` | Default calendar ID and other settings |
|
|
265
|
-
| `credentials.json` | OAuth credentials (created manually from Google Cloud Console) |
|
|
266
|
-
| `tokens.yml` | Access tokens (managed automatically) |
|
|
126
|
+
### `flycal slots`
|
|
267
127
|
|
|
268
|
-
|
|
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` |
|
|
269
136
|
|
|
270
|
-
###
|
|
137
|
+
### `flycal search`
|
|
271
138
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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) |
|
|
275
152
|
|
|
276
|
-
|
|
277
|
-
gem build flycal-cli.gemspec
|
|
278
|
-
gem push flycal-cli-0.1.0.gem
|
|
279
|
-
```
|
|
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.
|
|
280
154
|
|
|
281
|
-
|
|
155
|
+
Relative dates work in English and Italian: `today`/`oggi`, `tomorrow`/`domani`, `monday`/`lunedi`, `next monday`/`prossimo lunedi`, `last friday`/`scorso venerdi`.
|
|
282
156
|
|
|
283
|
-
|
|
284
|
-
2. Build and push:
|
|
157
|
+
---
|
|
285
158
|
|
|
286
|
-
|
|
287
|
-
gem build flycal-cli.gemspec
|
|
288
|
-
gem push flycal-cli-X.Y.Z.gem
|
|
289
|
-
```
|
|
159
|
+
## Optional: your own Google Cloud credentials
|
|
290
160
|
|
|
291
|
-
|
|
161
|
+
**Not needed** for the default test API — `flycal login` is enough.
|
|
292
162
|
|
|
293
|
-
|
|
294
|
-
git tag vX.Y.Z
|
|
295
|
-
git push origin vX.Y.Z
|
|
296
|
-
```
|
|
163
|
+
Use this only if you want flycal to talk to **your own** Google Cloud OAuth client instead of the project-provided test credentials.
|
|
297
164
|
|
|
298
|
-
|
|
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`
|
|
299
171
|
|
|
300
|
-
|
|
172
|
+
You can also ask a teammate who manages the Cloud project to share that JSON with you.
|
|
301
173
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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) |
|
|
305
183
|
|
|
306
|
-
|
|
184
|
+
MCP catalog for agents: [`mcp/tools.json`](mcp/tools.json)
|
|
307
185
|
|
|
308
186
|
## License
|
|
309
187
|
|
|
@@ -57,14 +57,11 @@ module FlycalCli
|
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
# Filter:
|
|
60
|
+
# Filter: summary/description contains any OR term from query ("a|b"), case-insensitive.
|
|
61
61
|
if query && !query.to_s.strip.empty?
|
|
62
|
-
q = query.strip.downcase
|
|
63
62
|
all_events.select! do |item|
|
|
64
63
|
event = item[:event]
|
|
65
|
-
|
|
66
|
-
description = (event.description || "").downcase
|
|
67
|
-
summary.include?(q) || description.include?(q)
|
|
64
|
+
DescriptionQuery.match?(event.summary, event.description, query)
|
|
68
65
|
end
|
|
69
66
|
end
|
|
70
67
|
|