# Copyright 2019 Hubert Chathi # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Polyjuice.Client.MsgBuilder do alias Polyjuice.Client.MsgBuilder, as: MsgBuilder @moduledoc """ Build a message out of composable parts. """ @doc ~S""" Escape special HTML characters. Returns an iodata. Examples: iex> Polyjuice.Client.MsgBuilder.html_escape("a<>&\"", false) ...> |> IO.iodata_to_binary() "a<>&\"" iex> Polyjuice.Client.MsgBuilder.html_escape("a<>&b\"", true) ...> |> IO.iodata_to_binary() "a<>&b"" """ @spec html_escape(str :: String.t(), escape_quotes :: boolean) :: iodata def html_escape("", esc_q) when is_boolean(esc_q), do: "" for {char, seq} <- Enum.zip('<>&', ["lt", "gt", "amp"]) do def html_escape(<> <> rest, esc_q) when is_boolean(esc_q) do [unquote("&" <> seq <> ";") | html_escape(rest, esc_q)] end end def html_escape(<> <> rest, true) do """ <> html_escape(rest, true) end def html_escape(str, esc_q) when is_boolean(esc_q) do size = chunk_size(str, esc_q, 0) <> = str [chunk | html_escape(rest, esc_q)] end defp chunk_size("", _, acc), do: acc defp chunk_size(<> <> _, _, acc) when c in '<>&', do: acc defp chunk_size(<> <> _, true, acc), do: acc defp chunk_size(<<_>> <> rest, esc_q, acc), do: chunk_size(rest, esc_q, acc + 1) defprotocol MsgData do def to_text(msg) def to_html(msg) end defimpl Polyjuice.Client.MsgBuilder.MsgData, for: List do def to_text([]), do: {"", false} def to_text([head | tail]) do {head_text, head_html_differs} = MsgData.to_text(head) {tail_text, tail_html_differs} = MsgData.to_text(tail) {[head_text | tail_text], head_html_differs or tail_html_differs} end def to_html([]), do: "" def to_html([head | tail]) do [MsgData.to_html(head) | MsgData.to_html(tail)] end end defimpl Polyjuice.Client.MsgBuilder.MsgData, for: BitString do def to_text(str), do: {str, false} def to_html(str) do MsgBuilder.html_escape(str, false) |> IO.iodata_to_binary() |> String.replace("\n", "
") end end defimpl Polyjuice.Client.MsgBuilder.MsgData, for: Tuple do def to_text({text, _}) when is_binary(text) or is_list(text), do: {text, true} def to_html({_, html}) when is_binary(html) or is_list(html), do: html end defmodule Link do @moduledoc false defstruct [:href, :contents] defimpl Polyjuice.Client.MsgBuilder.MsgData do def to_text(%{href: href, contents: contents}) do {contents_text, _} = MsgData.to_text(contents) { [?[, contents_text, "](", href, ?)], true } end def to_html(%{href: href, contents: contents}) do [ "", MsgData.to_html(contents), "" ] end end end @doc ~S""" Generate a Matrix message contents from message data. Examples: iex> Polyjuice.Client.MsgBuilder.to_message("foo") %{"msgtype" => "m.text", "body" => "foo"} iex> Polyjuice.Client.MsgBuilder.to_message(["foo", "bar"]) %{"msgtype" => "m.text", "body" => "foobar"} """ def to_message(msgdata, msgtype \\ "m.text") do {html, html_differs} = MsgData.to_text(msgdata) body = IO.iodata_to_binary(html) if html_differs do %{ "msgtype" => msgtype, "body" => body, "format" => "org.matrix.custom.html", "formatted_body" => MsgData.to_html(msgdata) |> IO.iodata_to_binary() } else %{ "msgtype" => msgtype, "body" => body } end end @doc ~S""" Mention a user. Examples: iex> Polyjuice.Client.MsgBuilder.to_message( ...> Polyjuice.Client.MsgBuilder.mention("@alice:example.com", "Alice") ...> ) %{ "msgtype" => "m.text", "body" => "Alice", "format" => "org.matrix.custom.html", "formatted_body" => "Alice" } iex> Polyjuice.Client.MsgBuilder.to_message( ...> Polyjuice.Client.MsgBuilder.mention("@alice:example.com") ...> ) %{ "msgtype" => "m.text", "body" => "@alice:example.com", "format" => "org.matrix.custom.html", "formatted_body" => "@alice:example.com" } """ @spec mention(user_id :: String.t(), display_name :: String.t() | nil) :: MsgData.t() def mention(user_id, display_name \\ nil) def mention(user_id, nil) when is_binary(user_id) do mention(user_id, user_id) end def mention(user_id, display_name) when is_binary(user_id) and is_binary(display_name) do { display_name, [ "", MsgBuilder.html_escape(display_name, false), "" ] } end @doc ~S""" Create an image. Examples: iex> Polyjuice.Client.MsgBuilder.to_message( ...> Polyjuice.Client.MsgBuilder.image("mxc://example.com/foo") ...> ) %{ "msgtype" => "m.text", "body" => "[mxc://example.com/foo]", "format" => "org.matrix.custom.html", "formatted_body" => "" } iex> Polyjuice.Client.MsgBuilder.to_message( ...> Polyjuice.Client.MsgBuilder.image("mxc://example.com/foo", alt: "some image") ...> ) %{ "msgtype" => "m.text", "body" => "some image", "format" => "org.matrix.custom.html", "formatted_body" => "\"some" } iex> Polyjuice.Client.MsgBuilder.to_message( ...> Polyjuice.Client.MsgBuilder.image( ...> "mxc://example.com/foo", alt: "some image", width: 100 ...> ) ...> ) %{ "msgtype" => "m.text", "body" => "some image", "format" => "org.matrix.custom.html", "formatted_body" => "\"some" } """ @spec image(src :: String.t(), attrs :: list()) :: MsgData.t() def image(src, attrs \\ []) when is_binary(src) and is_list(attrs) do {alt, rest} = Keyword.pop(attrs, :alt) text = if alt != nil, do: alt, else: [?[, src, ?]] html = [ " [ MsgBuilder.html_escape(to_string(name), true), "=\"", MsgBuilder.html_escape(to_string(value), true), "\" " ] end), "/>" ] {text, html} end @doc ~S""" Create a link. Examples: iex> Polyjuice.Client.MsgBuilder.to_message( ...> Polyjuice.Client.MsgBuilder.link( ...> Polyjuice.Client.MsgBuilder.image( ...> "mxc://example.com/foo", alt: "some image", width: 100 ...> ), ...> "https://matrix.org/" ...> ) ...> ) %{ "msgtype" => "m.text", "body" => "[some image](https://matrix.org/)", "format" => "org.matrix.custom.html", "formatted_body" => "\"some" } """ @spec link(contents :: MsgData.t(), href :: String.t()) :: MsgData.t() def link(contents, href) when is_binary(href) do %Link{href: href, contents: contents} end end