defmodule Glific.Templates do
@moduledoc """
The Templates context.
"""
import Ecto.Query, warn: false
use Tesla
plug(Tesla.Middleware.FormUrlencoded)
alias Glific.{
Notifications,
Partners.Organization,
Partners.Provider,
Repo,
Settings,
Tags.Tag,
Tags.TemplateTag,
Templates.SessionTemplate
}
require Logger
@doc """
Returns the list of session_templates.
## Examples
iex> list_session_templates()
[%SessionTemplate{}, ...]
"""
@spec list_session_templates(map()) :: [SessionTemplate.t()]
def list_session_templates(args),
do: Repo.list_filter(args, SessionTemplate, &Repo.opts_with_label/2, &filter_with/2)
@doc """
Return the count of session_templates, using the same filter as list_session_templates
"""
@spec count_session_templates(map()) :: integer
def count_session_templates(args),
do: Repo.count_filter(args, SessionTemplate, &filter_with/2)
# codebeat:disable[ABC,LOC]
@spec filter_with(Ecto.Queryable.t(), %{optional(atom()) => any}) :: Ecto.Queryable.t()
defp filter_with(query, filter) do
query = Repo.filter_with(query, filter)
Enum.reduce(filter, query, fn
{:is_hsm, is_hsm}, query ->
from(q in query, where: q.is_hsm == ^is_hsm)
{:is_active, is_active}, query ->
from(q in query, where: q.is_active == ^is_active)
{:status, status}, query ->
from(q in query, where: q.status == ^status)
{:term, term}, query ->
query
|> join(:left, [template], template_tag in TemplateTag,
as: :template_tag,
on: template_tag.template_id == template.id
)
|> join(:left, [template_tag: template_tag], tag in Tag,
as: :tag,
on: template_tag.tag_id == tag.id
)
|> where(
[template, tag: tag],
ilike(template.label, ^"%#{term}%") or
ilike(template.shortcode, ^"%#{term}%") or
ilike(template.body, ^"%#{term}%") or
ilike(tag.label, ^"%#{term}%") or
ilike(tag.shortcode, ^"%#{term}%")
)
|> distinct([template], template.id)
_, query ->
query
end)
end
# codebeat:enable[ABC,LOC]
@doc """
Gets a single session_template.
Raises `Ecto.NoResultsError` if the SessionTemplate does not exist.
## Examples
iex> get_session_template!(123)
%SessionTemplate{}
iex> get_session_template!(456)
** (Ecto.NoResultsError)
"""
@spec get_session_template!(integer) :: SessionTemplate.t()
def get_session_template!(id), do: Repo.get!(SessionTemplate, id)
@doc """
Creates a session_template.
## Examples
iex> create_session_template(%{field: value})
{:ok, %SessionTemplate{}}
iex> create_session_template(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
@spec create_session_template(map()) ::
{:ok, SessionTemplate.t()} | {:error, Ecto.Changeset.t()}
def create_session_template(%{is_hsm: true} = attrs) do
# validate HSM before calling the BSP's API
attrs =
if Map.has_key?(attrs, :shortcode),
do: Map.merge(attrs, %{shortcode: String.downcase(attrs.shortcode)}),
else: attrs
with :ok <- validate_hsm(attrs),
:ok <- validate_button_template(Map.merge(%{has_buttons: false}, attrs)) do
submit_for_approval(attrs)
end
end
def create_session_template(attrs),
do: do_create_session_template(attrs)
@spec validate_hsm(map()) :: :ok | {:error, [String.t()]}
defp validate_hsm(%{shortcode: shortcode, category: _, example: _} = _attrs) do
if String.match?(shortcode, ~r/^[a-z0-9_]*$/),
do: :ok,
else: {:error, ["shortcode", "only '_' and alphanumeric characters are allowed"]}
end
defp validate_hsm(_) do
{:error,
["HSM approval", "for HSM approval shortcode, category and example fields are required"]}
end
@spec validate_button_template(map()) :: :ok | {:error, [String.t()]}
defp validate_button_template(%{has_buttons: false} = _attrs), do: :ok
defp validate_button_template(%{has_buttons: true, button_type: _, buttons: _} = _attrs),
do: :ok
defp validate_button_template(_) do
{:error,
[
"Button Template",
"for Button Templates has_buttons, button_type and buttons fields are required"
]}
end
@doc false
@spec do_create_session_template(map()) ::
{:ok, SessionTemplate.t()} | {:error, Ecto.Changeset.t()}
def do_create_session_template(attrs) do
%SessionTemplate{}
|> SessionTemplate.changeset(attrs)
|> Repo.insert()
end
@spec submit_for_approval(map()) :: {:ok, SessionTemplate.t()} | {:error, String.t()}
defp submit_for_approval(attrs) do
Logger.info("Submitting template for approval with attrs as #{inspect(attrs)}")
bsp_module = Provider.bsp_module(attrs.organization_id, :template)
bsp_module.submit_for_approval(attrs)
end
@doc """
Imports pre approved templates from bsp
"""
@spec import_templates(non_neg_integer(), String.t()) :: {:ok, any} | {:error, any}
def import_templates(org_id, data) do
Provider.bsp_module(org_id, :template).import_templates(org_id, data)
end
@doc """
Bulk applying templates from CSV
"""
@spec bulk_apply_templates(non_neg_integer(), String.t()) :: {:ok, any} | {:error, any}
def bulk_apply_templates(org_id, data) do
Provider.bsp_module(org_id, :template).bulk_apply_templates(org_id, data)
end
@doc """
Updates a session_template.
## Examples
iex> update_session_template(session_template, %{field: new_value})
{:ok, %SessionTemplate{}}
iex> update_session_template(session_template, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
@spec update_session_template(SessionTemplate.t(), map()) ::
{:ok, SessionTemplate.t()} | {:error, Ecto.Changeset.t()}
def update_session_template(%SessionTemplate{} = session_template, attrs) do
session_template
|> SessionTemplate.update_changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a session_template.
## Examples
iex> delete_session_template(session_template)
{:ok, %SessionTemplate{}}
iex> delete_session_template(session_template)
{:error, %Ecto.Changeset{}}
"""
@spec delete_session_template(SessionTemplate.t()) ::
{:ok, SessionTemplate.t()} | {:error, Ecto.Changeset.t()}
def delete_session_template(%SessionTemplate{} = session_template) do
if session_template.is_hsm do
Task.Supervisor.async_nolink(Glific.TaskSupervisor, fn ->
org_id = session_template.organization_id
bsp_module = Provider.bsp_module(org_id, :template)
bsp_module.delete(org_id, Map.from_struct(session_template))
end)
end
Repo.delete(session_template)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking session_template changes.
## Examples
iex> change_session_template(session_template)
%Ecto.Changeset{data: %SessionTemplate{}}
"""
@spec change_session_template(SessionTemplate.t(), map()) :: Ecto.Changeset.t()
def change_session_template(%SessionTemplate{} = session_template, attrs \\ %{}) do
SessionTemplate.changeset(session_template, attrs)
end
@doc """
Create a session template form message
Body and type will be the message attributes
"""
@spec create_template_from_message(%{message_id: integer, input: map}) ::
{:ok, SessionTemplate.t()} | {:error, String.t()}
def create_template_from_message(%{message_id: message_id, input: input}) do
message =
Glific.Messages.get_message!(message_id)
|> Repo.preload([:contact])
Map.merge(
%{body: message.body, type: message.type, organization_id: message.organization_id},
input
)
|> create_session_template()
end
@doc """
get and update list of hsm of an organization
"""
@spec sync_hsms_from_bsp(non_neg_integer()) :: :ok | {:error, String.t()}
def sync_hsms_from_bsp(organization_id) when is_nil(organization_id),
do: {:error, "organization_id is not given"}
def sync_hsms_from_bsp(organization_id) do
bsp_module = Provider.bsp_module(organization_id, :template)
res = bsp_module.update_hsm_templates(organization_id)
Logger.info(
"Templates has been sync for org id: #{organization_id} with response: #{inspect(res)}"
)
res
end
@doc false
@spec update_hsms(list(), Organization.t()) :: :ok
def update_hsms(templates, organization) do
languages =
Settings.list_languages()
|> Enum.map(fn language -> {language.locale, language.id} end)
|> Map.new()
db_templates = hsm_template_uuid_map()
Enum.each(templates, fn template ->
cond do
!Map.has_key?(db_templates, template["bsp_id"]) ->
insert_hsm(template, organization, languages)
# this check is required,
# as is_active field can be updated by graphql API,
# and should not be reverted back
Map.has_key?(db_templates, template["bsp_id"]) ->
update_hsm(template, organization, languages)
true ->
true
end
end)
end
@spec update_hsm(map(), Organization.t(), map()) ::
{:ok, SessionTemplate.t()} | {:error, Ecto.Changeset.t()}
defp update_hsm(template, organization, languages) do
# get updated db templates to handle multiple approved translations
db_templates = hsm_template_uuid_map()
db_template_translations =
db_templates
|> Map.values()
|> Enum.filter(fn db_template ->
db_template.shortcode == template["elementName"] and
db_template.bsp_id != template["bsp_id"]
end)
approved_db_templates =
db_template_translations
|> Enum.filter(fn db_template -> db_template.status == "APPROVED" end)
with true <- template["status"] == "APPROVED",
true <- length(db_template_translations) >= 1,
true <- length(approved_db_templates) >= 1 do
approved_db_templates
|> Enum.each(fn approved_db_template ->
update_hsm_translation(template, approved_db_template, organization, languages)
end)
end
do_update_hsm(template, db_templates)
end
@spec insert_hsm(map(), Organization.t(), map()) :: :ok
defp insert_hsm(template, organization, languages) do
example =
case Jason.decode(template["meta"] || "{}") do
{:ok, meta} ->
meta["example"] || "NA"
_ ->
"NA"
end
if example,
do: do_insert_hsm(template, organization, languages, example),
else: :ok
end
@spec do_insert_hsm(map(), Organization.t(), map(), String.t()) :: :ok
defp do_insert_hsm(template, organization, languages, example) do
number_of_parameter = length(Regex.split(~r/{{.}}/, template["data"])) - 1
type =
template["templateType"]
|> String.downcase()
|> Glific.safe_string_to_atom()
# setting default language id if languageCode is not known
language_id = languages[template["languageCode"]] || organization.default_language_id
Logger.info("Language id for template #{template["elementName"]}
org_id: #{organization.id} has been updated as #{language_id}")
is_active =
if template["status"] in ["APPROVED", "SANDBOX_REQUESTED"],
do: true,
else: false
attrs =
%{
uuid: template["id"],
body: template["data"],
shortcode: template["elementName"],
label: template["elementName"],
category: template["category"],
example: example,
type: type,
language_id: language_id,
organization_id: organization.id,
is_hsm: true,
status: template["status"],
is_active: is_active,
number_parameters: number_of_parameter,
bsp_id: template["bsp_id"] || template["id"]
}
|> check_for_button_template()
%SessionTemplate{}
|> SessionTemplate.changeset(attrs)
|> Repo.insert()
|> case do
{:ok, template} ->
Logger.info("New Session Template Added with label: #{template.label}")
{:error, error} ->
Logger.error(
"Error adding new Session Template: #{inspect(error)} and attrs #{inspect(attrs)}"
)
end
:ok
end
@spec check_for_button_template(map()) :: map()
defp check_for_button_template(%{body: template_body} = template) do
[body | buttons] = template_body |> String.split(["| ["])
if body == template_body do
template
else
template
|> Map.put(:body, body)
|> Map.put(:has_buttons, true)
|> update_template_buttons(buttons)
end
end
@spec update_template_buttons(map(), list()) :: map()
defp update_template_buttons(template, buttons) do
parsed_buttons =
buttons
|> Enum.map(fn button ->
button_list = String.replace(button, "]", "") |> String.split(",")
parse_template_button(button_list, length(button_list))
end)
button_type =
if parsed_buttons |> Enum.any?(fn %{type: button_type} -> button_type == "QUICK_REPLY" end),
do: :quick_reply,
else: :call_to_action
template
|> Map.put(:buttons, parsed_buttons)
|> Map.put(:button_type, button_type)
end
@spec parse_template_button(list(), non_neg_integer()) :: map()
defp parse_template_button([text, content], 2) do
if String.contains?(content, "http"),
do: %{url: content, text: text, type: "URL"},
else: %{phone_number: content, text: text, type: "PHONE_NUMBER"}
end
defp parse_template_button([content], 1), do: %{text: content, type: "QUICK_REPLY"}
@spec do_update_hsm(map(), map()) ::
{:ok, SessionTemplate.t()} | {:error, Ecto.Changeset.t()}
defp do_update_hsm(template, db_templates) do
current_template = db_templates[template["bsp_id"]]
update_attrs =
if current_template.status != template["status"],
do: change_template_status(template["status"], current_template, template),
else: %{status: template["status"]}
db_templates[template["bsp_id"]]
|> SessionTemplate.changeset(update_attrs)
|> Repo.update()
end
@spec change_template_status(String.t(), map(), map()) :: map()
defp change_template_status("APPROVED", db_template, _bsp_template) do
Notifications.create_notification(%{
category: "Templates",
message: "Template #{db_template.shortcode} has been approved",
severity: Notifications.types().info,
organization_id: db_template.organization_id,
entity: %{
id: db_template.id,
shortcode: db_template.shortcode,
label: db_template.label,
uuid: db_template.uuid
}
})
%{status: "APPROVED", is_active: true}
end
defp change_template_status("REJECTED", db_template, bsp_template) do
Notifications.create_notification(%{
category: "Templates",
message: "Template #{db_template.shortcode} has been rejected",
severity: Notifications.types().info,
organization_id: db_template.organization_id,
entity: %{
id: db_template.id,
shortcode: db_template.shortcode,
label: db_template.label,
uuid: db_template.uuid
}
})
%{status: "REJECTED", reason: bsp_template["reason"]}
end
defp change_template_status(status, _db_template, _bsp_template), do: %{status: status}
@spec update_hsm_translation(map(), SessionTemplate.t(), Organization.t(), map()) ::
{:ok, SessionTemplate.t()} | {:error, Ecto.Changeset.t()}
defp update_hsm_translation(template, approved_db_template, organization, languages) do
number_of_parameter = template_parameters_count(%{body: template["data"]})
type =
template["templateType"]
|> String.downcase()
|> Glific.safe_string_to_atom()
# setting default language id if languageCode is not known
language_id = languages[template["languageCode"]] || organization.default_language_id
example =
case Jason.decode(template["meta"] || "{}") do
{:ok, meta} ->
meta["example"]
_ ->
nil
end
translation = %{
"#{language_id}" => %{
uuid: template["id"],
body: template["data"],
language_id: language_id,
status: template["status"],
type: type,
number_parameters: number_of_parameter,
example: example,
category: template["category"],
label: template["elementName"]
}
}
translations = Map.merge(approved_db_template.translations, translation)
update_attrs = %{
translations: translations
}
approved_db_template
|> SessionTemplate.changeset(update_attrs)
|> Repo.update()
end
@doc """
Returns the count of variables in template
"""
@spec template_parameters_count(map()) :: non_neg_integer()
def template_parameters_count(template) do
template = parse_buttons(template, false, Map.get(template, :has_buttons, false))
template.body
|> String.split()
|> Enum.reduce([], fn word, acc ->
with true <- String.match?(word, ~r/{{([1-9]|[1-9][0-9])}}/),
clean_word <- Glific.string_clean(word) do
acc ++ [clean_word]
else
_ -> acc
end
end)
|> Enum.uniq()
|> Enum.count()
end
@spec hsm_template_uuid_map() :: map()
defp hsm_template_uuid_map do
list_session_templates(%{filter: %{is_hsm: true}})
|> Map.new(fn %{bsp_id: bsp_id} = template ->
{bsp_id, template}
end)
end
@doc false
@spec parse_buttons(map(), boolean(), boolean()) :: map()
def parse_buttons(session_template, false, true) do
# parsing buttons only when template is not already translated, else buttons are part of body
updated_body =
session_template.buttons
|> Enum.reduce(session_template.body, fn button, acc ->
"#{acc}| [" <> do_parse_buttons(button["type"], button) <> "] "
end)
session_template
|> Map.merge(%{body: updated_body})
end
def parse_buttons(session_template, _is_translated, _has_buttons), do: session_template
@spec do_parse_buttons(String.t(), map()) :: String.t()
defp do_parse_buttons("URL", button), do: button["text"] <> ", " <> button["url"]
defp do_parse_buttons("PHONE_NUMBER", button),
do: button["text"] <> ", " <> button["phone_number"]
defp do_parse_buttons("QUICK_REPLY", button), do: button["text"]
end