cal-invite 0.2.1 → 0.3.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/AGENTS.md +13 -8
- data/CACHING.md +190 -98
- data/CHANGELOG.md +61 -2
- data/CONFIGURATION.md +25 -29
- data/README.md +132 -190
- data/Rakefile +2 -1
- data/SECURITY.md +39 -10
- data/lib/cal_invite/configuration.rb +20 -11
- data/lib/cal_invite/event.rb +87 -114
- data/lib/cal_invite/ical_timezone.rb +64 -18
- data/lib/cal_invite/providers/base_provider.rb +302 -264
- data/lib/cal_invite/providers/ical.rb +7 -152
- data/lib/cal_invite/providers/ics.rb +68 -13
- data/lib/cal_invite/providers/ics_content.rb +7 -129
- data/lib/cal_invite/providers.rb +8 -0
- data/lib/cal_invite/version.rb +1 -1
- data/lib/cal_invite.rb +1 -8
- metadata +4 -9
- data/.DS_Store +0 -0
- data/CLAUDE.md +0 -49
- data/lib/.DS_Store +0 -0
- data/lib/cal_invite/.DS_Store +0 -0
- data/lib/cal_invite/caching.rb +0 -74
- data/lib/tasks/rdoc.rake +0 -13
data/CONFIGURATION.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Complete reference for every configurable option in CalInvite: `Event` attributes, providers, the `generate_calendar_url` call, ICS/email-invite delivery, and global `CalInvite.configure` settings.
|
|
4
4
|
|
|
5
|
-
For caching-specific detail, see [CACHING.md](CACHING.md).
|
|
6
|
-
|
|
7
5
|
## Table of Contents
|
|
8
6
|
|
|
9
7
|
- [Event attributes](#event-attributes)
|
|
@@ -22,23 +20,23 @@ For caching-specific detail, see [CACHING.md](CACHING.md).
|
|
|
22
20
|
|
|
23
21
|
## Event attributes
|
|
24
22
|
|
|
25
|
-
All attributes are set via `CalInvite::Event.new(attributes)` or `event.update_attributes(attributes)
|
|
23
|
+
All attributes are set via `CalInvite::Event.new(attributes)` or `event.update_attributes(attributes)` (symbol or string keys). Any key not listed below raises `ArgumentError` (`Unknown attribute(s): ...`); `update_attributes` checks all keys before changing anything.
|
|
26
24
|
|
|
27
25
|
| Attribute | Type | Required | Default | Notes |
|
|
28
26
|
|------------------------|-----------------|----------------------------------------|---------|-------|
|
|
29
27
|
| `title` | `String` | yes | — | Non-blank. |
|
|
30
28
|
| `start_time` | `Time` | yes, unless `all_day` or using `multi_day_sessions` | — | Always pass UTC. |
|
|
31
|
-
| `end_time` | `Time` | yes, unless `all_day` or using `multi_day_sessions` | — | Always pass UTC. |
|
|
29
|
+
| `end_time` | `Time` | yes, unless `all_day` or using `multi_day_sessions` | — | Always pass UTC. Must be after `start_time` (`ArgumentError` otherwise); all-day events may end on the same date they start. |
|
|
32
30
|
| `description` | `String` | no | `nil` | Plain text; combined with `notes` in provider output. |
|
|
33
31
|
| `location` | `String` | no | `nil` | Physical location only. Kept separate from `url` so each provider formats it correctly. |
|
|
34
32
|
| `url` | `String` | no | `nil` | Virtual meeting link (Zoom, Meet, Teams, etc). Kept separate from `location`. |
|
|
35
33
|
| `attendees` | `Array<String, Hash>` | no | `nil` | Email strings, or `{ email:, name:, partstat:, rsvp: }` hashes for a display name (`CN=`), a specific RSVP status, and/or an explicit `RSVP=` override. Only emitted if `show_attendees` is `true`. `partstat` is one of `:accepted`, `:declined`, `:tentative`, `:needs_action` (default), `:delegated`. `rsvp:` defaults to `true` for `:request`/`:cancel`/`:publish` and `false` for `:reply`/`:counter`/`:decline_counter`; set it explicitly to override (e.g. `rsvp: false` on an already-`:accepted` attendee in a registration-confirmation invite — see [Replicating a ticketing-platform confirmation email](#replicating-a-ticketing-platform-confirmation-email)). |
|
|
36
34
|
| `show_attendees` | `Boolean` | no | `false` | Gate for including `attendees` in generated output. |
|
|
37
|
-
| `organizer` | `Hash`
|
|
38
|
-
| `timezone` | `String` | no | `'UTC'` | Controls display/formatting only — does not affect how `start_time`/`end_time` are interpreted.
|
|
35
|
+
| `organizer` | `String`/`Hash` | no, but required for `method: :request` | `nil` | `"jane@example.com"` or `{ name: "Jane Doe", email: "jane@example.com" }`. `name` is optional. See [Email meeting invites](#email-meeting-invites-rsvp-capable). |
|
|
36
|
+
| `timezone` | `String` | no | `CalInvite.configuration.timezone` (`'UTC'`) | Controls display/formatting only — does not affect how `start_time`/`end_time` are interpreted. Must be `'UTC'` or an IANA identifier (e.g. `'America/New_York'`), which produces a correctly converted `DTSTART;TZID=...` plus a full `VTIMEZONE` block; anything else raises `ArgumentError`. See [Timezones and VTIMEZONE](#timezones-and-vtimezone). |
|
|
39
37
|
| `notes` | `String` | no | `nil` | Appended to the description. |
|
|
40
38
|
| `all_day` | `Boolean` | no | `false` | When `true`, `start_time`/`end_time` validation is skipped. |
|
|
41
|
-
| `multi_day_sessions` | `Array<Hash>` | no | `[]` | `[{ start_time:, end_time: }, ...]`. Used instead of `start_time`/`end_time` for multi-session events. |
|
|
39
|
+
| `multi_day_sessions` | `Array<Hash>` | no | `[]` | `[{ start_time:, end_time: }, ...]`. Used instead of `start_time`/`end_time` for multi-session events. Each session needs both times, with `end_time` after `start_time`. `:ics`/`:ical` emit one `VEVENT` per session and `:yahoo` one URL per session (newline-separated); `:google`/`:outlook`/`:office365` can only express one range, so they use `start_time`/`end_time`, which default to the earliest session start and latest session end (explicit values win). |
|
|
42
40
|
| `uid` | `String` | no | randomly generated, memoized per `Event` instance | Stable RFC 5545 identifier. **Must** be reused across calls when you send an update (`:request`) or cancellation (`:cancel`) for a previously sent invite — see [Updating and cancelling invites](#updating-and-cancelling-invites). |
|
|
43
41
|
| `sequence` | `Integer` | no | `0` | RFC 5545 SEQUENCE. Increment it yourself each time you resend a `:request`/`:cancel` for the same `uid`. |
|
|
44
42
|
| `geo` | `Array<Float>`, `Hash` | no | `nil` | `[37.4595, -122.1418]` or `{ lat:, lng: }`. Emits `GEO:lat;lng` — lets Apple/Google Maps deep-link from the invite. |
|
|
@@ -88,7 +86,7 @@ Pass one of these symbols to `generate_calendar_url`:
|
|
|
88
86
|
|
|
89
87
|
`CalInvite::Providers::SUPPORTED_PROVIDERS` holds the full list programmatically.
|
|
90
88
|
|
|
91
|
-
`CalInvite::Providers::IcsContent` is also available directly (not via `generate_calendar_url`) if you want raw `.ics` content without
|
|
89
|
+
`CalInvite::Providers::IcsContent` is also available directly (not via `generate_calendar_url`) if you want raw `.ics` content without `Event#generate_calendar_url`'s validation — see [ICS content and downloads](#ics-content-and-downloads).
|
|
92
90
|
|
|
93
91
|
## `generate_calendar_url`
|
|
94
92
|
|
|
@@ -98,20 +96,26 @@ event.generate_calendar_url(provider, method: :publish)
|
|
|
98
96
|
|
|
99
97
|
| Param | Type | Default | Notes |
|
|
100
98
|
|-----------|----------|------------|-------|
|
|
101
|
-
| `provider`| `Symbol` | required | One of the provider symbols above. |
|
|
99
|
+
| `provider`| `Symbol` | required | One of the provider symbols above (a String also works). Anything else raises `ArgumentError` listing the supported providers. `:ics_content` is accepted as a legacy alias of `:ics`. |
|
|
102
100
|
| `method` | `Symbol` | `:publish` | `:publish`, `:request`, `:cancel`, `:reply`, `:counter`, or `:decline_counter`. Only honored by `:ics`/`:ical` — ignored by URL-based providers. `:request` requires `organizer` to be set on the event; see below. `:cancel` requires reusing the original `uid` — see [Updating and cancelling invites](#updating-and-cancelling-invites). `:reply`/`:counter`/`:decline_counter` omit `RSVP=TRUE` on `ATTENDEE` lines — see [Attendee RSVP status and METHOD:REPLY](#attendee-rsvp-status-and-methodreply) and [Attendee-proposed reschedules (COUNTER)](#attendee-proposed-reschedules-counter). Note: `:decline_counter` renders as `METHOD:DECLINECOUNTER` (one word, per RFC 5545). |
|
|
103
101
|
|
|
104
|
-
|
|
102
|
+
Output is generated fresh on every call. CalInvite doesn't cache anything: building the output takes microseconds, and a cached `.ics` would keep a stale `DTSTAMP`. If you want caching anyway, do it in your app, keyed on whatever identifies the content and changes with it (more examples, including HTTP caching, in [CACHING.md](CACHING.md)):
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
Rails.cache.fetch([meeting, meeting.sequence, :ics]) do
|
|
106
|
+
meeting.to_cal_invite_event.generate_calendar_url(:ics, method: :request)
|
|
107
|
+
end
|
|
108
|
+
```
|
|
105
109
|
|
|
106
110
|
## ICS content and downloads
|
|
107
111
|
|
|
108
112
|
Two ways to get raw `.ics` content:
|
|
109
113
|
|
|
110
114
|
```ruby
|
|
111
|
-
# Via Event (goes through validation
|
|
115
|
+
# Via Event (goes through validation)
|
|
112
116
|
content = event.generate_calendar_url(:ics, method: :publish)
|
|
113
117
|
|
|
114
|
-
# Directly via the provider (
|
|
118
|
+
# Directly via the provider (skips Event#validate!)
|
|
115
119
|
content = CalInvite::Providers::Ics.new(event, method: :publish).generate
|
|
116
120
|
```
|
|
117
121
|
|
|
@@ -331,10 +335,8 @@ A working reference implementation of this whole pattern — a `Meeting` model
|
|
|
331
335
|
persisting `uid`/`sequence`, and controller actions that send/reschedule/cancel
|
|
332
336
|
— lives in [`Example/calendar_app`](Example/calendar_app): see
|
|
333
337
|
`app/models/meeting.rb` and `app/controllers/meetings_controller.rb`. It
|
|
334
|
-
depends on the attendee-hash/`uid`/`sequence` features documented here,
|
|
335
|
-
|
|
336
|
-
tracks the published gem, so point it at `path: "../.."` locally if you want
|
|
337
|
-
to run this demo before that release is out.
|
|
338
|
+
depends on the attendee-hash/`uid`/`sequence` features documented here, which
|
|
339
|
+
shipped in 0.2.0, so it runs against the published gem.
|
|
338
340
|
|
|
339
341
|
## Tracking RSVPs: what the gem does and doesn't do
|
|
340
342
|
|
|
@@ -506,20 +508,17 @@ For any `timezone` other than `'UTC'` that's a recognized IANA/Olson identifier
|
|
|
506
508
|
timestamps with no `VTIMEZONE` block, which every calendar client understands
|
|
507
509
|
unambiguously.
|
|
508
510
|
|
|
509
|
-
|
|
510
|
-
`
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
511
|
+
Any other value — including raw offset strings like `'+01:00'` — raises
|
|
512
|
+
`ArgumentError` from `Event.new`/`update_attributes`: there's no timezone
|
|
513
|
+
database entry to convert against or build a `VTIMEZONE` from, and emitting a
|
|
514
|
+
`TZID` with no matching `VTIMEZONE` is invalid RFC 5545. Use the IANA name for
|
|
515
|
+
the place instead (e.g. `'Europe/Paris'`), or `'UTC'`.
|
|
514
516
|
|
|
515
517
|
## Global configuration (`CalInvite.configure`)
|
|
516
518
|
|
|
517
519
|
```ruby
|
|
518
520
|
# config/initializers/cal_invite.rb
|
|
519
521
|
CalInvite.configure do |config|
|
|
520
|
-
config.cache_store = Rails.cache # or :memory_store, :null_store, or a custom read/write/delete object
|
|
521
|
-
config.cache_prefix = 'my_app_cal_invite'
|
|
522
|
-
config.cache_expires_in = 3600 # seconds
|
|
523
522
|
config.webhook_secret = ENV['CAL_INVITE_WEBHOOK_SECRET']
|
|
524
523
|
config.timezone = 'UTC'
|
|
525
524
|
end
|
|
@@ -527,10 +526,7 @@ end
|
|
|
527
526
|
|
|
528
527
|
| Option | Type | Default | Notes |
|
|
529
528
|
|---------------------|-----------------------------------------|----------------|-------|
|
|
530
|
-
| `cache_store` | `:memory_store`, `:null_store`, or any object implementing `read`/`write`/`delete` | `nil` (caching disabled) | See [CACHING.md](CACHING.md) for full detail. |
|
|
531
|
-
| `cache_prefix` | `String` | `'cal_invite'` | Namespaces cache keys. |
|
|
532
|
-
| `cache_expires_in` | `Integer` (seconds) | `86400` (24h) | |
|
|
533
529
|
| `webhook_secret` | `String` | `nil` | Reserved for webhook signature verification in consuming apps. |
|
|
534
|
-
| `timezone` | `String` | `'UTC'` |
|
|
530
|
+
| `timezone` | `String` | `'UTC'` | Default `timezone` for events created without one; an explicit per-event `timezone:` wins. Validated like any event timezone, so an unknown value makes `Event.new` raise `ArgumentError`. |
|
|
535
531
|
|
|
536
|
-
|
|
532
|
+
`cache_store`, `cache_prefix` and `cache_expires_in` are deprecated and have no effect. Setting one prints a warning, and they'll be removed in 0.4.0, so delete them from your initializer.
|
data/README.md
CHANGED
|
@@ -1,268 +1,210 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CalInvite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
4
|
|
|
5
|
-
|
|
6
|
-

|
|
5
|
+
Generate "add to calendar" links and `.ics` calendar invites from Ruby, for Google Calendar, Outlook, Outlook 365, Yahoo, Apple Calendar, and any client that reads iCalendar files.
|
|
7
6
|
|
|
7
|
+
[](https://rubygems.org/gems/cal-invite)
|
|
8
|
+

|
|
8
9
|

|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- Ruby >= 3.0.0
|
|
13
|
-
- Rails 6.0, 6.1, 7.0, 7.1, 8.0, 8.1
|
|
11
|
+
You describe an event once and get a link or `.ics` content for each provider. With `method: :request`, the `.ics` is an RFC 5545 meeting request that Gmail, Outlook and Apple Mail show with Accept/Decline buttons, and later updates or cancellations replace the original invite. Any IANA timezone gets a full `VTIMEZONE` with correctly converted times. Attribute values are escaped, so end-user input can't add properties to an invite.
|
|
14
12
|
|
|
15
|
-
##
|
|
13
|
+
## Contents
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- Thunderbird Calendar
|
|
29
|
-
- Zoho Calendar
|
|
30
|
-
- Microsoft Teams Calendar
|
|
31
|
-
- Zoom Calendar Integration
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Quick start](#quick-start)
|
|
17
|
+
- [Providers](#providers)
|
|
18
|
+
- [More examples](#more-examples)
|
|
19
|
+
- [Serving a downloadable .ics file](#serving-a-downloadable-ics-file)
|
|
20
|
+
- [Sending email invites with RSVP](#sending-email-invites-with-rsvp)
|
|
21
|
+
- [Things to know](#things-to-know)
|
|
22
|
+
- [Upgrading to 0.3.0](#upgrading-to-030)
|
|
23
|
+
- [Documentation](#documentation)
|
|
24
|
+
- [Development](#development)
|
|
25
|
+
- [Sponsor](#sponsor)
|
|
32
26
|
|
|
33
27
|
## Installation
|
|
34
28
|
|
|
35
|
-
|
|
29
|
+
CalInvite is published on [RubyGems](https://rubygems.org/gems/cal-invite). It requires Ruby 3.0 or newer. It works with or without Rails, and supports Rails 6.0 through 8.1.
|
|
36
30
|
|
|
37
31
|
```ruby
|
|
38
|
-
|
|
32
|
+
# Gemfile
|
|
33
|
+
gem "cal-invite"
|
|
39
34
|
```
|
|
40
35
|
|
|
41
|
-
And then execute:
|
|
42
36
|
```bash
|
|
43
|
-
|
|
37
|
+
bundle install
|
|
44
38
|
```
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
require "cal_invite"
|
|
44
|
+
|
|
45
|
+
event = CalInvite::Event.new(
|
|
46
|
+
title: "Team Meeting",
|
|
47
|
+
start_time: Time.utc(2026, 11, 2, 15, 0), # always pass UTC
|
|
48
|
+
end_time: Time.utc(2026, 11, 2, 16, 0),
|
|
49
|
+
timezone: "America/New_York", # how the time is displayed
|
|
50
|
+
description: "Weekly sync",
|
|
51
|
+
location: "Conference Room A", # physical place
|
|
52
|
+
url: "https://zoom.us/j/123456789" # virtual meeting link
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
event.generate_calendar_url(:google) # => "https://calendar.google.com/calendar/render?action=TEMPLATE&..."
|
|
56
|
+
event.generate_calendar_url(:ics) # => "BEGIN:VCALENDAR\r\n..." (.ics file content)
|
|
49
57
|
```
|
|
50
58
|
|
|
51
|
-
##
|
|
59
|
+
## Providers
|
|
52
60
|
|
|
53
|
-
|
|
61
|
+
| Provider | Returns | Notes |
|
|
62
|
+
|--------------|---------|-------|
|
|
63
|
+
| `:google` | URL | Opens Google Calendar's "add event" page. |
|
|
64
|
+
| `:outlook` | URL | Outlook.com (personal accounts). |
|
|
65
|
+
| `:office365` | URL | Outlook on the web for work/school (Microsoft 365) accounts. |
|
|
66
|
+
| `:yahoo` | URL | One URL per session for multi-day events, separated by newlines. |
|
|
67
|
+
| `:ics` | `.ics` content | Standard iCalendar file; works with Apple Calendar, Thunderbird, Proton, Fastmail and others. |
|
|
68
|
+
| `:ical` | `.ics` content | Same content as `:ics`; kept for Apple Calendar naming. |
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
- Always provide times in UTC
|
|
57
|
-
- Use the timezone parameter to specify the display timezone
|
|
58
|
-
- Location and URL are handled separately for better calendar integration
|
|
70
|
+
Only `:ics` and `:ical` support the `method:` option (`:publish` by default, also `:request`, `:cancel`, `:reply`, `:counter`, `:decline_counter`). Any other provider name raises `ArgumentError`.
|
|
59
71
|
|
|
60
|
-
|
|
72
|
+
## More examples
|
|
61
73
|
|
|
62
74
|
```ruby
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
title: "
|
|
66
|
-
start_time: Time.
|
|
67
|
-
|
|
68
|
-
description: "Weekly team sync",
|
|
69
|
-
location: "Conference Room A", # Physical location
|
|
70
|
-
timezone: "America/New_York", # Display timezone
|
|
71
|
-
attendees: ["person@example.com"],
|
|
72
|
-
show_attendees: true,
|
|
73
|
-
notes: "Please bring your laptop"
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
# Create an event with both physical and virtual locations
|
|
77
|
-
event = CalInvite::Event.new(
|
|
78
|
-
title: "Hybrid Meeting",
|
|
79
|
-
start_time: Time.current.utc,
|
|
80
|
-
end_time: Time.current.utc + 2.hours,
|
|
81
|
-
description: "Weekly team sync",
|
|
82
|
-
location: "Conference Room A", # Physical location
|
|
83
|
-
url: "https://zoom.us/j/123456789", # Virtual meeting URL
|
|
84
|
-
timezone: "America/New_York",
|
|
85
|
-
attendees: ["person@example.com"],
|
|
75
|
+
# Attendees: email strings, or hashes with a display name and RSVP status
|
|
76
|
+
CalInvite::Event.new(
|
|
77
|
+
title: "Planning",
|
|
78
|
+
start_time: Time.utc(2026, 11, 2, 15), end_time: Time.utc(2026, 11, 2, 16),
|
|
79
|
+
attendees: ["bob@example.com", { email: "ada@example.com", name: "Ada", partstat: :accepted }],
|
|
86
80
|
show_attendees: true
|
|
87
81
|
)
|
|
88
82
|
|
|
89
83
|
# All-day event
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
CalInvite::Event.new(title: "Company Offsite", start_time: Time.utc(2026, 12, 4), all_day: true)
|
|
85
|
+
|
|
86
|
+
# Recurring event, with a reminder 15 minutes before
|
|
87
|
+
CalInvite::Event.new(
|
|
88
|
+
title: "Standup",
|
|
89
|
+
start_time: Time.utc(2026, 11, 2, 14), end_time: Time.utc(2026, 11, 2, 14, 15),
|
|
90
|
+
timezone: "Europe/Paris",
|
|
91
|
+
rrule: "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR",
|
|
92
|
+
reminders: [15]
|
|
96
93
|
)
|
|
97
94
|
|
|
98
|
-
# Multi-day event
|
|
99
|
-
|
|
95
|
+
# Multi-day event with separate sessions
|
|
96
|
+
CalInvite::Event.new(
|
|
100
97
|
title: "Training Workshop",
|
|
101
98
|
multi_day_sessions: [
|
|
102
|
-
{
|
|
103
|
-
|
|
104
|
-
end_time: Time.parse("2024-03-01 17:00:00 UTC")
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
start_time: Time.parse("2024-03-02 09:00:00 UTC"),
|
|
108
|
-
end_time: Time.parse("2024-03-02 13:00:00 UTC")
|
|
109
|
-
}
|
|
99
|
+
{ start_time: Time.utc(2026, 3, 1, 13), end_time: Time.utc(2026, 3, 1, 22) },
|
|
100
|
+
{ start_time: Time.utc(2026, 3, 2, 14), end_time: Time.utc(2026, 3, 2, 18) }
|
|
110
101
|
],
|
|
111
|
-
description: "Advanced Ruby Training",
|
|
112
|
-
location: "Training Center",
|
|
113
|
-
url: "https://zoom.us/j/123456789", # Virtual meeting URL kept separate
|
|
114
102
|
timezone: "America/New_York",
|
|
115
|
-
|
|
103
|
+
location: "Training Center"
|
|
116
104
|
)
|
|
117
|
-
|
|
118
|
-
# Generate calendar URLs
|
|
119
|
-
ical_url = event.generate_calendar_url(:ical)
|
|
120
|
-
google_url = event.generate_calendar_url(:google)
|
|
121
|
-
outlook_url = event.generate_calendar_url(:outlook)
|
|
122
|
-
outlook365_url = event.generate_calendar_url(:office365)
|
|
123
|
-
yahoo_url = event.generate_calendar_url(:yahoo)
|
|
124
105
|
```
|
|
125
106
|
|
|
126
|
-
|
|
107
|
+
For a multi-day event, `:ics`, `:ical` and `:yahoo` produce one entry per session. Google and Outlook can only show one range, so they show the whole span, from the first session's start to the last session's end.
|
|
127
108
|
|
|
128
|
-
|
|
109
|
+
Every attribute (`organizer`, `geo`, `busy`, `visibility`, `importance`, `calendar_name`, ...) is listed in [CONFIGURATION.md](CONFIGURATION.md#event-attributes).
|
|
129
110
|
|
|
130
|
-
|
|
131
|
-
2. Generate the ICS content
|
|
132
|
-
3. Send the file to the user
|
|
133
|
-
|
|
134
|
-
Here's a basic example of the controller logic:
|
|
111
|
+
## Serving a downloadable .ics file
|
|
135
112
|
|
|
136
113
|
```ruby
|
|
137
|
-
#
|
|
138
|
-
def
|
|
139
|
-
event =
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
filename = "#{event.title.downcase.gsub(/[^0-9A-Za-z.\-]/, '_')}_#{Time.now.strftime('%Y%m%d')}.ics"
|
|
143
|
-
|
|
144
|
-
send_data(
|
|
145
|
-
content,
|
|
146
|
-
filename: filename,
|
|
147
|
-
type: 'text/calendar; charset=UTF-8',
|
|
148
|
-
disposition: 'attachment'
|
|
114
|
+
# app/controllers/events_controller.rb
|
|
115
|
+
def calendar
|
|
116
|
+
event = CalInvite::Event.new(...)
|
|
117
|
+
download = CalInvite::Providers::IcsDownload.wrap_for_download(
|
|
118
|
+
event.generate_calendar_url(:ics), event.title
|
|
149
119
|
)
|
|
120
|
+
|
|
121
|
+
send_data download[:content],
|
|
122
|
+
type: download[:headers]["Content-Type"],
|
|
123
|
+
filename: "#{CalInvite::Providers::IcsDownload.sanitize_filename(event.title)}.ics",
|
|
124
|
+
disposition: "attachment"
|
|
150
125
|
end
|
|
151
126
|
```
|
|
152
127
|
|
|
153
|
-
|
|
128
|
+
## Sending email invites with RSVP
|
|
154
129
|
|
|
155
|
-
|
|
130
|
+
A plain `.ics` attachment opens as a file. To have mail clients show it as a meeting invite with Accept/Decline buttons, you need three things: an `organizer`, `method: :request`, and a matching `method=REQUEST` on the attachment's content type.
|
|
156
131
|
|
|
157
|
-
By default, a `.ics` attachment opens as a file in most mail clients. To get
|
|
158
|
-
Gmail/Outlook/Apple Mail to render the message as an invite with Accept/Decline
|
|
159
|
-
actions (like Luma or Google Calendar invites do), set an `organizer` on the
|
|
160
|
-
event and pass `method: :request` when generating. Full walkthrough and all
|
|
161
|
-
options in [CONFIGURATION.md](CONFIGURATION.md#email-meeting-invites-rsvp-capable).
|
|
162
|
-
|
|
163
|
-
### ICS File Generation
|
|
164
|
-
|
|
165
|
-
The gem provides two ways to generate ICS files:
|
|
166
|
-
|
|
167
|
-
1. Direct content generation:
|
|
168
132
|
```ruby
|
|
169
133
|
event = CalInvite::Event.new(
|
|
170
|
-
title: "
|
|
171
|
-
start_time: Time.
|
|
172
|
-
|
|
173
|
-
|
|
134
|
+
title: "Design Review",
|
|
135
|
+
start_time: Time.utc(2026, 11, 2, 15), end_time: Time.utc(2026, 11, 2, 16),
|
|
136
|
+
organizer: { name: "Jane Doe", email: "jane@example.com" },
|
|
137
|
+
attendees: ["guest@example.com"],
|
|
138
|
+
show_attendees: true
|
|
174
139
|
)
|
|
175
140
|
|
|
176
|
-
#
|
|
177
|
-
|
|
141
|
+
# In your ActionMailer method:
|
|
142
|
+
attachments["invite.ics"] = {
|
|
143
|
+
mime_type: "text/calendar; method=REQUEST",
|
|
144
|
+
content: event.generate_calendar_url(:ics, method: :request)
|
|
145
|
+
}
|
|
178
146
|
```
|
|
179
147
|
|
|
180
|
-
|
|
181
|
-
```ruby
|
|
182
|
-
# In your controller
|
|
183
|
-
def download_ics
|
|
184
|
-
event = CalInvite::Event.new(
|
|
185
|
-
title: "Meeting",
|
|
186
|
-
start_time: Time.current.utc,
|
|
187
|
-
end_time: Time.current.utc + 1.hour,
|
|
188
|
-
timezone: "America/New_York"
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
content = CalInvite::Providers::Ics.new(event).generate
|
|
192
|
-
filename = "#{event.title.downcase.gsub(/[^0-9A-Za-z.\-]/, '_')}_#{Time.now.strftime('%Y%m%d')}.ics"
|
|
193
|
-
|
|
194
|
-
send_data(
|
|
195
|
-
content,
|
|
196
|
-
filename: filename,
|
|
197
|
-
type: 'text/calendar; charset=UTF-8',
|
|
198
|
-
disposition: 'attachment'
|
|
199
|
-
)
|
|
200
|
-
end
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### Important Notes
|
|
204
|
-
|
|
205
|
-
1. Time Handling:
|
|
206
|
-
- Always provide times in UTC to the Event constructor
|
|
207
|
-
- Use the timezone parameter to specify the display timezone
|
|
208
|
-
- All-day events should use beginning_of_day.utc and end_of_day.utc
|
|
148
|
+
Save `event.uid`. To reschedule or cancel later, rebuild the event with the same `uid` and a higher `sequence`, then send `method: :request` again (for a reschedule) or `method: :cancel`. See [Updating and cancelling invites](CONFIGURATION.md#updating-and-cancelling-invites). The [example Rails app](Example/calendar_app) has a working implementation.
|
|
209
149
|
|
|
210
|
-
|
|
211
|
-
- Physical location goes in the `location` parameter
|
|
212
|
-
- Virtual meeting URL goes in the `url` parameter
|
|
213
|
-
- They are handled separately for better calendar integration
|
|
150
|
+
## Things to know
|
|
214
151
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
152
|
+
- Pass `start_time` and `end_time` as UTC `Time`s. `timezone` only controls how the time is displayed. It must be `"UTC"` or an IANA name such as `"America/New_York"`, and defaults to `CalInvite.configuration.timezone` (`"UTC"`).
|
|
153
|
+
- Put the physical place in `location` and the video-call link in `url`. Each provider formats them its own way.
|
|
154
|
+
- Invalid input raises `ArgumentError`: a misspelled attribute (`locaton:`), an unknown timezone, an end time before the start time, or an unsupported provider.
|
|
155
|
+
- CalInvite generates links and invites but doesn't receive RSVP replies. It also can't set options that exist only in the Google and Microsoft APIs, such as guest permissions. See [Tracking RSVPs](CONFIGURATION.md#tracking-rsvps-what-the-gem-does-and-doesnt-do).
|
|
156
|
+
- Output is cheap to build, so nothing is cached and it's generated fresh on every call. To cache it in your app, or answer repeat downloads with `304 Not Modified`, see [CACHING.md](CACHING.md).
|
|
219
157
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
CalInvite includes built-in caching support to improve performance when generating calendar URLs. To enable caching in your Rails application:
|
|
158
|
+
Global settings:
|
|
223
159
|
|
|
224
160
|
```ruby
|
|
225
161
|
# config/initializers/cal_invite.rb
|
|
226
162
|
CalInvite.configure do |config|
|
|
227
|
-
#
|
|
228
|
-
config.cache_store = Rails.cache
|
|
229
|
-
|
|
230
|
-
# Optional: Set cache prefix
|
|
231
|
-
config.cache_prefix = 'my_app_cal_invite'
|
|
232
|
-
|
|
233
|
-
# Optional: Set expiration time (in seconds)
|
|
234
|
-
config.cache_expires_in = 3600 # 1 hour
|
|
163
|
+
config.timezone = "America/New_York" # default for events that don't set one
|
|
235
164
|
end
|
|
236
165
|
```
|
|
237
166
|
|
|
238
|
-
|
|
167
|
+
## Upgrading to 0.3.0
|
|
239
168
|
|
|
240
|
-
|
|
169
|
+
0.3.0 is stricter. Unknown attributes, invalid timezones and invalid time ranges now raise `ArgumentError`, where 0.2.x silently produced a broken invite. The gem also no longer loads all of `active_support/core_ext`. In plain Ruby, `1.hour` and `Time.current` no longer work unless you require them yourself.
|
|
241
170
|
|
|
242
|
-
|
|
171
|
+
The `cache_store`/`cache_prefix`/`cache_expires_in` settings and the top-level `::BaseProvider` constant are deprecated. They still work, but print a warning, and they'll be removed in 0.4.0. The full list of changes is in [CHANGELOG.md](CHANGELOG.md).
|
|
243
172
|
|
|
244
|
-
|
|
173
|
+
## Documentation
|
|
245
174
|
|
|
246
|
-
|
|
175
|
+
- [CONFIGURATION.md](CONFIGURATION.md): every attribute, provider and option, plus guides for email invites, updates and cancellations, RSVP status, COUNTER proposals, and timezones.
|
|
176
|
+
- [API reference](https://dashapi-ai.github.io/cal-invite/): generated from the YARD comments (`bundle exec rake rdoc` builds it locally).
|
|
177
|
+
- [CACHING.md](CACHING.md): caching output in plain Ruby, Rails, Sinatra, Hanami and Grape, including HTTP caching for `.ics` feeds.
|
|
178
|
+
- [CHANGELOG.md](CHANGELOG.md): release history.
|
|
247
179
|
|
|
248
|
-
|
|
180
|
+
## Development
|
|
249
181
|
|
|
250
|
-
|
|
182
|
+
```bash
|
|
183
|
+
bin/setup # install dependencies
|
|
184
|
+
bundle exec rake test # full suite (+ coverage report in coverage/)
|
|
185
|
+
bundle exec rake test TEST=test/cal_invite/event_test.rb # one file
|
|
186
|
+
bin/console # IRB with the local lib/ loaded
|
|
187
|
+
```
|
|
251
188
|
|
|
252
|
-
|
|
189
|
+
To try unreleased changes in the [example Rails app](Example/calendar_app), switch its Gemfile to the commented-out `gem "cal-invite", path: "../../"` line and run `bundle install`. Switch it back before committing.
|
|
253
190
|
|
|
254
|
-
|
|
191
|
+
To release, bump `lib/cal_invite/version.rb`, update `CHANGELOG.md`, merge to `main`, and push a `vX.Y.Z` tag. CI then runs the tests, publishes the gem to RubyGems and deploys the docs.
|
|
255
192
|
|
|
256
|
-
##
|
|
193
|
+
## Contributing
|
|
257
194
|
|
|
258
|
-
|
|
195
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/DashAPI-ai/cal-invite). Please add tests for your change and run `bundle exec rake test` before opening a PR. To report a security issue, see [SECURITY.md](SECURITY.md). Everyone taking part in the project is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
|
259
196
|
|
|
260
|
-
|
|
197
|
+
## Sponsor
|
|
261
198
|
|
|
262
|
-
|
|
199
|
+
CalInvite is sponsored by [DashAPI](https://dashapi.ai).
|
|
263
200
|
|
|
264
|
-
|
|
201
|
+
<a href="https://dashapi.ai">
|
|
202
|
+
<picture>
|
|
203
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/sponsors/dashapi-logo-dark.svg">
|
|
204
|
+
<img src="assets/sponsors/dashapi-logo-light.svg" alt="DashAPI" width="140">
|
|
205
|
+
</picture>
|
|
206
|
+
</a>
|
|
265
207
|
|
|
266
|
-
##
|
|
208
|
+
## License
|
|
267
209
|
|
|
268
|
-
|
|
210
|
+
Released under the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
|
@@ -25,7 +25,8 @@ RDoc::Task.new(:rdoc) do |rdoc|
|
|
|
25
25
|
rdoc.options << '--exclude' << '^(test|spec|features)/'
|
|
26
26
|
|
|
27
27
|
# Include files to document
|
|
28
|
-
rdoc.rdoc_files.include('README.md', '
|
|
28
|
+
rdoc.rdoc_files.include('README.md', 'CONFIGURATION.md', 'CACHING.md', 'CHANGELOG.md',
|
|
29
|
+
'SECURITY.md', 'CODE_OF_CONDUCT.md', 'LICENSE.txt', 'lib/**/*.rb')
|
|
29
30
|
rdoc.rdoc_files.exclude('lib/cal_invite/version.rb')
|
|
30
31
|
end
|
|
31
32
|
|
data/SECURITY.md
CHANGED
|
@@ -2,20 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
## Supported Versions
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Security fixes are released for the latest minor version only. Please upgrade to
|
|
6
|
+
the most recent release before reporting.
|
|
7
7
|
|
|
8
8
|
| Version | Supported |
|
|
9
9
|
| ------- | ------------------ |
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
| 4.0.x | :white_check_mark: |
|
|
13
|
-
| < 4.0 | :x: |
|
|
10
|
+
| 0.2.x | :white_check_mark: |
|
|
11
|
+
| < 0.2 | :x: |
|
|
14
12
|
|
|
15
13
|
## Reporting a Vulnerability
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
**Please do not open a public issue for security problems.**
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
Report vulnerabilities privately through GitHub:
|
|
18
|
+
[Report a vulnerability](https://github.com/DashAPI-ai/cal-invite/security/advisories/new)
|
|
19
|
+
(Security tab → "Report a vulnerability").
|
|
20
|
+
|
|
21
|
+
Please include:
|
|
22
|
+
|
|
23
|
+
- the affected version(s) and provider(s) (`:ics`, `:google`, ...)
|
|
24
|
+
- a minimal `CalInvite::Event` that reproduces the issue, and the output you got
|
|
25
|
+
- the impact you expect (e.g. injected `.ics` properties, broken URL encoding)
|
|
26
|
+
|
|
27
|
+
What to expect:
|
|
28
|
+
|
|
29
|
+
- an acknowledgement within 5 business days;
|
|
30
|
+
- an assessment, and a fix timeline if the report is accepted, within 14 days;
|
|
31
|
+
- a patched release plus a published GitHub Security Advisory (with credit, if
|
|
32
|
+
you'd like it) once a fix is available. If the report is declined, we'll
|
|
33
|
+
explain why.
|
|
34
|
+
|
|
35
|
+
## Scope
|
|
36
|
+
|
|
37
|
+
CalInvite turns `CalInvite::Event` attributes into calendar URLs and `.ics`
|
|
38
|
+
content. Attribute values are treated as untrusted:
|
|
39
|
+
|
|
40
|
+
- **URL providers** (`:google`, `:outlook`, `:office365`, `:yahoo`) URL-encode
|
|
41
|
+
every value.
|
|
42
|
+
- **`.ics` providers** (`:ics`, `:ical`, `IcsContent`) escape TEXT values per
|
|
43
|
+
RFC 5545, quote and escape parameter values (e.g. `CN=`) per RFC 6868, and raise
|
|
44
|
+
`ArgumentError` rather than emit a value that would break out of its content
|
|
45
|
+
line.
|
|
46
|
+
|
|
47
|
+
Anything that lets attribute values alter the structure of generated output
|
|
48
|
+
(extra properties, components, parameters, or URL query parameters) is in scope.
|
|
49
|
+
How the host application stores, sends, or serves the generated content (email
|
|
50
|
+
delivery, HTTP headers you set yourself, etc.) is out of scope.
|