defmodule GlificWeb.Flows.FlowEditorController do @moduledoc """ The Flow Editor Controller """ use GlificWeb, :controller plug(:set_appsignal_namespace) alias Glific.{ Contacts, Dialogflow, Flows, Flows.ContactField, Flows.Flow, Flows.FlowCount, Flows.FlowLabel, GCS.GcsWorker, Partners, Repo, Settings, Sheets, Templates.InteractiveTemplate, Templates.InteractiveTemplates, Users.User } defp set_appsignal_namespace(conn, _params) do # Configures all actions in this controller to report Glific.Appsignal.set_namespace("flow_editor_controller") conn end @doc false @spec globals(Plug.Conn.t(), map) :: Plug.Conn.t() def globals(conn, _params) do conn |> json(%{results: []}) end @doc false @spec groups(Plug.Conn.t(), map) :: Plug.Conn.t() def groups(conn, _params) do group_list = Glific.Groups.list_groups( %{filter: %{organization_id: conn.assigns[:organization_id]}}, true ) |> Enum.reduce([], fn group, acc -> [%{uuid: "#{group.id}", name: group.label, type: "group"} | acc] end) conn |> json(%{results: group_list}) end @doc false @spec groups_post(Plug.Conn.t(), map) :: Plug.Conn.t() def groups_post(conn, _params) do conn |> json(%{ uuid: 0, query: nil, status: "not-ready", count: 0, name: "ALERT: PLEASE CREATE NEW GROUP FROM THE ORGANIZATION SETTINGS" }) end @doc false @spec fields(Plug.Conn.t(), map) :: Plug.Conn.t() def fields(conn, _params) do fields = ContactField.list_contacts_fields(%{ filter: %{organization_id: conn.assigns[:organization_id]} }) |> Enum.reduce([], fn cf, acc -> [%{key: cf.shortcode, name: cf.name, value_type: cf.value_type, label: cf.name} | acc] end) json(conn, %{results: fields}) end @doc """ Add Contact fields into the database. The response should be a map with 3 keys % { Key: Field name, name: Field display name value_type: type of the value} We are not supporting this for now. We will add that in future """ @spec fields_post(Plug.Conn.t(), map) :: Plug.Conn.t() def fields_post(conn, params) do # need to store this into DB, the value_type will default to text in this case # the shortcode is the name, lower cased, and camelized ContactField.create_contact_field(%{ name: params["label"], shortcode: String.downcase(params["label"]) |> String.replace(" ", "_"), organization_id: conn.assigns[:organization_id] }) |> case do {:ok, contact_field} -> conn |> json(%{ key: contact_field.shortcode, name: contact_field.name, label: contact_field.name, value_type: contact_field.value_type }) {:error, _} -> conn |> put_status(400) |> json(%{ error: %{status: 400, message: "Cannot create new field with label #{params["label"]}"} }) end end @doc """ Get all the tags so that user can apply them on incoming message. We are not supporting this for now. To enable It should return a list of map having uuid and name as keys [%{uuid: tag.uuid, name: tag.label}] """ @spec labels(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def labels(conn, _params) do flow_list = FlowLabel.get_all_flowlabel(conn.assigns[:organization_id]) json(conn, %{results: flow_list}) end @doc """ Store a label (new tag) in the system. The return response should be a map of 3 keys. [%{uuid: tag.uuid, name: params["name"], count}] We are not supporting them for now. We will come back to this in near future """ @spec labels_post(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def labels_post(conn, params) do {:ok, flow_label} = FlowLabel.create_flow_label(%{ name: params["name"], organization_id: conn.assigns[:organization_id] }) json(conn, %{uuid: flow_label.uuid, name: flow_label.name, count: 0}) end @doc """ A list of all the communication channels. For Glific it's just WhatsApp. We are not supporting them for now. We will come back to this in near future """ @spec channels(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def channels(conn, _params) do channels = %{ results: [ %{ uuid: generate_uuid(), name: "WhatsApp", address: "", schemes: ["whatsapp"], roles: ["send", "receive"] } ] } json(conn, channels) end @doc """ A list of all the NLP classifiers. For Glific it's just Dialogflow We are not supporting them for now. We will come back to this in near future """ @spec classifiers(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def classifiers(conn, _params) do organization_id = conn.assigns[:organization_id] classifiers = %{ results: [ %{ uuid: "dialogflow_uuid", name: "Dialogflow", type: "dialogflow", intents: Dialogflow.Intent.get_intent_name_list(organization_id) } ] } json(conn, classifiers) end @doc """ We are not sure how to use this but this endpoint is required for flow editor. Will come back to this in future. """ @spec ticketers(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def ticketers(conn, _params) do ticketers = %{results: []} json(conn, ticketers) end @doc """ We are not using this for now but this is required for flow editor config. """ @spec resthooks(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def resthooks(conn, _params) do resthooks = %{results: []} json(conn, resthooks) end @doc """ A list of all the interactive templates in format that is understood by flow editor """ @spec interactive_templates(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def interactive_templates(conn, _params) do results = InteractiveTemplates.list_interactives(%{ filter: %{organization_id: conn.assigns[:organization_id]} }) |> Enum.reduce([], fn interactive, acc -> [ %{ id: interactive.id, name: interactive.label, type: interactive.type, interactive_content: interactive.interactive_content, created_on: interactive.inserted_at, modified_on: interactive.updated_at } | acc ] end) json(conn, %{results: results}) end @doc """ Fetching single interactive template and returning in format that is understood by flow editor or Return error Interactive message not found """ @spec interactive_template(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def interactive_template(conn, params) do [id] = params["vars"] {:ok, id} = Glific.parse_maybe_integer(id) case Repo.fetch_by(InteractiveTemplate, %{id: id}) do {:ok, interactive_template} -> %{ id: interactive_template.id, name: interactive_template.label, type: interactive_template.type, interactive_content: interactive_template.interactive_content, created_on: interactive_template.inserted_at, modified_on: interactive_template.updated_at, translations: get_interactive_translations(interactive_template.translations) } |> then(&json(conn, &1)) {:error, _} -> json(conn, %{error: "Interactive message not found"}) end end @spec get_interactive_translations(map) :: map() defp get_interactive_translations(interactive_translations) do language_map = Settings.get_language_id_local_map() interactive_translations |> Enum.map(fn {language_id, value} -> %{language_map[language_id] => value} end) |> Enum.reduce(%{}, fn translation, acc -> Map.merge(acc, translation) end) end @doc false @spec templates(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def templates(conn, _params) do results = Glific.Templates.list_session_templates(%{ filter: %{organization_id: conn.assigns[:organization_id], status: "APPROVED"} }) |> Enum.reduce([], fn template, acc -> template = Glific.Repo.preload(template, :language) language = template.language [ %{ uuid: template.uuid, name: template.label, created_on: template.inserted_at, modified_on: template.updated_at, translations: Enum.concat( [ %{ language: language.locale, content: template.body, variable_count: template.number_parameters, status: "approved", channel: %{uuid: "", name: "WhatsApp"} } ], get_template_translations(template.translations) ) } | acc ] end) json(conn, %{results: results}) end @spec get_template_translations(nil | map) :: list() defp get_template_translations(nil), do: [] defp get_template_translations(template_translations) do language_map = Settings.get_language_id_local_map() template_translations |> Enum.reduce([], fn {language_id, translation}, acc -> [ %{ content: translation["body"], variable_count: translation["number_parameters"], status: "approved", language: language_map[language_id], channel: %{uuid: "", name: "WhatsApp"} } | acc ] end) end @doc false @spec languages(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def languages(conn, _params) do results = Glific.Partners.organization(conn.assigns[:organization_id]).languages |> Enum.reduce([], fn language, acc -> [%{iso: language.locale, name: language.label} | acc] end) json(conn, %{results: results}) end @doc false @spec environment(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def environment(conn, _params) do environment = %{} json(conn, environment) end @doc false @spec recipients(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def recipients(conn, _params) do # we should return only staff contact ids here # ideally should only be able to send them an HSM template, so we need # this to be fixed in the frontend recipients = Contacts.list_user_contacts() |> Enum.reduce([], fn c, acc -> [%{id: "#{c.id}", name: c.name, type: "contact", extra: c.id} | acc] end) json(conn, %{results: recipients}) end @doc """ instead of reading a file we can call it directly from Assets. We will come back on that when we have more clarity of the use cases """ @spec completion(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def completion(conn, _params) do completion = File.read!(Path.join(:code.priv_dir(:glific), "data/flows/completion.json")) |> Jason.decode!() functions = File.read!(Path.join(:code.priv_dir(:glific), "data/flows/functions.json")) |> Jason.decode!() results = %{ context: completion, functions: functions } json(conn, results) end @doc """ This is used to checking if the connection between frontend and backend is established or not. """ @spec activity(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def activity(conn, params) do {nodes, segments, recent_messages} = FlowCount.get_flow_count_list(params["flow"]) |> Enum.reduce( {%{}, %{}, %{}}, fn fc, acc -> {nodes, segments, recent_messages} = acc case fc.type do "node" -> {Map.put(nodes, fc.uuid, fc.count), segments, recent_messages} "exit" -> key = "#{fc.uuid}:#{fc.destination_uuid}" { nodes, Map.put(segments, key, fc.count), Map.put(recent_messages, key, get_recent_message(fc)) } _ -> acc end end ) activity = %{nodes: nodes, segments: segments, recentMessages: recent_messages} json(conn, activity) end @spec get_recent_message(FlowCount.t()) :: list() defp get_recent_message(flow_count) do # flow editor shows only last 3 messages. We are just tacking 5 for the safe side. flow_count.recent_messages |> Enum.map(fn msg -> %{text: msg["message"], sent: msg["date"]} end) |> Enum.take(5) end @doc """ Let's get all the flows or a latest flow revision """ @spec flows(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def flows(conn, %{"vars" => vars}) do results = case vars do [] -> ## We need to fix this before merging this branch Flows.list_flows(%{ filter: %{organization_id: conn.assigns[:organization_id], status: "published"} }) |> Enum.reduce([], fn flow, acc -> [ %{ uuid: flow.uuid, name: flow.name, type: flow.flow_type, archived: false, labels: [], expires: 10_080, parent_refs: [] } | acc ] end) [flow_uuid] -> with {:ok, flow} <- Repo.fetch_by(Flow, %{ uuid: flow_uuid, organization_id: conn.assigns[:organization_id] }), do: Flow.get_latest_definition(flow.id) end json(conn, %{results: results}) end @doc """ Get all or a specific revision for a flow """ @spec revisions(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def revisions(conn, %{"vars" => vars}) do case vars do [flow_uuid] -> json(conn, Flows.get_flow_revision_list(flow_uuid)) [flow_uuid, revision_id] -> json(conn, Flows.get_flow_revision(flow_uuid, revision_id)) end end @doc """ Save a revision for a flow and get the revision id """ @spec save_revisions(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def save_revisions(conn, params) do revision = Flows.create_flow_revision(params) json(conn, %{revision: revision.id}) end @doc """ Validate media to send as attachment """ @spec validate_media(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def validate_media(conn, params) do json(conn, Glific.Messages.validate_media(params["url"], params["type"])) end @doc false @spec attachments_enabled(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def attachments_enabled(conn, _) do organization_id = conn.assigns[:organization_id] json(conn, %{is_enabled: Partners.attachments_enabled?(organization_id)}) end @doc false @spec flow_attachment(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def flow_attachment(conn, %{"media" => media, "extension" => extension} = _params) do organization_id = conn.assigns[:organization_id] remote_name = conn.assigns[:current_user] |> remote_name(extension) res = GcsWorker.upload_media(media.path, remote_name, organization_id) |> case do {:ok, media} -> %{url: media.url, type: media.type, error: nil} {:error, error} -> %{url: nil, error: error} end json(conn, res) end @doc false @spec sheets(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def sheets(conn, _params) do results = Sheets.list_sheets(%{ filter: %{organization_id: conn.assigns[:organization_id], is_active: true} }) |> Enum.reduce([], fn sheet, acc -> [ %{ id: sheet.id, name: sheet.label, url: sheet.url } | acc ] end) json(conn, %{results: results}) end @doc false @spec recents(Plug.Conn.t(), nil | maybe_improper_list | map) :: Plug.Conn.t() def recents(conn, params) do [exit_uuid, destination_uuid, flow_uuid] = params["vars"] {:ok, flow_count} = Repo.fetch_by(FlowCount, %{ uuid: exit_uuid, destination_uuid: destination_uuid, flow_uuid: flow_uuid, organization_id: conn.assigns[:organization_id] }) results = Enum.reduce(flow_count.recent_messages, [], fn recent_message, acc -> [ %{ contact: recent_message["contact"], operand: recent_message["message"], time: recent_message["date"] } | acc ] end) json(conn, results) end @doc false @spec generate_uuid() :: String.t() defp generate_uuid do Ecto.UUID.generate() end @spec remote_name(User.t() | nil, String.t(), Ecto.UUID.t()) :: String.t() defp remote_name(user, extension, uuid \\ Ecto.UUID.generate()) do {year, week} = Timex.iso_week(Timex.now()) "outbound/#{year}-#{week}/#{user.name}/#{uuid}.#{extension}" end end