diff options
author | href <href@random.sh> | 2020-03-11 21:18:34 +0100 |
---|---|---|
committer | href <href@random.sh> | 2020-03-11 21:18:34 +0100 |
commit | a28d24470ddeca6196219a1333c1ccac1319efef (patch) | |
tree | 4f29e3c8fb6afbb1f99d6b8737f844c95fca54df /lib/lsg_irc/link_plugin.ex | |
parent | up to 420*100 (diff) |
welp
Diffstat (limited to 'lib/lsg_irc/link_plugin.ex')
-rw-r--r-- | lib/lsg_irc/link_plugin.ex | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/lib/lsg_irc/link_plugin.ex b/lib/lsg_irc/link_plugin.ex new file mode 100644 index 0000000..61bdbf9 --- /dev/null +++ b/lib/lsg_irc/link_plugin.ex @@ -0,0 +1,172 @@ +defmodule LSG.IRC.LinkPlugin do + @moduledoc """ + # Link Previewer + + An extensible link previewer for IRC. + + To extend the supported sites, create a new handler implementing the callbacks. + + See `link_plugin/` directory for examples. The first in list handler that returns true to the `match/2` callback will be used, + and if the handler returns `:error` or crashes, will fallback to the default preview. + + Unsupported websites will use the default link preview method, which is for html document the title, otherwise it'll use + the mimetype and size. + + ## Configuration: + + ``` + config :lsg, LSG.IRC.LinkPlugin, + handlers: [ + LSG.IRC.LinkPlugin.Youtube: [ + invidious: true + ], + LSG.IRC.LinkPlugin.Twitter: [], + LSG.IRC.LinkPlugin.Imgur: [], + ] + ``` + + """ + + @ircdoc """ + # Link preview + + Previews links (just post a link!). + + Announces real URL after redirections and provides extended support for YouTube, Twitter and Imgur. + """ + def short_irc_doc, do: false + def irc_doc, do: @ircdoc + require Logger + + def start_link() do + GenServer.start_link(__MODULE__, []) + end + + @callback match(uri :: URI.t, options :: Keyword.t) :: {true, params :: Map.t} | false + @callback expand(uri :: URI.t, params :: Map.t, options :: Keyword.t) :: {:ok, lines :: [] | String.t} | :error + + defstruct [:client] + + def init([]) do + {:ok, _} = Registry.register(IRC.PubSub, "message", []) + Logger.info("Link handler started") + {:ok, %__MODULE__{}} + end + + def handle_info({:irc, :text, message = %{text: text}}, state) do + String.split(text) + |> Enum.map(fn(word) -> + if String.starts_with?(word, "http://") || String.starts_with?(word, "https://") do + uri = URI.parse(word) + if uri.scheme && uri.host do + spawn(fn() -> + case expand_link([uri]) do + {:ok, uris, text} -> + text = case uris do + [uri] -> text + [uri | _] -> ["-> #{URI.to_string(uri)}", text] + end + message.replyfun.(text) + _ -> nil + end + end) + end + end + end) + {:noreply, state} + end + + def handle_info(msg, state) do + {:noreply, state} + end + + def terminate(_reason, state) do + :ok + end + + # 1. Match the first valid handler + # 2. Try to run the handler + # 3. If :error or crash, default link. + # If :skip, nothing + # 4. ? + + # Over five redirections: cancel. + def expand_link([_, _, _, _, _, _ | _]) do + :error + end + + def expand_link(acc=[uri | _]) do + handlers = Keyword.get(Application.get_env(:lsg, __MODULE__, [handlers: []]), :handlers) + handler = Enum.reduce_while(handlers, nil, fn({module, opts}, acc) -> + module = Module.concat([module]) + case module.match(uri, opts) do + {true, params} -> {:halt, {module, params, opts}} + false -> {:cont, acc} + end + end) + run_expand(acc, handler) + end + + def run_expand(acc, nil) do + expand_default(acc) + end + + def run_expand(acc=[uri|_], {module, params, opts}) do + case module.expand(uri, params, opts) do + {:ok, data} -> {:ok, acc, data} + :error -> expand_default(acc) + :skip -> nil + end + rescue + e -> + Logger.error(inspect(e)) + expand_default(acc) + catch + e, b -> + Logger.error(inspect({b})) + expand_default(acc) + end + + def expand_default(acc = [uri = %URI{scheme: scheme} | _]) when scheme in ["http", "https"] do + headers = [] + options = [follow_redirect: false, max_body_length: 30_000_000] + case HTTPoison.get(URI.to_string(uri), headers, options) do + {:ok, %HTTPoison.Response{status_code: 200, headers: headers, body: body}} -> + headers = Enum.reduce(headers, %{}, fn({key, value}, acc) -> + Map.put(acc, String.downcase(key), value) + end) + text = case Map.get(headers, "content-type") do + "text/html"<>_ -> + html = Floki.parse(body) + case Floki.find(html, "title") do + [{"title", [], [title]} | _] -> + title + _ -> + nil + end + other -> + "file: #{other}, size: #{Map.get(headers, "content-length", "?")} bytes" + end + {:ok, acc, text} + {:ok, resp = %HTTPoison.Response{headers: headers, status_code: redirect, body: body}} when redirect in 300..399 -> + headers = Enum.reduce(headers, %{}, fn({key, value}, acc) -> + Map.put(acc, String.downcase(key), value) + end) + link = Map.get(headers, "location") + new_uri = URI.parse(link) + expand_link([new_uri | acc]) + {:ok, %HTTPoison.Response{status_code: code}} -> + {:ok, acc, "Error #{code}"} + {:error, %HTTPoison.Error{reason: reason}} -> + {:ok, acc, "Error #{to_string(reason)}"} + {:error, error} -> + {:ok, acc, "Error #{inspect(error)}"} + end + end + + # Unsupported scheme, came from a redirect. + def expand_default(acc = [uri | _]) do + {:ok, [uri], "-> #{URI.to_string(uri)}"} + end + +end |