cal-invite 0.1.4 → 0.2.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.
@@ -46,9 +46,15 @@ module CalInvite
46
46
  "VERSION:2.0",
47
47
  "PRODID:-//CalInvite//EN",
48
48
  "CALSCALE:GREGORIAN",
49
- "METHOD:PUBLISH"
49
+ "METHOD:#{method_value}"
50
50
  ]
51
51
 
52
+ calendar_lines << "X-WR-CALNAME:#{escape_text(event.calendar_name)}" if event.calendar_name
53
+
54
+ unless event.all_day
55
+ calendar_lines.concat(vtimezone_lines || [])
56
+ end
57
+
52
58
  if event.all_day
53
59
  calendar_lines.concat(generate_all_day_event)
54
60
  elsif event.multi_day_sessions.any?
@@ -71,7 +77,7 @@ module CalInvite
71
77
  def generate_all_day_event
72
78
  vevent = [
73
79
  "BEGIN:VEVENT",
74
- "UID:#{generate_uid}",
80
+ "UID:#{event.uid}",
75
81
  "DTSTAMP:#{format_timestamp(Time.now.utc)}",
76
82
  "DTSTART;VALUE=DATE:#{format_date(event.start_time)}",
77
83
  "DTEND;VALUE=DATE:#{format_date(event.end_time)}",
@@ -90,7 +96,7 @@ module CalInvite
90
96
  def generate_vevent(start_time, end_time)
91
97
  vevent = [
92
98
  "BEGIN:VEVENT",
93
- "UID:#{generate_uid}",
99
+ "UID:#{event.uid}",
94
100
  "DTSTAMP:#{format_timestamp(Time.now.utc)}",
95
101
  "DTSTART;TZID=#{event.timezone}:#{format_local_timestamp(start_time)}",
96
102
  "DTEND;TZID=#{event.timezone}:#{format_local_timestamp(end_time)}",
@@ -102,7 +108,8 @@ module CalInvite
102
108
  end
103
109
 
104
110
  # Adds optional fields to the event component if they exist.
105
- # Handles description, location, URL, and attendees.
111
+ # Handles description, location, URL, geo, organizer, attendees, recurrence,
112
+ # visibility/busy status, and reminders.
106
113
  #
107
114
  # @param vevent [Array<String>] The current event lines array
108
115
  # @return [void]
@@ -121,11 +128,23 @@ module CalInvite
121
128
  vevent << "URL:#{escape_text(url)}"
122
129
  end
123
130
 
124
- if attendees_list.any?
125
- attendees_list.each do |attendee|
126
- vevent << "ATTENDEE;RSVP=TRUE:mailto:#{attendee}"
127
- end
131
+ vevent << geo_line if geo_line
132
+
133
+ if organizer = organizer_line
134
+ vevent << organizer
128
135
  end
136
+
137
+ attendees_list.each { |attendee| vevent << attendee_line(attendee) }
138
+
139
+ vevent << rrule_line if rrule_line
140
+ vevent << "SEQUENCE:#{event.sequence}"
141
+ vevent << status_line
142
+ vevent << transp_line
143
+ vevent << busystatus_line
144
+ vevent << class_line
145
+ vevent.concat(importance_lines)
146
+ vevent << disallow_counter_line if disallow_counter_line
147
+ vevent.concat(valarm_lines)
129
148
  end
130
149
 
131
150
  # Formats a time object as an UTC timestamp in iCalendar format.
@@ -145,20 +164,13 @@ module CalInvite
145
164
  end
146
165
 
147
166
  # Formats a time object as a local timestamp in iCalendar format.
148
- # Note: Times are expected to be in UTC already.
167
+ # Converts from UTC to the event's timezone; times are expected to be in
168
+ # UTC already.
149
169
  #
150
170
  # @param time [Time] The time to format
151
171
  # @return [String] The formatted local time (YYYYMMDDTHHmmSS)
152
172
  def format_local_timestamp(time)
153
- time.strftime("%Y%m%dT%H%M%S")
154
- end
155
-
156
- # Generates a unique identifier for the calendar event.
157
- # Format: timestamp-randomhex@cal-invite
158
- #
159
- # @return [String] The generated UID
160
- def generate_uid
161
- "#{Time.now.to_i}-#{SecureRandom.hex(8)}@cal-invite"
173
+ local_wall_time(time).strftime("%Y%m%dT%H%M%S")
162
174
  end
163
175
 
164
176
  # Escapes special characters in text according to iCalendar spec.
@@ -36,9 +36,11 @@ module CalInvite
36
36
  "VERSION:2.0",
37
37
  "PRODID:-//CalInvite//EN",
38
38
  "CALSCALE:GREGORIAN",
39
- "METHOD:PUBLISH"
39
+ "METHOD:#{method_value}"
40
40
  ]
41
41
 
42
+ calendar_lines << "X-WR-CALNAME:#{escape_text(event.calendar_name)}" if event.calendar_name
43
+
42
44
  if event.multi_day_sessions.any?
43
45
  event.multi_day_sessions.each do |session|
44
46
  calendar_lines.concat(generate_vevent(session[:start_time], session[:end_time]))
@@ -61,7 +63,7 @@ module CalInvite
61
63
  def generate_vevent(start_time, end_time)
62
64
  [
63
65
  "BEGIN:VEVENT",
64
- "UID:#{generate_uid}",
66
+ "UID:#{event.uid}",
65
67
  "DTSTAMP:#{format_timestamp(Time.now.utc)}",
66
68
  "DTSTART:#{format_timestamp(start_time)}",
67
69
  "DTEND:#{format_timestamp(end_time)}",
@@ -69,7 +71,18 @@ module CalInvite
69
71
  description_line,
70
72
  location_line,
71
73
  url_line,
74
+ geo_line,
75
+ organizer_line,
72
76
  attendee_lines,
77
+ rrule_line,
78
+ "SEQUENCE:#{event.sequence}",
79
+ status_line,
80
+ transp_line,
81
+ busystatus_line,
82
+ class_line,
83
+ importance_lines,
84
+ disallow_counter_line,
85
+ valarm_lines,
73
86
  "END:VEVENT"
74
87
  ].compact
75
88
  end
@@ -82,14 +95,6 @@ module CalInvite
82
95
  time.utc.strftime("%Y%m%dT%H%M%SZ")
83
96
  end
84
97
 
85
- # Generates a unique identifier for the calendar event.
86
- # Format: timestamp-randomhex@cal-invite
87
- #
88
- # @return [String] The generated UID
89
- def generate_uid
90
- "#{Time.now.to_i}-#{SecureRandom.hex(8)}@cal-invite"
91
- end
92
-
93
98
  # Escapes special characters in text according to iCalendar spec.
94
99
  #
95
100
  # @param text [String, nil] The text to escape
@@ -131,8 +136,8 @@ module CalInvite
131
136
  #
132
137
  # @return [Array<String>, nil] Array of ATTENDEE lines, or nil if no attendees or not showing
133
138
  def attendee_lines
134
- return nil unless event.show_attendees && event.attendees&.any?
135
- event.attendees.map { |attendee| "ATTENDEE;RSVP=TRUE:mailto:#{attendee}" }
139
+ return nil if attendees_list.empty?
140
+ attendees_list.map { |attendee| attendee_line(attendee) }
136
141
  end
137
142
  end
138
143
 
@@ -142,10 +147,19 @@ module CalInvite
142
147
  # Generates appropriate HTTP headers for ICS file download.
143
148
  #
144
149
  # @param filename [String] The desired filename for the download
150
+ # @param method [Symbol, nil] The iCalendar METHOD used to generate the content
151
+ # (:publish or :request). When given, it's added as a `method` parameter on
152
+ # the Content-Type header — this is what lets mail clients (Gmail, Outlook,
153
+ # Apple Mail) recognize the attachment as a meeting invite and render RSVP
154
+ # actions instead of a plain file attachment. Must match the METHOD in the
155
+ # .ics content itself.
145
156
  # @return [Hash] HTTP headers for the ICS file download
146
- def self.headers(filename)
157
+ def self.headers(filename, method: nil)
158
+ content_type = 'text/calendar; charset=UTF-8'
159
+ content_type += "; method=#{method == :decline_counter ? 'DECLINECOUNTER' : method.to_s.upcase}" if method
160
+
147
161
  {
148
- 'Content-Type' => 'text/calendar; charset=UTF-8',
162
+ 'Content-Type' => content_type,
149
163
  'Content-Disposition' => "attachment; filename=#{sanitize_filename(filename)}"
150
164
  }
151
165
  end
@@ -162,23 +176,20 @@ module CalInvite
162
176
  #
163
177
  # @param content [String] The ICS calendar content
164
178
  # @param title [String] The event title to use in the filename
179
+ # @param method [Symbol, nil] The iCalendar METHOD used to generate `content`
180
+ # (:publish or :request); forwarded to {headers}. Must match the METHOD
181
+ # used when generating `content` itself.
165
182
  # @return [Hash] Hash containing content and headers for download
166
183
  # @example
167
184
  # result = IcsDownload.wrap_for_download(ics_content, "team-meeting")
168
185
  # # => { content: "BEGIN:VCALENDAR...", headers: { 'Content-Type' => '...' } }
169
- def self.wrap_for_download(content, title)
186
+ def self.wrap_for_download(content, title, method: nil)
170
187
  filename = sanitize_filename("#{title.downcase}_#{Time.now.strftime('%Y%m%d')}.ics")
171
188
  {
172
189
  content: content,
173
- headers: headers(filename)
190
+ headers: headers(filename, method: method)
174
191
  }
175
192
  end
176
193
  end
177
-
178
- # Compatibility class aliases
179
- # @api private
180
- class Ics < IcsContent; end
181
- # @api private
182
- class Ical < IcsContent; end
183
194
  end
184
195
  end
@@ -115,8 +115,8 @@ module CalInvite
115
115
  params[:body] = url_encode(description_parts.join("\n\n")) if description_parts.any?
116
116
  params[:location] = url_encode(format_location) if format_location
117
117
 
118
- if attendees = attendees_list
119
- params[:to] = url_encode(attendees.join(';'))
118
+ if attendees_list.any?
119
+ params[:to] = url_encode(attendee_emails.join(';'))
120
120
  end
121
121
 
122
122
  params
@@ -120,8 +120,8 @@ module CalInvite
120
120
  params[:body] = url_encode(description_parts.join("\n\n")) if description_parts.any?
121
121
  params[:location] = url_encode(format_location) if format_location
122
122
 
123
- if attendees = attendees_list
124
- params[:to] = url_encode(attendees.join(';'))
123
+ if attendees_list.any?
124
+ params[:to] = url_encode(attendee_emails.join(';'))
125
125
  end
126
126
 
127
127
  params
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CalInvite
4
- VERSION = "0.1.4"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/cal_invite.rb CHANGED
@@ -1,4 +1,7 @@
1
1
  # lib/cal_invite.rb
2
+ require 'logger'
3
+ require 'mutex_m'
4
+ require 'bigdecimal'
2
5
  require 'active_support'
3
6
  require 'active_support/core_ext'
4
7
  require 'securerandom'
@@ -8,6 +11,7 @@ require 'uri'
8
11
  require 'cal_invite/version'
9
12
  require 'cal_invite/configuration'
10
13
  require 'cal_invite/caching'
14
+ require 'cal_invite/ical_timezone'
11
15
  require 'cal_invite/event'
12
16
  require 'cal_invite/providers'
13
17
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cal-invite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephane Paquet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-19 00:00:00.000000000 Z
11
+ date: 2026-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,20 +24,76 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: tzinfo
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: logger
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mutex_m
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bigdecimal
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
27
83
  - !ruby/object:Gem::Dependency
28
84
  name: rdoc
29
85
  requirement: !ruby/object:Gem::Requirement
30
86
  requirements:
31
87
  - - "~>"
32
88
  - !ruby/object:Gem::Version
33
- version: '6.5'
89
+ version: '8.0'
34
90
  type: :development
35
91
  prerelease: false
36
92
  version_requirements: !ruby/object:Gem::Requirement
37
93
  requirements:
38
94
  - - "~>"
39
95
  - !ruby/object:Gem::Version
40
- version: '6.5'
96
+ version: '8.0'
41
97
  description: CalInvite provides a simple way to generate calendar invite URLs for
42
98
  various providers (Google, Outlook, Yahoo) and ICS files
43
99
  email:
@@ -49,12 +105,16 @@ files:
49
105
  - ".DS_Store"
50
106
  - ".rdoc_options"
51
107
  - ".rubocop.yml"
108
+ - AGENTS.md
52
109
  - CACHING.md
53
110
  - CHANGELOG.md
111
+ - CLAUDE.md
54
112
  - CODE_OF_CONDUCT.md
113
+ - CONFIGURATION.md
55
114
  - LICENSE.txt
56
115
  - README.md
57
116
  - Rakefile
117
+ - SECURITY.md
58
118
  - gemfiles/Gemfile.rails6
59
119
  - gemfiles/Gemfile.rails7
60
120
  - gemfiles/Gemfile.rails8
@@ -64,6 +124,7 @@ files:
64
124
  - lib/cal_invite/caching.rb
65
125
  - lib/cal_invite/configuration.rb
66
126
  - lib/cal_invite/event.rb
127
+ - lib/cal_invite/ical_timezone.rb
67
128
  - lib/cal_invite/providers.rb
68
129
  - lib/cal_invite/providers/base_provider.rb
69
130
  - lib/cal_invite/providers/google.rb
@@ -76,13 +137,13 @@ files:
76
137
  - lib/cal_invite/version.rb
77
138
  - lib/tasks/rdoc.rake
78
139
  - sig/cal/invite.rbs
79
- homepage: https://github.com/the-pew-inc/cal-invite
140
+ homepage: https://github.com/DashAPI-ai/cal-invite
80
141
  licenses:
81
142
  - MIT
82
143
  metadata:
83
- homepage_uri: https://github.com/the-pew-inc/cal-invite
84
- source_code_uri: https://github.com/the-pew-inc/cal-invite
85
- changelog_uri: https://github.com/the-pew-inc/cal-invite/blob/master/CHANGELOG.md
144
+ homepage_uri: https://github.com/DashAPI-ai/cal-invite
145
+ source_code_uri: https://github.com/DashAPI-ai/cal-invite
146
+ changelog_uri: https://github.com/DashAPI-ai/cal-invite/blob/master/CHANGELOG.md
86
147
  post_install_message:
87
148
  rdoc_options:
88
149
  - "--title"