defmodule Gcal do
import Gcal.HTTPoison
@moduledoc """
`Gcal` lets you interact with your `Google` Calendar via the API.
View your calendar events, modify event details and create new events.
"""
@baseurl "https://www.googleapis.com/calendar/v3"
@doc """
`headers/1` returns the required headers for making an HTTP request.
Arguments:
`acces_token`: the valid `Google` Auth Session token.
"""
def headers(access_token) do
[
Authorization: "Bearer #{access_token}",
"Content-Type": "application/json"
]
end
@doc """
`get_calendar_list/1` gets the list of available calendars for the person.
https://developers.google.com/calendar/api/v3/reference/calendarList/list
Arguments:
`access_token`: the valid `Google` Auth access token.
Sample response:
```elixir
{:ok, %{
etag: "\"p320ebocgmjpfs0g\"",
items: [
%{
"accessRole" => "owner",
"backgroundColor" => "#9fe1e7",
"colorId" => "14",
"conferenceProperties" => %{
"allowedConferenceSolutionTypes" => ["hangoutsMeet"]
},
"defaultReminders" => [%{"method" => "popup", "minutes" => 10}],
"etag" => "\"1553070512390000\"",
"foregroundColor" => "#000000",
"id" => "nelson@gmail.com",
"kind" => "calendar#calendarListEntry",
"notificationSettings" => %{
"notifications" => [
%{"method" => "email", "type" => "eventCreation"},
%{"method" => "email", "type" => "eventChange"},
%{"method" => "email", "type" => "eventCancellation"},
%{"method" => "email", "type" => "eventResponse"}
]
},
"primary" => true,
"selected" => true,
"summary" => "nelson@gmail.com",
"timeZone" => "Europe/London"
},
%{
"accessRole" => "owner",
"backgroundColor" => "#d06b64",
"colorId" => "2",
"conferenceProperties" => %{
"allowedConferenceSolutionTypes" => ["hangoutsMeet"]
},
"defaultReminders" => [],
"etag" => "\"1553070512692000\"",
"foregroundColor" => "#000000",
"id" => "rpia5b9frqmvvd549c1scs82mk@group.calendar.google.com",
"kind" => "calendar#calendarListEntry",
"location" => "London, UK",
"selected" => true,
"summary" => "dwyl",
"timeZone" => "Europe/London"
}
],
kind: "calendar#calendarList",
nextSyncToken: "CIDl4ZC08v4CEg9uZWxzb25AZHd5bC5jb20="
}}
```
"""
def get_calendar_list(access_token) do
httpoison().get("#{@baseurl}/users/me/calendarList", headers(access_token))
|> parse_body_response()
end
@doc """
`get_calendar_details/2` gets the details of the desired calendar.
Arguments:
`access_token`: the valid `Google` Auth access token
`datetime`: the date and time of the day to fetch the events.
`calendar`: (optional) the string name of the calendar; defaults to "primary"
Sample response:
```elixir
{:ok, %{
conferenceProperties: %{"allowedConferenceSolutionTypes" => ["hangoutsMeet"]},
etag: "\"oftesUJ77GfcrwCPCmctnI90Qzs\"",
id: "nelson@gmail.com",
kind: "calendar#calendar",
summary: "nelson@gmail.com",
timeZone: "Europe/London"
}}
```
"""
def get_calendar_details(access_token, cal_name \\ "primary") do
httpoison().get("#{@baseurl}/calendars/#{cal_name}", headers(access_token))
|> parse_body_response()
end
@doc """
`get_event_list/3` gets the list of events of the desired calendar.
Arguments:
`access_token`: the valid `Google` Auth Session token
`datetime`: the date and time of the day to fetch the events.
`calendar`: (optional) the string name of the calendar; defaults to "primary"
Sample response:
```elixir
%{
accessRole: "owner",
defaultReminders: [%{method: "popup", minutes: 10}],
etag: "\"p32odpveognrvs0g\"",
items: [
%{
attendees: [
%{email: "nelson@gmail.com", responseStatus: "accepted", self: true},
%{email: "ines@gmail.com", organizer: true, responseStatus: "accepted"},
%{email: "simon@gmail.com", responseStatus: "accepted"},
%{email: "busy@gmail.com", responseStatus: "declined"}
],
created: "2019-11-10T17:39:38.000Z",
creator: %{email: "ines@gmail.com"},
description: "Daily Standup for @dwyl team",
end: %{dateTime: "2023-05-15T10:00:00+01:00", timeZone: "Europe/London"},
etag: "\"3359131283178000\"",
eventType: "default",
htmlLink: "https://www.google.com/calendar/event?eid=a21pMWVicWpqYzYy",
iCalUID: "kmi1ebqjjc62s2hlukjj706unq_R20230324T093000@google.com",
id: "kmi1ebqjjc62s2hlukjj706unq_20230515T083000Z",
kind: "calendar#event",
location: "https://zoom.us/j/33713371",
organizer: %{email: "ines@gmail.com"},
originalStartTime: %{
dateTime: "2023-05-15T09:30:00+01:00",
timeZone: "Europe/London"
},
recurringEventId: "kmi1ebqjjc62s2hlukjj706unq_R20230324T093000",
reminders: %{useDefault: true},
sequence: 2,
start: %{dateTime: "2023-05-15T09:30:00+01:00", timeZone: "Europe/London"},
status: "confirmed",
summary: "Daily Standup",
updated: "2023-03-23T10:00:41.589Z"
},
%{
attendees: [
%{email: "nelson@gmail.com", responseStatus: "accepted", self: true},
%{
email: "ines@gmail.com",
organizer: true,
responseStatus: "accepted"
}
],
created: "2023-04-28T08:21:26.000Z",
creator: %{email: "ines@gmail.com"},
end: %{dateTime: "2023-05-15T17:00:00+01:00", timeZone: "Europe/London"},
etag: "\"3367047572704000\"",
eventType: "default",
htmlLink: "https://www.google.com/calendar/event?eid=cnNodDJvMnRmcDNyMjN",
iCalUID: "rsht2o2tfp3r23kfqfc4pip@google.com",
id: "rsht2o2tfp3r23kfqfc4pip",
kind: "calendar#event",
organizer: %{email: "ines@gmail.com"},
reminders: %{useDefault: true},
sequence: 1,
start: %{dateTime: "2023-05-15T09:00:00+01:00", timeZone: "Europe/London"},
status: "confirmed",
summary: "House Work",
updated: "2023-05-08T05:29:46.352Z"
},
%{
created: "2023-05-15T09:48:24.000Z",
creator: %{email: "nelson@dwyl.com", self: true},
end: %{dateTime: "2023-05-15T15:00:00+01:00", timeZone: "Europe/London"},
etag: "\"3368288209788000\"",
eventType: "default",
htmlLink: "https://www.google.com/calendar/event?eid=OHQ1dHNyaHM0cDdpZG",
iCalUID: "8t5tsrhs4p7idmuj5ap8457ppc@google.com",
id: "8t5tsrhs4p7idmuj5ap8457ppc",
kind: "calendar#event",
organizer: %{email: "nelson@dwyl.com", self: true},
reminders: %{useDefault: true},
sequence: 0,
start: %{dateTime: "2023-05-15T14:00:00+01:00", timeZone: "Europe/London"},
status: "confirmed",
summary: "New Event using Gcal",
updated: "2023-05-15T09:48:24.894Z"
}
],
kind: "calendar#events",
nextSyncToken: "CLDc_diF9_4CELDc_diF9_4CGAUgluyY_AE=",
summary: "nelson@gmail.com",
timeZone: "Europe/London",
updated: "2023-05-15T09:48:24.894Z"
}
```
"""
def get_event_list(access_token, datetime, cal_name \\ "primary") do
# Get primary calendar
{:ok, cal} = get_calendar_details(access_token, cal_name)
# Get events of primary calendar
params = %{
singleEvents: true,
timeMin: datetime |> Timex.beginning_of_day() |> Timex.format!("{RFC3339}"),
timeMax: datetime |> Timex.end_of_day() |> Timex.format!("{RFC3339}")
}
{:ok, event_list} =
httpoison().get("#{@baseurl}/calendars/#{cal.id}/events", headers(access_token),
params: params
)
|> parse_body_response()
{:ok, event_list}
end
@doc """
`create_event/3` creates a new event in the desired calendar
Arguments:
`acces_token`: the valid `Google` Auth Session token.
`event_details`: the details of the event to be created
`cal_name`(optional): the calendar to create the event in
Sample `event_details`:
```elixir
%{
"title" => title,
"date" => date,
"start" => start,
"stop" => stop,
"all_day" => all_day,
"hoursFromUTC" => hoursFromUTC
}
```
"""
# Create new event to the primary calendar.
def create_event(access_token, event_details, cal_name \\ "primary") do
e = Useful.atomize_map_keys(event_details)
# Get primary calendar
{:ok, primary_cal} = get_calendar_details(access_token, cal_name)
# Setting `start` and `stop` according to the `all-day` boolean,
# If `all-day` is set to true, we should return the date instead of the datetime,
# as per https://developers.google.com/calendar/api/v3/reference/events/insert.
start = all_day_or_datetime(e, e.start)
stop = all_day_or_datetime(e, e.stop)
# Post new event
body = Jason.encode!(%{summary: e.title, start: start, end: stop})
httpoison().post(
"#{@baseurl}/calendars/#{primary_cal.id}/events",
body,
headers(access_token)
)
|> parse_body_response()
end
@doc """
`all_day_or_datetime/2` is a helper function
that formats the date (in the case of an all_day event)
or datetime in the format that the Google Calendar API accepts.
"""
def all_day_or_datetime(e, time) do
case e.all_day do
true ->
%{date: e.date}
false ->
%{
dateTime:
Timex.parse!("#{e.date} #{time} #{e.hoursFromUTC}", "{YYYY}-{0M}-{D} {h24}:{m} {Z}")
|> Timex.format!("{RFC3339}")
}
end
end
# Parse JSON body response
defp parse_body_response({:ok, response}) do
body = Map.get(response, :body)
# make keys of map atoms for easier access in templates
if body == nil do
{:error, :no_body}
else
{:ok, str_key_map} = Jason.decode(body)
# github.com/dwyl/useful#atomize_map_keys1
{:ok, Useful.atomize_map_keys(str_key_map)}
end
end
end