cal-invite 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87266e97a53aac81a619ae283a7c14c05f67140e96f6d57bc8aca97789c232f4
4
- data.tar.gz: c169424e6171d5f814b41ef6d8154c790efdd1a9693244db1b93265f940dbe6c
3
+ metadata.gz: c410ac7a9f4a0163b9984196b0b9eb0f68974c92c9baf55fdd0d5b3048478dab
4
+ data.tar.gz: a9e3f6f5ed24978f5f891f2a8818f68fa6457f0d28570ff0d5a19c0b4460e3c1
5
5
  SHA512:
6
- metadata.gz: 0fd39a7fa410f1c3212015f3bf4276cbee1473fe2b5f887aef9bcbc1d43ca1da9af1dfb9f72dec990ae25c7e0af33b260778981fd01a795cba3658fafced1fc4
7
- data.tar.gz: 5743ca62b4e8f7062e748ba717e29b5e68909e812a526575dc65b06f17b95d69aec3c7458cb1481c39e6f986b0c594a0ec2a8f390f54d0d78e7909dd21ef4517
6
+ metadata.gz: 7ae7e0b223af18c95eff52db6614ff44c4efc3277b146ab909a60582b86c66140e82bade3fc6507a9eb1f198c404de0896ffcd3bda8b7846bf7917d45500f7b5
7
+ data.tar.gz: 97e1766052ab48c325eaab422d06420e7f98d94cdddd6d9320f9605b5180a1fed783b9d5555bfff47f0ea1ceeaa15225d504f2b538def27395272e8a2752c140
data/CHANGELOG.md CHANGED
@@ -1,10 +1,21 @@
1
- ## [Unreleased]
1
+ # Cal Invite
2
+
3
+ ## [Released]
4
+
5
+ ## [v0.1.2] - 2024-12-17
2
6
 
3
- ## [0.1.1] - 2024-12-17
7
+ - Add support to Microsoft office 365 calendar invite URL
8
+ - Update the README
9
+ - Add an example
10
+ - Better testing
11
+
12
+ ## [v0.1.1] - 2024-12-17
4
13
 
5
14
  Fixing a bug in the gemspec file
6
15
 
7
- ## [0.1.0] - 2024-12-17
16
+ ## [Unreleased]
17
+
18
+ ## [v0.1.0] - 2024-12-17
8
19
 
9
20
  First public release of the Calendar Invite gem
10
21
 
data/README.md CHANGED
@@ -1,9 +1,11 @@
1
- # CalInvite
1
+ # 📅 CalInvite
2
2
 
3
3
  A Ruby gem for generating calendar invitations across multiple calendar platforms with caching and webhook support.
4
4
 
