Complete guide to iCalendar events with Ruby
It’s common to receive a calendar invita via email:
In reality, the email just has a file with .ics
extension attached. You email client recognizes it and adds it to your calendar.
An .ics
file can look more-less like this:
You can use gem icalendar to generate .ics
files with Ruby.
Initial setup #
Let’s assume that a User wants to add a sports Game to his calendar.
Let’s generate some dummy events:
# config/seeds.rb
5.times do |index|
starts_at = index.days.ago
ends_at = starts_at + 2 * 60 * 60
title = Faker::Sports.sport
description = Faker::Quote.famous_last_words
address = Faker::Address.full_address
game = Game.create!(starts_at:, ends_at:, title:, description:, address:)
end
5.times do |index|
starts_at = index.days.ago
ends_at = starts_at + 2 * 60 * 60
title = Faker::Sports.sport
description = Faker::Quote.famous_last_words
address = Faker::Address.full_address
game = Game.create!(starts_at:, ends_at:, title:, description:, address:)
end
bundle add faker
rails g scaffold Game starts_at:datetime ends_at:datetime title:string description:text address:text
rails db:migrate db:seed
Create a rich .ics
file #
We will generate .ics
files for game/user in a Service, so that it is reusable:
bundle add icalendar
mkdir app/services
mkdir app/services/games
echo > app/services/games/icalendar_event.rb
The icalendar object with almost all possible options:
# app/services/games/icalendar_event.rb
# Games::IcalendarEvent.new(game: Game.last).call
# Games::IcalendarEvent.new(game: Game.last, user: User.last).call
class Games::IcalendarEvent
require 'icalendar'
include Rails.application.routes.url_helpers
def initialize(game:, user: nil)
@game = game
@user = user
@url = game_url(@game)
end
def call
ical = ::Icalendar::Calendar.new
event = ::Icalendar::Event.new
event.dtstart = Icalendar::Values::DateTime.new @game.starts_at
event.dtend = Icalendar::Values::DateTime.new @game.ends_at
event.summary = @game.title
event.description = "Watch #{@game.title} live at #{@url}"
event.uid = @game.id.to_s # important for updating/canceling an event
event.sequence = Time.now.to_i # important for updating/canceling an event
event.url = @url
event.location = @game.address # location on map
# event.attendee = %w(mailto:abc@example.com mailto:xyz@example.com)
if @user.present?
event.attendee = Icalendar::Values::CalAddress.new("mailto:#{@user.email}", partstat: 'accepted') # DECLINED # TENTATIVE
end
# event.organizer = "mailto:organizer@example.com"
event.organizer = Icalendar::Values::CalAddress.new("mailto:#{ApplicationMailer.default_params[:from]}", cn: 'Yaro from Superails')
event.status = 'CONFIRMED' # 'CANCELLED'
event.ip_class = 'PUBLIC' # 'PRIVATE'
# event.attach = Icalendar::Values::Uri.new @url
# event.append_attach = Icalendar::Values::Uri.new(@url, "fmttype" => "application/binary")
event.created = @game.created_at
event.last_modified = @game.updated_at
event.alarm do |a|
a.summary = "#{@game.title} starts in 30 minutes!"
a.trigger = '-PT30M'
end
ical.add_event(event)
ical.append_custom_property('METHOD', 'REQUEST') # add event to calendar by default!
ical.publish
# ical.ip_method = 'REQUEST'
# ical.ip_method = 'PUBLISH'
# ical.ip_method = 'CANCEL'
ical
end
end
The complete icalendar documentation can give you more insight on all the above options.
This way you can call
Games::IcalendarEvent.new(game: Game.last).call
or
Games::IcalendarEvent.new(game: Game.last, user: User.last).call
and generate an ical object for any game-user pair.
How can we deliver the .ics
file?
That there are 3 common ways to share calendar events with users:
- Link to download ical file (easiest)
- webcal: allow user to “sync with a remote calendar”
- Send calendar invite via email (most common)
1. Link to download icalendar file #
This way a user can download a game/event as an .ics
file, click on it and add it to his calendar:
# app/controllers/games_controller.rb
def show
respond_to do |format|
format.html
format.ics do
# calendar = Games::IcalendarEvent.new(game: @game, user: User.last).call
calendar = Games::IcalendarEvent.new(game: @game).call
send_data calendar.to_ical, type: 'text/calendar', disposition: 'attachment', filename: "Game#{@game.id}.ics"
# render body: calendar.to_ical
# render inline: calendar.to_ical
# render plain: calendar.to_ical
end
end
end
<%= link_to game.title, game_path(game, format: :ics) %>
Adding a downloaded .ics
file to calendar:
2. Webcal calendar sync #
Allow a user to “subscribe” to your calendar. His calendar app will “ping” your calendar endpoint from time to time to get the updated events list.
In the below example, we add all games/events to the calendar:
# app/controllers/games_controller.rb
# class GamesController < ActionController::Base
class GamesController < ApplicationController
skip_before_action :authenticate_user, only: :index
def index
@games = Game.all
respond_to do |format|
# format.html
format.ics do
cal = Icalendar::Calendar.new
cal.x_wr_calname = 'All games'
@games.each do |game|
cal.event do |event|
event.dtstart = game.starts_at
event.dtend = game.ends_at
event.summary = game.title
event.description = game.description
event.uid = game.id.to_s
event.sequence = Time.now.to_i
end
end
cal.publish
render plain: cal.to_ical
end
end
end
end
Add a link to “subscribe” to the calendar. When the user clicks the link, it will open in the users calendar app and suggest adding an external calendar:
<%= link_to 'subscribe', games_url(protocol: :webcal, format: :ics) %>
When you click the link, it will open your calendar app and suggest adding/syncing a calendar:
In a few seconds, you will see all the calendar events in your calendar app:
Important notes:
- The URL should be publicly accessible
- Must use URL helpers, not path helpers
-
protocol
must bewebcal
, nothttp
-
format
must beics
-
render plain
in controller -
cal.x_wr_calname
to set default calendar name
This will not work on localhost, because you don’t have a real web url that the calendar app can send a request to! => use nginx, or do it on staging/production.
3. Send calendar invite via email #
We are all used to getting invites via email and accepting/declining them.
Let’s create a button to email the event to the calendar:
rails g mailer game_mailer add_game
# config/routes.rb
resources :games do
member do
post :email_to_calendar
end
end
# app/controllers/streaming/game_streams_controller.rb
def email_to_calendar
@game = Game.find(params[:id])
# send email here
GameMailer.with(game: @game, user: current_user).add_game.deliver_later
redirect_to games_path, notice: 'Email sent'
end
# app/mailers/game_mailer.rb
class GameMailer < ApplicationMailer
def add_game
@game = params[:game]
@user = params[:user]
@game_url = streaming_sport_game_stream_url(sport_slug: @game.sport_slug, game_slug: @game.slug)
@subject = [@game.title, @game.display_starts_at].join(', ')
ical = Games::IcalendarEvent.new(game: @game, user: @user).call # generate the ical file
# attach ICS file
mail.attachments["#{@subject}.ics"] = { mime_type: 'application/ics', content: ical.to_ical }
# mail.attachments["#{@subject}.ics"] = { mime_type: 'text/calendar', content: ical.to_ical }
# mail.attachments["#{@subject}.ics"] = { mime_type: 'text/calendar; method=REQUEST', content: ical.to_ical }
mail to: @user.email,
from: 'Yaro from Superails <admin@superails.com>',
cc: Rails.application.config_for(:settings)[:support_email],
subject: @subject
end
end
<%= button_to 'Email to calendar', email_to_calendar_game_path(game) %>
Now when you click the link, an email invite will be sent:
If you send follow-up emails with changed start/end time of the event, it will be automatically updated in the users calendar! Also, if you send an update with event.status = 'CANCELLED'
, the event will be removed from the calendar!
That’s it! 🎉🥳🍾
References:
Did you like this article? Did it save you some time?