lib/gcal.ex

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