5
5
  [![Gem Version](https://badge.fury.io/rb/cal-invite.svg)](https://badge.fury.io/rb/cal-invite)
6
- [![Ruby](https://github.com/the-pew-inc/cal-invite/workflows/Ruby/badge.svg)](https://github.com/yourusername/cal-invite/actions)
6
+ ![Build Status](https://github.com/the-pew-inc/cal-invite/actions/workflows/main.yml/badge.svg)
7
+
8
+ [![License](https://img.shields.io/github/license/the-pew-inc/cal-invite.svg)]
7
9
 
8
10
  ## Compatibility
9
11
 
@@ -15,18 +17,18 @@ A Ruby gem for generating calendar invitations across multiple calendar platform
15
17
  Direct Integration:
16
18
  - Apple iCal
17
19
  - Microsoft Outlook
20
+ - Microsoft Outlook 365
18
21
  - Google Calendar
19
22
  - Yahoo Calendar
20
23
  - Standard .ics file generation
21
24
 
22
25
  Any calendar application that supports the iCalendar (.ics) standard should work, including but not limited to:
23
- - ProtonCalendar
26
+ - Proton Calendar
24
27
  - FastMail Calendar
25
28
  - Thunderbird Calendar
26
29
  - Zoho Calendar
27
30
  - Microsoft Teams Calendar
28
31
  - Zoom Calendar Integration
29
- - Office 365 Calendar
30
32
 
31
33
  ## Installation
32
34
 
@@ -85,11 +87,12 @@ event = CalInvite::Event.new(
85
87
  notes: "Bring your own laptop"
86
88
  )
87
89
 
88
- ical_url = event.calendar_url(:ical)
89
- google_url = event.calendar_url(:google)
90
- outlook_url = event.calendar_url(:outlook)
91
- yahoo_url = event.calendar_url(:yahoo)
92
- ics_content = event.calendar_url(:ics)
90
+ ical_url = event.calendar_url(:ical)
91
+ google_url = event.calendar_url(:google)
92
+ outlook_url = event.calendar_url(:outlook)
93
+ outlook365_url = event.calendar_url(:office365)
94
+ yahoo_url = event.calendar_url(:yahoo)
95
+ ics_content = event.calendar_url(:ics)
93
96
  ```
94
97
 
95
98
  ## Development
@@ -98,9 +101,15 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
98
101
 
99
102
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
100
103
 
104
+ ## Testing
105
+
106
+ Add test(s) as necessary.
107
+
108
+ Run all the tests before submiting: `bundle exec rake test`
109
+
101
110
  ## Contributing
102
111
 
103
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/cal-invite. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/cal-invite/blob/master/CODE_OF_CONDUCT.md).
112
+ Bug reports and pull requests are welcome on GitHub at https://github.com/the-pew-inc/cal-invite. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/the-pew-inc/cal-invite/blob/master/CODE_OF_CONDUCT.md).
104
113
 
105
114
  ## License
106
115
 
@@ -108,4 +117,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
108
117
 
109
118
  ## Code of Conduct
110
119
 
111
- Everyone interacting in the Cal::Invite project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/cal-invite/blob/master/CODE_OF_CONDUCT.md).
120
+ Everyone interacting in the Cal::Invite project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/the-pew-inc/cal-invite/blob/master/CODE_OF_CONDUCT.md).
@@ -2,6 +2,7 @@
2
2
 
3
3
  # lib/cal_invite/event.rb
4
4
  module CalInvite
5
+ # Represents a calendar event with its properties and validation rules
5
6
  class Event
6
7
  attr_accessor :title,
7
8
  :start_time,
@@ -13,31 +14,73 @@ module CalInvite
13
14
  :timezone,
14
15
  :show_attendees,
15
16
  :notes,
16
- :multi_day_sessions
17
+ :multi_day_sessions,
18
+ :all_day
17
19
 
20
+ # Initialize a new Event instance
21
+ #
22
+ # @param attributes [Hash] The attributes to initialize the event with
23
+ # @option attributes [String] :title The event title
24
+ # @option attributes [Time] :start_time The event start time
25
+ # @option attributes [Time] :end_time The event end time
26
+ # @option attributes [String] :description The event description
27
+ # @option attributes [String] :location The event location
28
+ # @option attributes [String] :url The event URL
29
+ # @option attributes [Array<String>] :attendees List of event attendees
30
+ # @option attributes [String] :timezone The event timezone (default: 'UTC')
31
+ # @option attributes [Boolean] :show_attendees Whether to show attendees (default: false)
32
+ # @option attributes [String] :notes Additional notes for the event
33
+ # @option attributes [Array<Hash>] :multi_day_sessions List of sessions for multi-day events
34
+ # @option attributes [Boolean] :all_day Whether this is an all-day event (default: false)
35
+ # @raise [ArgumentError] if required attributes are missing
18
36
  def initialize(attributes = {})
19
37
  @show_attendees = attributes.delete(:show_attendees) || false
20
38
  @timezone = attributes.delete(:timezone) || 'UTC'
21
39
  @multi_day_sessions = attributes.delete(:multi_day_sessions) || []
40
+ @all_day = attributes.delete(:all_day) || false
41
+
42
+ # Convert times to UTC before storing
43
+ if attributes[:start_time]
44
+ attributes[:start_time] = ensure_utc(attributes[:start_time])
45
+ end
46
+ if attributes[:end_time]
47
+ attributes[:end_time] = ensure_utc(attributes[:end_time])
48
+ end
22
49
 
23
50
  attributes.each do |key, value|
24
51
  send("#{key}=", value) if respond_to?("#{key}=")
25
52
  end
53
+
54
+ validate!
26
55
  end
27
56
 
57
+ # Generate a calendar URL for the specified provider
58
+ #
59
+ # @param provider [Symbol] The calendar provider to generate the URL for
60
+ # @return [String] The generated calendar URL
61
+ # @raise [ArgumentError] if required attributes are missing
28
62
  def calendar_url(provider)
29
- provider_class = CalInvite::Providers.const_get(provider.to_s.camelize)
63
+ validate!
64
+ provider_class = CalInvite::Providers.const_get(capitalize_provider(provider.to_s))
30
65
  generator = provider_class.new(self)
31
66
  generator.generate
32
67
  end
33
68
 
34
69
  # Convert a UTC time to event's timezone
70
+ #
71
+ # @param time [Time, nil] The time to convert
72
+ # @return [Time, nil] The converted time in the event's timezone
35
73
  def localize_time(time)
36
74
  return time unless time
37
- time.in_time_zone(timezone)
75
+ # When timezone is UTC, we should preserve the UTC time
76
+ # without any conversion
77
+ return time if timezone == 'UTC'
78
+ time.getlocal(timezone_offset)
38
79
  end
39
80
 
40
- # Get all event sessions (including multi-day)
81
+ # Get all event sessions including multi-day sessions
82
+ #
83
+ # @return [Array<Array<Time>>] Array of start and end time pairs
41
84
  def sessions
42
85
  return [@start_time, @end_time] if multi_day_sessions.empty?
43
86
 
@@ -45,5 +88,35 @@ module CalInvite
45
88
  [session[:start_time], session[:end_time]]
46
89
  end
47
90
  end
91
+
92
+ private
93
+
94
+ def ensure_utc(time)
95
+ return nil unless time
96
+ time.is_a?(Time) ? time.utc : Time.parse(time.to_s).utc
97
+ end
98
+
99
+ def timezone_offset
100
+ return '+00:00' if timezone == 'UTC'
101
+ timezone # assume timezone is already in offset format
102
+ end
103
+
104
+ def capitalize_provider(string)
105
+ # Handles both simple capitalization (ical -> Ical)
106
+ # and compound names (office365 -> Office365)
107
+ string.split('_').map(&:capitalize).join
108
+ end
109
+
110
+ # Validate the event attributes
111
+ #
112
+ # @raise [ArgumentError] if required attributes are missing
113
+ def validate!
114
+ raise ArgumentError, "Title is required" if title.nil? || title.strip.empty?
115
+
116
+ unless all_day
117
+ raise ArgumentError, "Start time is required for non-all-day events" if start_time.nil?
118
+ raise ArgumentError, "End time is required for non-all-day events" if end_time.nil?
119
+ end
120
+ end
48
121
  end
49
122
  end
@@ -3,23 +3,41 @@
3
3
  # lib/cal_invite/providers/base_provider.rb
4
4
  module CalInvite
5
5
  module Providers
6
+ # Base class for calendar providers that implements common functionality
7
+ # and defines the interface that all providers must implement
6
8
  class BaseProvider
9
+ # @return [CalInvite::Event] The event being processed
7
10
  attr_reader :event
8
11
 
12
+ # Initialize a new calendar provider
13
+ #
14
+ # @param event [CalInvite::Event] The event to generate a calendar URL for
9
15
  def initialize(event)
10
16
  @event = event
11
17
  end
12
18
 
19
+ # Generate a calendar URL for the event
20
+ # This method must be implemented by all provider subclasses
21
+ #
22
+ # @abstract
23
+ # @raise [NotImplementedError] if the provider class doesn't implement this method
13
24
  def generate
14
25
  raise NotImplementedError, "#{self.class} must implement #generate"
15
26
  end
16
27
 
17
28
  protected
18
29
 
30
+ # URL encode a string for use in calendar URLs
31
+ #
32
+ # @param str [#to_s] The string to encode
33
+ # @return [String] The URL encoded string
19
34
  def url_encode(str)
20
35
  URI.encode_www_form_component(str.to_s)
21
36
  end
22
37
 
38
+ # Format the event description including notes and URL if present
39
+ #
40
+ # @return [String, nil] The formatted description or nil if no content
23
41
  def format_description
24
42
  parts = []
25
43
  parts << event.description if event.description
@@ -28,6 +46,9 @@ module CalInvite
28
46
  parts.join("\n\n")
29
47
  end
30
48
 
49
+ # Format the event location, combining physical location and URL if both present
50
+ #
51
+ # @return [String, nil] The formatted location or nil if neither location nor URL present
31
52
  def format_location
32
53
  return event.url if event.url && !event.location
33
54
  return event.location if event.location && !event.url
@@ -35,6 +56,9 @@ module CalInvite
35
56
  nil
36
57
  end
37
58
 
59
+ # Get the list of attendees if showing attendees is enabled
60
+ #
61
+ # @return [Array<String>] The list of attendees or empty array if disabled/none present
38
62
  def attendees_list
39
63
  return [] unless event.show_attendees && event.attendees&.any?
40
64
  event.attendees
@@ -4,11 +4,9 @@
4
4
  module CalInvite
5
5
  module Providers
6
6
  class Google < BaseProvider
7
- BASE_URL = "https://calendar.google.com/calendar/render"
8
-
9
7
  def generate
10
- if event.multi_day_sessions.any?
11
- generate_multi_day_event
8
+ if event.all_day
9
+ generate_all_day_event
12
10
  else
13
11
  generate_single_event
14
12
  end
@@ -16,50 +14,55 @@ module CalInvite
16
14
 
17
15
  private
18
16
 
19
- def generate_single_event
17
+ def generate_all_day_event
20
18
  params = {
21
- action: "TEMPLATE",
22
- text: event.title,
23
- dates: format_dates(event.start_time, event.end_time),
24
- details: format_description,
25
- location: format_location,
26
- ctz: event.timezone
19
+ action: 'TEMPLATE',
20
+ text: url_encode(event.title),
21
+ dates: format_all_day_dates
27
22
  }
28
23
 
29
- if attendees_list.any?
30
- params[:add] = attendees_list.join(',')
31
- end
32
-
33
- "#{BASE_URL}?#{URI.encode_www_form(params)}"
24
+ add_optional_params(params)
25
+ build_url(params)
34
26
  end
35
27
 
36
- def generate_multi_day_event
37
- # For multi-day events, Google Calendar supports recurring events
38
- sessions = event.multi_day_sessions.map do |session|
39
- format_dates(session[:start_time], session[:end_time])
40
- end
41
-
28
+ def generate_single_event
42
29
  params = {
43
- action: "TEMPLATE",
44
- text: event.title,
45
- dates: sessions.join(','),
46
- details: format_description,
47
- location: format_location,
48
- ctz: event.timezone
30
+ action: 'TEMPLATE',
31
+ text: url_encode(event.title),
32
+ dates: format_dates
49
33
  }
50
34
 
51
- if attendees_list.any?
52
- params[:add] = attendees_list.join(',')
53
- end
35
+ add_optional_params(params)
36
+ build_url(params)
37
+ end
38
+
39
+ def format_all_day_dates
40
+ # For all-day events, use current date if no start_time specified
41
+ start_date = event.start_time || Time.now
42
+ end_date = event.end_time || (start_date + 86400) # Add one day if no end_time
54
43
 
55
- "#{BASE_URL}?#{URI.encode_www_form(params)}"
44
+ "#{start_date.strftime('%Y%m%d')}/#{end_date.strftime('%Y%m%d')}"
56
45
  end
57
46
 
58
- def format_dates(start_time, end_time)
59
- start_time = event.localize_time(start_time)
60
- end_time = event.localize_time(end_time)
47
+ def format_dates
48
+ raise ArgumentError, "Start time is required" unless event.start_time
49
+ raise ArgumentError, "End time is required" unless event.end_time
50
+
51
+ start_time = event.start_time
52
+ end_time = event.end_time
53
+
54
+ "#{start_time.utc.strftime('%Y%m%dT%H%M%SZ')}/#{end_time.utc.strftime('%Y%m%dT%H%M%SZ')}"
55
+ end
56
+
57
+ def add_optional_params(params)
58
+ params[:details] = url_encode(format_description) if format_description
59
+ params[:location] = url_encode(format_location) if format_location
60
+ params
61
+ end
61
62
 
62
- "#{start_time.strftime('%Y%m%dT%H%M%S')}/#{end_time.strftime('%Y%m%dT%H%M%S')}"
63
+ def build_url(params)
64
+ query = params.map { |k, v| "#{k}=#{v}" }.join('&')
65
+ "https://calendar.google.com/calendar/render?#{query}"
63
66
  end
64
67
  end
65
68
  end
@@ -8,61 +8,70 @@ module CalInvite
8
8
  [
9
9
  "BEGIN:VCALENDAR",
10
10
  "VERSION:2.0",
11
- "PRODID:-//CalInvite//EN",
11
+ "PRODID:-//CalInvite//Ruby//EN",
12
12
  generate_events,
13
13
  "END:VCALENDAR"
14
- ].flatten.join("\r\n")
14
+ ].join("\n")
15
15
  end
16
16
 
17
17
  private
18
18
 
19
19
  def generate_events
20
20
  if event.multi_day_sessions.any?
21
- event.multi_day_sessions.map { |session| generate_vevent(session[:start_time], session[:end_time]) }
21
+ event.multi_day_sessions.map { |session| generate_vevent(session) }.join("\n")
22
22
  else
23
- [generate_vevent(event.start_time, event.end_time)]
23
+ generate_vevent
24
24
  end
25
25
  end
26
26
 
27
- def generate_vevent(start_time, end_time)
28
- vevent = [
29
- "BEGIN:VEVENT",
30
- "UID:#{generate_uid}",
31
- "DTSTAMP:#{format_time(Time.now)}",
32
- "DTSTART;TZID=#{event.timezone}:#{format_time(start_time)}",
33
- "DTEND;TZID=#{event.timezone}:#{format_time(end_time)}",
34
- "SUMMARY:#{event.title}",
35
- "DESCRIPTION:#{format_description}",
36
- ]
27
+ def generate_vevent(session = nil)
28
+ lines = ["BEGIN:VEVENT"]
37
29
 
38
- if format_location
39
- vevent << "LOCATION:#{format_location}"
40
- end
30
+ if event.all_day
31
+ start_date = event.start_time || Time.now
32
+ end_date = event.end_time || (start_date + 86400)
33
+
34
+ lines << "DTSTART;VALUE=DATE:#{format_date(start_date)}"
35
+ lines << "DTEND;VALUE=DATE:#{format_date(end_date)}"
36
+ else
37
+ start_time = session ? session[:start_time] : event.start_time
38
+ end_time = session ? session[:end_time] : event.end_time
39
+
40
+ raise ArgumentError, "Start time is required for non-all-day events" unless start_time
41
+ raise ArgumentError, "End time is required for non-all-day events" unless end_time
41
42
 
42
- if event.url
43
- vevent << "URL:#{event.url}"
43
+ lines << "DTSTART:#{format_time(start_time)}"
44
+ lines << "DTEND:#{format_time(end_time)}"
44
45
  end
45
46
 
46
- if attendees_list.any?
47
- attendees_list.each do |attendee|
48
- vevent << "ATTENDEE;RSVP=TRUE:mailto:#{attendee}"
47
+ lines.concat([
48
+ "SUMMARY:#{event.title}",
49
+ "UID:#{generate_uid}"
50
+ ])
51
+
52
+ lines << "DESCRIPTION:#{format_description}" if format_description
53
+ lines << "LOCATION:#{format_location}" if format_location
54
+
55
+ if attendees = attendees_list
56
+ attendees.each do |attendee|
57
+ lines << "ATTENDEE:mailto:#{attendee}"
49
58
  end
50
59
  end
51
60
 
52
- vevent << "END:VEVENT"
53
- vevent
61
+ lines << "END:VEVENT"
62
+ lines.join("\n")
54
63
  end
55
64
 
56
65
  def generate_uid
57
- "#{Time.now.to_i}-#{SecureRandom.hex(8)}@cal-invite"
66
+ SecureRandom.uuid
58
67
  end
59
68
 
60
- def format_time(time)
61
- event.localize_time(time).strftime("%Y%m%dT%H%M%S")
69
+ def format_date(time)
70
+ time.strftime("%Y%m%d")
62
71
  end
63
72
 
64
- def escape_text(text)
65
- text.to_s.gsub(/[,;\\]/) { |match| "\\#{match}" }
73
+ def format_time(time)
74
+ time.utc.strftime("%Y%m%dT%H%M%SZ")
66
75
  end
67
76
  end
68
77
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/cal_invite/providers/office365.rb
4
+ module CalInvite
5
+ module Providers
6
+ class Office365 < BaseProvider
7
+ def generate
8
+ if event.all_day
9
+ generate_all_day_event
10
+ else
11
+ generate_single_event
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def generate_all_day_event
18
+ params = {
19
+ subject: url_encode(event.title),
20
+ path: '/calendar/action/compose',
21
+ allday: 'true'
22
+ }
23
+
24
+ # Use current date if no start_time specified
25
+ start_date = event.start_time || Time.now
26
+ end_date = event.end_time || (start_date + 86400) # Add one day if no end_time
27
+
28
+ params[:startdt] = url_encode(format_date(start_date))
29
+ params[:enddt] = url_encode(format_date(end_date))
30
+
31
+ add_optional_params(params)
32
+ build_url(params)
33
+ end
34
+
35
+ def generate_single_event
36
+ params = {
37
+ subject: url_encode(event.title),
38
+ path: '/calendar/action/compose'
39
+ }
40
+
41
+ raise ArgumentError, "Start time is required" unless event.start_time
42
+ raise ArgumentError, "End time is required" unless event.end_time
43
+
44
+ params[:startdt] = url_encode(format_time(event.start_time))
45
+ params[:enddt] = url_encode(format_time(event.end_time))
46
+
47
+ add_optional_params(params)
48
+ build_url(params)
49
+ end
50
+
51
+ def format_date(time)
52
+ time.strftime('%Y-%m-%d')
53
+ end
54
+
55
+ def format_time(time)
56
+ time.utc.strftime('%Y-%m-%dT%H:%M:%S')
57
+ end
58
+
59
+ def add_optional_params(params)
60
+ if description = format_description
61
+ params[:body] = url_encode(description)
62
+ end
63
+
64
+ if location = format_location
65
+ params[:location] = url_encode(location)
66
+ end
67
+
68
+ if attendees = attendees_list
69
+ params[:to] = url_encode(attendees.join(';'))
70
+ end
71
+
72
+ params
73
+ end
74
+
75
+ def build_url(params)
76
+ query = params.map { |k, v| "#{k}=#{v}" }.join('&')
77
+ "https://outlook.office.com/owa/?#{query}"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -4,11 +4,9 @@
4
4
  module CalInvite
5
5
  module Providers
6
6
  class Outlook < BaseProvider
7
- BASE_URL = "https://outlook.live.com/calendar/0/deeplink/compose"
8
-
9
7
  def generate
10
- if event.multi_day_sessions.any?
11
- generate_multi_day_event
8
+ if event.all_day
9
+ generate_all_day_event
12
10
  else
13
11
  generate_single_event
14
12
  end
@@ -16,49 +14,62 @@ module CalInvite
16
14
 
17
15
  private
18
16
 
17
+ def generate_all_day_event
18
+ params = {
19
+ path: '/calendar/0/action/compose',
20
+ subject: url_encode(event.title),
21
+ allday: 'true'
22
+ }
23
+
24
+ # Use current date if no start_time specified
25
+ start_date = event.start_time || Time.now
26
+ end_date = event.end_time || (start_date + 86400) # Add one day if no end_time
27
+
28
+ params[:startdt] = format_date(start_date)
29
+ params[:enddt] = format_date(end_date)
30
+
31
+ add_optional_params(params)
32
+ build_url(params)
33
+ end
34
+
19
35
  def generate_single_event
20
36
  params = {
21
- subject: event.title,
22
- startdt: format_time(event.start_time),
23
- enddt: format_time(event.end_time),
24
- body: format_description,
25
- location: format_location,
26
- path: '/calendar/action/compose',
27
- rru: 'addevent'
37
+ path: '/calendar/0/action/compose',
38
+ subject: url_encode(event.title)
28
39
  }
29
40
 
30
- if attendees_list.any?
31
- params[:to] = attendees_list.join(';')
32
- end
41
+ raise ArgumentError, "Start time is required" unless event.start_time
42
+ raise ArgumentError, "End time is required" unless event.end_time
33
43
 
34
- "#{BASE_URL}?#{URI.encode_www_form(params)}"
44
+ params[:startdt] = format_time(event.start_time)
45
+ params[:enddt] = format_time(event.end_time)
46
+
47
+ add_optional_params(params)
48
+ build_url(params)
49
+ end
50
+
51
+ def format_date(time)
52
+ time.strftime('%Y-%m-%d')
53
+ end
54
+
55
+ def format_time(time)
56
+ time.utc.strftime('%Y-%m-%dT%H:%M:%S')
35
57
  end
36
58
 
37
- def generate_multi_day_event
38
- # For Outlook, we create separate events for each session
39
- sessions = event.multi_day_sessions.map do |session|
40
- params = {
41
- subject: event.title,
42
- startdt: format_time(session[:start_time]),
43
- enddt: format_time(session[:end_time]),
44
- body: format_description,
45
- location: format_location,
46
- path: '/calendar/action/compose',
47
- rru: 'addevent'
48
- }
49
-
50
- if attendees_list.any?
51
- params[:to] = attendees_list.join(';')
52
- end
53
-
54
- "#{BASE_URL}?#{URI.encode_www_form(params)}"
59
+ def add_optional_params(params)
60
+ params[:body] = url_encode(format_description) if format_description
61
+ params[:location] = url_encode(format_location) if format_location
62
+
63
+ if attendees = attendees_list
64
+ params[:to] = url_encode(attendees.join(';'))
55
65
  end
56
66
 
57
- sessions.join("\n")
67
+ params
58
68
  end
59
69
 
60
- def format_time(time)
61
- event.localize_time(time).strftime("%Y-%m-%dT%H:%M:%S")
70
+ def build_url(params)
71
+ query = params.map { |k, v| "#{k}=#{v}" }.join('&')
72
+ "https://outlook.live.com/calendar/0/action/compose?#{query}"
62
73
  end
63
74
  end
64
75
  end
@@ -7,7 +7,9 @@ module CalInvite
7
7
  BASE_URL = "https://calendar.yahoo.com"
8
8
 
9
9
  def generate
10
- if event.multi_day_sessions.any?
10
+ if event.all_day
11
+ generate_all_day_event
12
+ elsif event.multi_day_sessions.any?
11
13
  generate_multi_day_event
12
14
  else
13
15
  generate_single_event
@@ -16,14 +18,37 @@ module CalInvite
16
18
 
17
19
  private
18
20
 
21
+ def generate_all_day_event
22
+ start_date = event.start_time || Time.now
23
+ end_date = event.end_time || (start_date + 86400)
24
+
25
+ params = {
26
+ v: 60,
27
+ view: 'd',
28
+ type: 20,
29
+ title: event.title,
30
+ st: format_date(start_date),
31
+ et: format_date(end_date),
32
+ desc: format_description,
33
+ in_loc: format_location,
34
+ crnd: event.timezone,
35
+ allday: 'true'
36
+ }
37
+
38
+ "#{BASE_URL}/?#{URI.encode_www_form(params)}"
39
+ end
40
+
19
41
  def generate_single_event
42
+ raise ArgumentError, "Start time is required" unless event.start_time
43
+ raise ArgumentError, "End time is required" unless event.end_time
44
+
20
45
  params = {
21
46
  v: 60,
22
47
  view: 'd',
23
48
  type: 20,
24
49
  title: event.title,
25
- st: format_start_time(event.start_time),
26
- et: format_end_time(event.end_time),
50
+ st: format_time(event.start_time),
51
+ et: format_time(event.end_time),
27
52
  desc: format_description,
28
53
  in_loc: format_location,
29
54
  crnd: event.timezone
@@ -41,8 +66,8 @@ module CalInvite
41
66
  view: 'd',
42
67
  type: 20,
43
68
  title: event.title,
44
- st: format_start_time(session[:start_time]),
45
- et: format_end_time(session[:end_time]),
69
+ st: format_time(session[:start_time]),
70
+ et: format_time(session[:end_time]),
46
71
  desc: format_description,
47
72
  in_loc: format_location,
48
73
  crnd: event.timezone
@@ -54,12 +79,12 @@ module CalInvite
54
79
  sessions.join("\n")
55
80
  end
56
81
 
57
- def format_start_time(time)
58
- event.localize_time(time).strftime("%Y%m%dT%H%M%S")
82
+ def format_date(time)
83
+ time.strftime("%Y%m%d")
59
84
  end
60
85
 
61
- def format_end_time(time)
62
- event.localize_time(time).strftime("%Y%m%dT%H%M%S")
86
+ def format_time(time)
87
+ time.utc.strftime("%Y%m%dT%H%M%S")
63
88
  end
64
89
  end
65
90
  end
@@ -3,13 +3,15 @@
3
3
  # lib/cal_invite/providers.rb
4
4
  module CalInvite
5
5
  module Providers
6
- SUPPORTED_PROVIDERS = %i[google ical outlook yahoo ics].freeze
6
+ SUPPORTED_PROVIDERS = %i[google ical outlook yahoo ics office365].freeze
7
7
 
8
8
  autoload :BaseProvider, 'cal_invite/providers/base_provider'
9
9
  autoload :Google, 'cal_invite/providers/google'
10
10
  autoload :Ical, 'cal_invite/providers/ical'
11
11
  autoload :Outlook, 'cal_invite/providers/outlook'
12
+ autoload :Office365, 'cal_invite/providers/office365'
12
13
  autoload :Yahoo, 'cal_invite/providers/yahoo'
13
14
  autoload :Ics, 'cal_invite/providers/ics'
15
+
14
16
  end
15
17
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CalInvite
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/cal_invite.rb CHANGED
@@ -1,4 +1,8 @@
1
1
  # lib/cal_invite.rb
2
+ require 'securerandom'
3
+ require 'time'
4
+ require 'uri'
5
+
2
6
  require 'cal_invite/version'
3
7
  require 'cal_invite/configuration'
4
8
  require 'cal_invite/event'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cal-invite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephane Paquet
@@ -10,9 +10,8 @@ bindir: exe
10
10
  cert_chain: []
11
11
  date: 2024-12-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: CalInvite is a gem to generate calendar invitations and provides an easu
14
- way to generate .ics files or link to quickly save an event date/time into the major
15
- calendards such as Google Calendar, Microsoft Outlook, Apple Calendar and many more.
13
+ description: CalInvite provides a simple way to generate calendar invite URLs for
14
+ various providers (Google, Outlook, Yahoo) and ICS files
16
15
  email:
17
16
  - 176050+spaquet@users.noreply.github.com
18
17
  executables: []
@@ -36,6 +35,7 @@ files:
36
35
  - lib/cal_invite/providers/google.rb
37
36
  - lib/cal_invite/providers/ical.rb
38
37
  - lib/cal_invite/providers/ics.rb
38
+ - lib/cal_invite/providers/office365.rb
39
39
  - lib/cal_invite/providers/outlook.rb
40
40
  - lib/cal_invite/providers/yahoo.rb
41
41
  - lib/cal_invite/version.rb
@@ -65,5 +65,5 @@ requirements: []
65
65
  rubygems_version: 3.5.22
66
66
  signing_key:
67
67
  specification_version: 4
68
- summary: CalInvite is a gem to generate calendar invitations.
68
+ summary: A Ruby gem for generating calendar invite URLs and ICS files
69
69
  test_files: []