lib/glific/third_party/dialogflow/dialogflow.ex

defmodule Glific.Dialogflow do
  @moduledoc """
  Module to communicate with DialogFlow v2. This module was taken directly from:
  https://github.com/resuelve/flowex/

  I pulled it into our repository since the comments were in Spanish and it did not
  seem to be maintained, that we could not use as is. The dependency list was quite old etc.
  """
  require Logger

  alias Glific.{
    Dialogflow.Intent,
    Dialogflow.Sessions,
    Flows.Action,
    Flows.FlowContext,
    Messages.Message,
    Partners,
    Repo
  }

  alias GoogleApi.Dialogflow.V2.{
    Api.Projects,
    Connection,
    Model.GoogleCloudDialogflowV2ListIntentsResponse
  }

  @doc """
  The request controller which sends and parses requests.
  """
  @spec request(non_neg_integer, atom, String.t(), String.t() | map) :: tuple
  def request(organization_id, method, path, body) do
    %{url: url, id: id, email: email} = project_info(organization_id)

    dflow_url = "#{url}/#{id}/locations/global/agent/#{path}"

    method
    |> do_request(dflow_url, body(body), headers(email, organization_id))
    |> case do
      {:ok, %Tesla.Env{status: status, body: body}} when status in 200..299 ->
        {:ok, Jason.decode!(body)}

      {:ok, %Tesla.Env{status: status, body: body}} when status in 400..499 ->
        {:error, Jason.decode!(body)}

      {:ok, %Tesla.Env{status: status, body: body}} when status >= 500 ->
        {:error, Jason.decode!(body)}

      {:error, %Tesla.Error{reason: reason}} ->
        {:error, reason}

      {:error, :timeout} ->
        {:error, "Timeout"}
    end
  end

  @spec do_request(atom(), String.t(), String.t(), list()) :: Tesla.Env.result()
  defp do_request(:post, url, body, header),
    do: Tesla.post(url, body, headers: header, opts: [adapter: [recv_timeout: 30_000]])

  defp do_request(_, url, _, _), do: Tesla.get(url)

  # ---------------------------------------------------------------------------
  # Encode body
  # ---------------------------------------------------------------------------
  @spec body(String.t() | map) :: String.t()
  defp body(""), do: ""
  defp body(body), do: Poison.encode!(body)

  # ---------------------------------------------------------------------------
  # Headers for all subsequent API calls
  # ---------------------------------------------------------------------------
  @spec headers(String.t(), non_neg_integer) :: list
  defp headers(_email, org_id) do
    token = Partners.get_goth_token(org_id, "dialogflow")

    [
      {"Authorization", "Bearer #{token.token}"},
      {"Content-Type", "application/json"}
    ]
  end

  # ---------------------------------------------------------------------------
  # Get the project details needed for authentication and to send via the API
  # ---------------------------------------------------------------------------
  @spec project_info(non_neg_integer) :: %{
          :url => String.t(),
          :id => String.t(),
          :email => String.t()
        }
  defp project_info(organization_id) do
    case Partners.organization(organization_id).services["dialogflow"] do
      nil ->
        %{
          url: nil,
          id: nil,
          email: nil
        }

      credential ->
        service_account = Jason.decode!(credential.secrets["service_account"])

        %{
          url: "https://dialogflow.googleapis.com/v2beta1/projects",
          id: service_account["project_id"],
          email: service_account["client_email"]
        }
    end
  end

  @doc """
  Execute a webhook action, could be either get or post for now
  """
  @spec execute(Action.t(), FlowContext.t(), Message.t()) :: :ok
  def execute(action, context, message) do
    Sessions.detect_intent(message, context.id, action.result_name)
  end

  @doc """
  Get the list of all intents from the NLP agent
  """
  @spec get_intent_list(non_neg_integer) ::
          {:ok, GoogleCloudDialogflowV2ListIntentsResponse.t()}
          | {:ok, Tesla.Env.t()}
          | {:ok, list()}
          | {:error, any()}
  def get_intent_list(organization_id) do
    %{url: _url, id: project_id, email: _email} = project_info(organization_id)
    parent = "projects/#{project_id}/agent"

    with token <- Partners.get_goth_token(organization_id, "dialogflow"),
         false <- is_nil(token) do
      response =
        Connection.new(token.token)
        |> Projects.dialogflow_projects_agent_intents_list(parent)

      sync_with_db(response, organization_id)
      response
    else
      true ->
        {:ok, "no token found"}
    end
  end

  @spec sync_with_db(tuple, non_neg_integer) :: :ok
  defp sync_with_db({:ok, res}, organization_id) do
    existing_items = Intent.get_intent_name_list(organization_id)

    intent_name_list =
      res.intents
      |> Enum.map(fn intent ->
        %{
          name: intent.displayName,
          organization_id: organization_id,
          inserted_at: DateTime.utc_now(),
          updated_at: DateTime.utc_now()
        }
      end)
      |> List.insert_at(0, %{
        ## Default list items.
        name: "all",
        organization_id: organization_id,
        inserted_at: DateTime.utc_now(),
        updated_at: DateTime.utc_now()
      })
      |> Enum.filter(fn el -> !Enum.member?(existing_items, el.name) end)

    Intent
    |> Repo.insert_all(intent_name_list)

    :ok
  end

  defp sync_with_db({:error, message}, _organization_id) do
    Logger.error(message)
    :ok
  end
end