summaryrefslogtreecommitdiff
path: root/lib/lsg_irc
diff options
context:
space:
mode:
Diffstat (limited to '')
l---------lib/lsg_irc/.#youtube_handler.ex1
-rw-r--r--lib/lsg_irc/alcolog_plugin.ex200
-rw-r--r--lib/lsg_irc/alcoolisme_plugin.ex198
-rw-r--r--lib/lsg_irc/base_handler.ex35
-rw-r--r--lib/lsg_irc/base_plugin.ex35
-rw-r--r--lib/lsg_irc/coronavirus_plugin.ex131
-rw-r--r--lib/lsg_irc/dice_handler.ex73
-rw-r--r--lib/lsg_irc/dice_plugin.ex66
-rw-r--r--lib/lsg_irc/finance_plugin.ex189
-rw-r--r--lib/lsg_irc/kick_roulette_handler.ex32
-rw-r--r--lib/lsg_irc/kick_roulette_plugin.ex32
-rw-r--r--lib/lsg_irc/last_fm_plugin.ex (renamed from lib/lsg_irc/last_fm_handler.ex)60
-rw-r--r--lib/lsg_irc/link_plugin.ex172
-rw-r--r--lib/lsg_irc/link_plugin/imgur.ex84
-rw-r--r--lib/lsg_irc/link_plugin/twitter.ex91
-rw-r--r--lib/lsg_irc/link_plugin/youtube.ex69
-rw-r--r--lib/lsg_irc/outline_plugin.ex72
-rw-r--r--lib/lsg_irc/preums_plugin.ex108
-rw-r--r--lib/lsg_irc/quatre_cent_vingt_plugin.ex1
-rw-r--r--lib/lsg_irc/txt_plugin.ex (renamed from lib/lsg_irc/txt_handler.ex)238
-rw-r--r--lib/lsg_irc/txt_plugin/markov.ex9
-rw-r--r--lib/lsg_irc/txt_plugin/markov_native.ex33
-rw-r--r--lib/lsg_irc/txt_plugin/markov_py_markovify.ex39
-rw-r--r--lib/lsg_irc/wikipedia_plugin.ex4
-rw-r--r--lib/lsg_irc/youtube_handler.ex75
-rw-r--r--lib/lsg_irc/youtube_plugin.ex105
26 files changed, 1821 insertions, 331 deletions
diff --git a/lib/lsg_irc/.#youtube_handler.ex b/lib/lsg_irc/.#youtube_handler.ex
new file mode 120000
index 0000000..0557ec1
--- /dev/null
+++ b/lib/lsg_irc/.#youtube_handler.ex
@@ -0,0 +1 @@
+href@echoes.1780 \ No newline at end of file
diff --git a/lib/lsg_irc/alcolog_plugin.ex b/lib/lsg_irc/alcolog_plugin.ex
new file mode 100644
index 0000000..b5392a3
--- /dev/null
+++ b/lib/lsg_irc/alcolog_plugin.ex
@@ -0,0 +1,200 @@
+defmodule LSG.IRC.AlcologPlugin do
+ require Logger
+
+ @moduledoc """
+ # alcoolisme _(v2)_
+
+ * **`!santai <montant> <degrés d'alcool> [annotation]`** enregistre un nouveau verre de `montant` d'une boisson à `degrés d'alcool`.
+
+ * **!`<trigger>` `[coeff]` `[annotation]`** enregistre de l'alcoolisme.
+ * **!alcoolisme `[pseudo]`** affiche les points d'alcoolisme.
+
+ Triggers/Coeffs:
+
+ * bière: (coeffs: 25, 50/+, 75/++, 100/+++, ++++)
+ * pinard: (coeffs: +, ++, +++, ++++)
+ * shot/fort/whisky/rhum/..: (coeffs: +, ++, +++, ++++)
+ * eau (-1)
+
+ Annotation: champ libre!
+
+ """
+
+ @triggers %{
+ "apero" =>
+ %{
+ triggers: ["apero", "apéro", "apairo", "santai"],
+ default_coeff: "25",
+ coeffs: %{
+ "+" => 2,
+ "++" => 3,
+ "+++" => 4,
+ "++++" => 5,
+ }
+ },
+ "bière" =>
+ %{
+ triggers: ["beer", "bière", "biere", "biaire"],
+ default_coeff: "25",
+ coeffs: %{
+ "25" => 1,
+ "50" => 2,
+ "75" => 3,
+ "100" => 4,
+ "+" => 2,
+ "++" => 3,
+ "+++" => 4,
+ "++++" => 5,
+ }
+ },
+ "pinard" => %{
+ triggers: ["pinard", "vin", "rouge", "blanc", "rosé", "rose"],
+ annotations: ["rouge", "blanc", "rosé", "rose"],
+ default_coeff: "",
+ coeffs: %{
+ "" => 1,
+ "+" => 2,
+ "++" => 3,
+ "+++" => 4,
+ "++++" => 5,
+ "vase" => 6,
+ }
+ },
+ "fort" => %{
+ triggers: ["shot", "royaume", "whisky", "rhum", "armagnac", "dijo"],
+ default_coeff: "",
+ coeffs: %{
+ "" => 3,
+ "+" => 5,
+ "++" => 7,
+ "+++" => 9,
+ "++++" => 11
+ }
+ },
+ "eau" => %{
+ triggers: ["eau"],
+ default_coeff: "1",
+ coeffs: %{
+ "1" => -2,
+ "+" => -3,
+ "++" => -4,
+ "+++" => -6,
+ "++++" => -8
+ }
+ }
+ }
+
+ @santai ["SANTÉ", "SANTÉ", "SANTAIIII", "SANTAIIIIIIIIII", "SANTAI", "A LA TIENNE"]
+
+ def irc_doc, do: @moduledoc
+
+ def start_link(), do: GenServer.start_link(__MODULE__, [])
+
+ def init(_) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:alcoolisme", [])
+ for {_, config} <- @triggers do
+ for trigger <- config.triggers do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{trigger}", [])
+ for coeff when byte_size(coeff) > 0 <- Map.keys(config.coeffs) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{trigger}#{coeff}", [])
+ end
+ end
+ end
+ dets_filename = (LSG.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
+ {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
+ {:ok, dets}
+ end
+
+ for {name, config} <- @triggers do
+ coeffs = Map.get(config, "coeffs")
+ default_coeff_value = Map.get(config.coeffs, config.default_coeff)
+
+
+ IO.puts "at triggers #{inspect config}"
+ # Handle each trigger
+ for trigger <- config.triggers do
+
+ # … with a known coeff …
+ for {coef, value} when byte_size(coef) > 0 <- config.coeffs do
+ def handle_info({:irc, :trigger, unquote(trigger), m = %IRC.Message{trigger: %IRC.Trigger{args: [unquote(coef)<>_ | args], type: :bang}}}, dets) do
+ handle_bang(unquote(name), unquote(value), m, args, dets)
+ {:noreply, dets}
+ end
+ def handle_info({:irc, :trigger, unquote(trigger)<>unquote(coef), m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, dets) do
+ handle_bang(unquote(name), unquote(value), m, args, dets)
+ {:noreply, dets}
+ end
+ end
+
+ # … or without
+ def handle_info({:irc, :trigger, unquote(trigger), m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, dets) do
+ handle_bang(unquote(name), unquote(default_coeff_value), m, args, dets)
+ {:noreply, dets}
+ end
+
+ end
+
+ end
+
+ def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, dets) do
+ nick = case args do
+ [nick] -> nick
+ [] -> m.sender.nick
+ end
+ case get_statistics_for_nick(dets, nick) do
+ {count, {_, last_at, last_points, last_type, last_descr}} ->
+ m.replyfun.("#{nick} a #{count} points d'alcoolisme. Dernier verre: #{present_type(last_type,
+ last_descr)} [#{last_points}] #{format_relative_timestamp(last_at)}")
+ _ ->
+ m.replyfun.("honteux mais #{nick} n'a pas l'air alcoolique du tout. /kick")
+ end
+ {:noreply, dets}
+ end
+
+ defp handle_bang(name, points, message, args, dets) do
+ description = case args do
+ [] -> nil
+ [something] -> something
+ something when is_list(something) -> Enum.join(something, " ")
+ _ -> nil
+ end
+ now = DateTime.to_unix(DateTime.utc_now(), :milliseconds)
+ :ok = :dets.insert(dets, {String.downcase(message.sender.nick), now, points, name, description})
+ {count, {_, last_at, last_points, last_type, last_descr}} = get_statistics_for_nick(dets, message.sender.nick)
+ sante = @santai |> Enum.shuffle() |> Enum.random()
+ message.replyfun.("#{sante} #{message.sender.nick} #{format_points(points)}! (total #{count} points)")
+ end
+
+ defp get_statistics_for_nick(dets, nick) do
+ qvc = :dets.lookup(dets, String.downcase(nick))
+ IO.puts inspect(qvc)
+ count = Enum.reduce(qvc, 0, fn({_nick, _ts, points, _type, _descr}, acc) -> acc + points end)
+ last = List.last(qvc) || nil
+ {count, last}
+ end
+
+ def present_type(type, descr) when descr in [nil, ""], do: "#{type}"
+ def present_type(type, description), do: "#{type} (#{description})"
+
+ def format_points(int) when int > 0 do
+ "+#{Integer.to_string(int)}"
+ end
+ def format_points(int) when int < 0 do
+ Integer.to_string(int)
+ end
+ def format_points(0), do: "0"
+
+ defp format_relative_timestamp(timestamp) do
+ alias Timex.Format.DateTime.Formatters
+ alias Timex.Timezone
+ date = timestamp
+ |> DateTime.from_unix!(:milliseconds)
+ |> Timezone.convert("Europe/Paris")
+
+ {:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
+ {:ok, detail} = Formatters.Default.lformat(date, " ({h24}:{m})", "fr")
+
+ relative <> detail
+ end
+end
+
diff --git a/lib/lsg_irc/alcoolisme_plugin.ex b/lib/lsg_irc/alcoolisme_plugin.ex
new file mode 100644
index 0000000..03dc75b
--- /dev/null
+++ b/lib/lsg_irc/alcoolisme_plugin.ex
@@ -0,0 +1,198 @@
+defmodule LSG.IRC.AlcoolismePlugin do
+ require Logger
+
+ @moduledoc """
+ # Alcoolisme
+
+ * **!`<trigger>` `[coeff]` `[annotation]`** enregistre de l'alcoolisme.
+ * **!alcoolisme `[pseudo]`** affiche les points d'alcoolisme.
+
+ Triggers/Coeffs:
+
+ * bière: (coeffs: 25, 50/+, 75/++, 100/+++, ++++)
+ * pinard: (coeffs: +, ++, +++, ++++)
+ * shot/fort/whisky/rhum/..: (coeffs: +, ++, +++, ++++)
+ * eau (-1)
+
+ Annotation: champ libre!
+
+ """
+
+ @triggers %{
+ "apero" =>
+ %{
+ triggers: ["apero", "apéro", "apairo", "santai"],
+ default_coeff: "25",
+ coeffs: %{
+ "+" => 2,
+ "++" => 3,
+ "+++" => 4,
+ "++++" => 5,
+ }
+ },
+ "bière" =>
+ %{
+ triggers: ["beer", "bière", "biere", "biaire"],
+ default_coeff: "25",
+ coeffs: %{
+ "25" => 1,
+ "50" => 2,
+ "75" => 3,
+ "100" => 4,
+ "+" => 2,
+ "++" => 3,
+ "+++" => 4,
+ "++++" => 5,
+ }
+ },
+ "pinard" => %{
+ triggers: ["pinard", "vin", "rouge", "blanc", "rosé", "rose"],
+ annotations: ["rouge", "blanc", "rosé", "rose"],
+ default_coeff: "",
+ coeffs: %{
+ "" => 1,
+ "+" => 2,
+ "++" => 3,
+ "+++" => 4,
+ "++++" => 5,
+ "vase" => 6,
+ }
+ },
+ "fort" => %{
+ triggers: ["shot", "royaume", "whisky", "rhum", "armagnac", "dijo"],
+ default_coeff: "",
+ coeffs: %{
+ "" => 3,
+ "+" => 5,
+ "++" => 7,
+ "+++" => 9,
+ "++++" => 11
+ }
+ },
+ "eau" => %{
+ triggers: ["eau"],
+ default_coeff: "1",
+ coeffs: %{
+ "1" => -2,
+ "+" => -3,
+ "++" => -4,
+ "+++" => -6,
+ "++++" => -8
+ }
+ }
+ }
+
+ @santai ["SANTÉ", "SANTÉ", "SANTAIIII", "SANTAIIIIIIIIII", "SANTAI", "A LA TIENNE"]
+
+ def irc_doc, do: @moduledoc
+
+ def start_link(), do: GenServer.start_link(__MODULE__, [])
+
+ def init(_) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:alcoolisme", [])
+ for {_, config} <- @triggers do
+ for trigger <- config.triggers do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{trigger}", [])
+ for coeff when byte_size(coeff) > 0 <- Map.keys(config.coeffs) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{trigger}#{coeff}", [])
+ end
+ end
+ end
+ dets_filename = (LSG.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
+ {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
+ {:ok, dets}
+ end
+
+ for {name, config} <- @triggers do
+ coeffs = Map.get(config, "coeffs")
+ default_coeff_value = Map.get(config.coeffs, config.default_coeff)
+
+
+ IO.puts "at triggers #{inspect config}"
+ # Handle each trigger
+ for trigger <- config.triggers do
+
+ # … with a known coeff …
+ for {coef, value} when byte_size(coef) > 0 <- config.coeffs do
+ def handle_info({:irc, :trigger, unquote(trigger), m = %IRC.Message{trigger: %IRC.Trigger{args: [unquote(coef)<>_ | args], type: :bang}}}, dets) do
+ handle_bang(unquote(name), unquote(value), m, args, dets)
+ {:noreply, dets}
+ end
+ def handle_info({:irc, :trigger, unquote(trigger)<>unquote(coef), m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, dets) do
+ handle_bang(unquote(name), unquote(value), m, args, dets)
+ {:noreply, dets}
+ end
+ end
+
+ # … or without
+ def handle_info({:irc, :trigger, unquote(trigger), m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, dets) do
+ handle_bang(unquote(name), unquote(default_coeff_value), m, args, dets)
+ {:noreply, dets}
+ end
+
+ end
+
+ end
+
+ def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, dets) do
+ nick = case args do
+ [nick] -> nick
+ [] -> m.sender.nick
+ end
+ case get_statistics_for_nick(dets, nick) do
+ {count, {_, last_at, last_points, last_type, last_descr}} ->
+ m.replyfun.("#{nick} a #{count} points d'alcoolisme. Dernier verre: #{present_type(last_type,
+ last_descr)} [#{last_points}] #{format_relative_timestamp(last_at)}")
+ _ ->
+ m.replyfun.("honteux mais #{nick} n'a pas l'air alcoolique du tout. /kick")
+ end
+ {:noreply, dets}
+ end
+
+ defp handle_bang(name, points, message, args, dets) do
+ description = case args do
+ [] -> nil
+ [something] -> something
+ something when is_list(something) -> Enum.join(something, " ")
+ _ -> nil
+ end
+ now = DateTime.to_unix(DateTime.utc_now(), :milliseconds)
+ :ok = :dets.insert(dets, {String.downcase(message.sender.nick), now, points, name, description})
+ {count, {_, last_at, last_points, last_type, last_descr}} = get_statistics_for_nick(dets, message.sender.nick)
+ sante = @santai |> Enum.shuffle() |> Enum.random()
+ message.replyfun.("#{sante} #{message.sender.nick} #{format_points(points)}! (total #{count} points)")
+ end
+
+ defp get_statistics_for_nick(dets, nick) do
+ qvc = :dets.lookup(dets, String.downcase(nick))
+ IO.puts inspect(qvc)
+ count = Enum.reduce(qvc, 0, fn({_nick, _ts, points, _type, _descr}, acc) -> acc + points end)
+ last = List.last(qvc) || nil
+ {count, last}
+ end
+
+ def present_type(type, descr) when descr in [nil, ""], do: "#{type}"
+ def present_type(type, description), do: "#{type} (#{description})"
+
+ def format_points(int) when int > 0 do
+ "+#{Integer.to_string(int)}"
+ end
+ def format_points(int) when int < 0 do
+ Integer.to_string(int)
+ end
+ def format_points(0), do: "0"
+
+ defp format_relative_timestamp(timestamp) do
+ alias Timex.Format.DateTime.Formatters
+ alias Timex.Timezone
+ date = timestamp
+ |> DateTime.from_unix!(:milliseconds)
+ |> Timezone.convert("Europe/Paris")
+
+ {:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
+ {:ok, detail} = Formatters.Default.lformat(date, " ({h24}:{m})", "fr")
+
+ relative <> detail
+ end
+end
+
diff --git a/lib/lsg_irc/base_handler.ex b/lib/lsg_irc/base_handler.ex
deleted file mode 100644
index 35b2ade..0000000
--- a/lib/lsg_irc/base_handler.ex
+++ /dev/null
@@ -1,35 +0,0 @@
-defmodule LSG.IRC.BaseHandler do
-
- def irc_doc, do: nil
-
- def start_link(client) do
- GenServer.start_link(__MODULE__, [client])
- end
-
- def init([client]) do
- ExIRC.Client.add_handler client, self
- {:ok, client}
- end
-
- def handle_info({:received, "!help", %ExIRC.SenderInfo{nick: nick}, chan}, client) do
- url = LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :index)
- ExIRC.Client.msg(client, :privmsg, chan, "#{nick}: #{url}")
- {:noreply, client}
- end
-
- def handle_info({:received, "version", %ExIRC.SenderInfo{nick: nick}}, client) do
- {:ok, vsn} = :application.get_key(:lsg, :vsn)
- ver = List.to_string(vsn)
- url = LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :index)
- version = "v#{ver} ; #{url} ; source: https://git.yt/115ans/sys"
- ExIRC.Client.msg(client, :privmsg, nick, version)
- {:noreply, client}
- end
-
- def handle_info(msg, client) do
- IO.inspect(msg)
- {:noreply, client}
- end
-
-end
-
diff --git a/lib/lsg_irc/base_plugin.ex b/lib/lsg_irc/base_plugin.ex
new file mode 100644
index 0000000..c0cfc59
--- /dev/null
+++ b/lib/lsg_irc/base_plugin.ex
@@ -0,0 +1,35 @@
+defmodule LSG.IRC.BasePlugin do
+
+ def irc_doc, do: nil
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:version", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:help", [])
+ {:ok, nil}
+ end
+
+ def handle_info({:irc, :trigger, "help", message = %{trigger: %{type: :bang}}}, _) do
+ url = LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :index)
+ message.replyfun.(url)
+ {:noreply, nil}
+ end
+
+ def handle_info({:irc, :trigger, "version", message = %{trigger: %{type: :bang}}}, _) do
+ {:ok, vsn} = :application.get_key(:lsg, :vsn)
+ ver = List.to_string(vsn)
+ url = LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :index)
+ version = "v#{ver} ; #{url} ; source: https://git.yt/115ans/sys"
+ message.replyfun.(version)
+ {:noreply, nil}
+ end
+
+ def handle_info(msg, _) do
+ {:noreply, nil}
+ end
+
+end
+
diff --git a/lib/lsg_irc/coronavirus_plugin.ex b/lib/lsg_irc/coronavirus_plugin.ex
new file mode 100644
index 0000000..9a017f3
--- /dev/null
+++ b/lib/lsg_irc/coronavirus_plugin.ex
@@ -0,0 +1,131 @@
+defmodule LSG.IRC.CoronavirusPlugin do
+ require Logger
+ @moduledoc """
+ # Corona Virus
+
+ Données de [Johns Hopkins University](https://github.com/CSSEGISandData/COVID-19) et mises à jour a peu près tous les jours.
+
+ * `!coronavirus [France | Country]`: :-)
+ * `!coronavirus`: top 10 confirmés et non guéris
+ * `!coronavirus confirmés`: top 10 confirmés
+ * `!coronavirus morts`: top 10 morts
+ * `!coronavirus soignés`: top 10 soignés
+ """
+ def irc_doc, do: @moduledoc
+
+ def start_link(), do: GenServer.start_link(__MODULE__, [])
+
+ def init(_) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:coronavirus", [])
+ {data, next} = fetch_data(%{})
+ :timer.send_after(next, :update)
+ {:ok, %{data: data}}
+ end
+
+ def handle_info(:update, state) do
+ {data, next} = fetch_data(state.data)
+ :timer.send_after(next, :update)
+ {:noreply, %{data: data}}
+ end
+
+ def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :bang, args: args}}}, state) when args in [[], ["morts"], ["confirmés"], ["soignés"], ["malades"]] do
+ {field, name} = case args do
+ ["confirmés"] -> {:confirmed, "confirmés"}
+ ["morts"] -> {:deaths, "morts"}
+ ["soignés"] -> {:recovered, "soignés"}
+ _ -> {:current, "malades"}
+ end
+ sorted = state.data
+ |> Enum.filter(fn({_, %{region: region}}) -> region == true end)
+ |> Enum.map(fn({location, data}) -> {location, Map.get(data, field, 0)} end)
+ |> Enum.sort_by(fn({_,count}) -> count end, &>=/2)
+ |> Enum.take(10)
+ |> Enum.with_index()
+ |> Enum.map(fn({{location, count}, index}) ->
+ "##{index+1}: #{location} #{count}"
+ end)
+ |> Enum.intersperse(" - ")
+ |> Enum.join()
+ m.replyfun.("Corona virus top 10 #{name}: " <> sorted)
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :bang, args: location}}}, state) do
+ location = Enum.join(location, " ") |> String.downcase()
+ if data = Map.get(state.data, location) do
+ m.replyfun.("Corona virus: #{location}: #{data.current} malades, #{data.confirmed} confirmés, #{data.deaths} morts, #{data.recovered} soignés")
+ end
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :query, args: location}}}, state) do
+ m.replyfun.("https://github.com/CSSEGISandData/COVID-19")
+ {:noreply, state}
+ end
+
+ # 1. Try to fetch data for today
+ # 2. Fetch yesterday if no results
+ defp fetch_data(current_data, date \\ nil) do
+ now = Date.utc_today()
+ url = fn(date) ->
+ "https://github.com/CSSEGISandData/COVID-19/raw/master/csse_covid_19_data/csse_covid_19_daily_reports/#{date}.csv"
+ end
+ request_date = date || now
+ Logger.debug("Coronavirus check date: #{inspect request_date}")
+ {:ok, date_s} = Timex.format({request_date.year, request_date.month, request_date.day}, "%m-%d-%Y", :strftime)
+ cur_url = url.(date_s)
+ Logger.debug "Fetching URL #{cur_url}"
+ case HTTPoison.get(cur_url, [], follow_redirect: true) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: csv}} ->
+ # Parse CSV update data
+ data = csv
+ |> String.strip()
+ |> String.split("\n")
+ |> Enum.drop(1)
+ |> Enum.reduce(%{}, fn(line, acc) ->
+ [state, region, update, confirmed, deaths, recovered,_lat, _lng] = line
+ |> String.strip()
+ |> String.split(",")
+
+ state = String.downcase(state)
+ region = String.downcase(region)
+ confirmed = String.to_integer(confirmed)
+ deaths = String.to_integer(deaths)
+ recovered = String.to_integer(recovered)
+
+ current = (confirmed - recovered) - deaths
+
+ entry = %{update: update, confirmed: confirmed, deaths: deaths, recovered: recovered, current: current, region: region}
+
+ acc = if state && state != "" do
+ Map.put(acc, state, entry)
+ else
+ acc
+ end
+
+ region_entry = Map.get(acc, region, %{update: nil, confirmed: 0, deaths: 0, recovered: 0, current: 0})
+ region_entry = %{
+ update: region_entry.update || update,
+ confirmed: region_entry.confirmed + confirmed,
+ deaths: region_entry.deaths + deaths,
+ current: region_entry.current + current,
+ recovered: region_entry.recovered + recovered,
+ region: true
+ }
+
+ Map.put(acc, region, region_entry)
+ end)
+ Logger.info "Updated coronavirus database"
+ {data, :timer.minutes(60)}
+ {:ok, %HTTPoison.Response{status_code: 404}} ->
+ Logger.debug "Corona 404 #{cur_url}"
+ date = Date.add(date || now, -1)
+ fetch_data(current_data, date)
+ other ->
+ Logger.error "Coronavirus: Update failed #{inspect other}"
+ {current_data, :timer.minutes(5)}
+ end
+ end
+
+end
+
diff --git a/lib/lsg_irc/dice_handler.ex b/lib/lsg_irc/dice_handler.ex
deleted file mode 100644
index 7ff7b4d..0000000
--- a/lib/lsg_irc/dice_handler.ex
+++ /dev/null
@@ -1,73 +0,0 @@
-defmodule LSG.IRC.DiceHandler do
- require Logger
-
- @moduledoc """
- # dice
-
- * **!dice `[1 | lancés]` `[6 | faces]`**: lance une ou plusieurs fois un dé de 6 ou autre faces
- """
-
- @default_faces 6
- @default_rolls 1
- @max_rolls 50
-
- def short_irc_doc, do: "!dice (jeter un dé)"
- defstruct client: nil, dets: nil
-
- def irc_doc, do: @moduledoc
-
- def start_link(client) do
- GenServer.start_link(__MODULE__, [client])
- end
-
- def init([client]) do
- ExIRC.Client.add_handler(client, self())
- {:ok, _} = Registry.register(IRC.PubSub, "dice", [])
- {:ok, %__MODULE__{client: client}}
- end
-
- def handle_info({:received, "!dice", sender, chan}, state) do
- roll(state, sender, chan, @default_faces, @default_rolls)
- {:noreply, state}
- end
-
- def handle_info({:received, "!dice "<>params, sender, chan}, state) do
- {rolls, faces} = case String.split(params, " ", parts: 2) do
- [faces, rolls] -> {rolls, faces}
- [rolls] -> {rolls, @default_faces}
- end
-
- to_integer = fn(string, default) ->
- case Integer.parse(string) do
- {int, _} -> int
- _ -> default
- end
- end
-
- {faces, rolls} = {to_integer.(faces, @default_faces), to_integer.(rolls, @default_rolls)}
-
- roll(state, sender, chan, faces, rolls)
-
- {:noreply, state}
- end
-
- def handle_info(info, state) do
- {:noreply, state}
- end
-
- defp roll(state, %{nick: nick}, chan, faces, 1) when faces > 0 do
- random = :crypto.rand_uniform(1, faces+1)
- ExIRC.Client.msg(state.client, :privmsg, chan, "#{nick} dice: #{random}")
- end
- defp roll(state, %{nick: nick}, chan, faces, rolls) when faces > 0 and rolls > 0 and rolls <= @max_rolls do
- {results, acc} = Enum.map_reduce(Range.new(1, rolls), 0, fn(i, acc) ->
- random = :crypto.rand_uniform(1, faces+1)
- {random, acc + random}
- end)
- results = Enum.join(results, "; ")
- ExIRC.Client.msg(state.client, :privmsg, chan, "#{nick} dice [#{acc}] #{results}")
- end
-
- defp roll(_, _, _, _, _), do: nil
-
-end
diff --git a/lib/lsg_irc/dice_plugin.ex b/lib/lsg_irc/dice_plugin.ex
new file mode 100644
index 0000000..a507b8e
--- /dev/null
+++ b/lib/lsg_irc/dice_plugin.ex
@@ -0,0 +1,66 @@
+defmodule LSG.IRC.DicePlugin do
+ require Logger
+
+ @moduledoc """
+ # dice
+
+ * **!dice `[1 | lancés]` `[6 | faces]`**: lance une ou plusieurs fois un dé de 6 ou autre faces
+ """
+
+ @default_faces 6
+ @default_rolls 1
+ @max_rolls 50
+
+ def short_irc_doc, do: "!dice (jeter un dé)"
+ defstruct client: nil, dets: nil
+
+ def irc_doc, do: @moduledoc
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:dice", [])
+ {:ok, %__MODULE__{}}
+ end
+
+ def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: args}}}, state) do
+ to_integer = fn(string, default) ->
+ case Integer.parse(string) do
+ {int, _} -> int
+ _ -> default
+ end
+ end
+
+ {rolls, faces} = case args do
+ [] -> {@default_rolls, @default_faces}
+ [faces, rolls] -> {to_integer.(rolls, @default_rolls), to_integer.(faces, @default_faces)}
+ [rolls] -> {to_integer.(rolls, @default_rolls), @default_faces}
+ end
+
+ roll(state, message, faces, rolls)
+
+ {:noreply, state}
+ end
+
+ def handle_info(info, state) do
+ {:noreply, state}
+ end
+
+ defp roll(state, message, faces, 1) when faces > 0 do
+ random = :crypto.rand_uniform(1, faces+1)
+ message.replyfun.("#{message.sender.nick} dice: #{random}")
+ end
+ defp roll(state, message, faces, rolls) when faces > 0 and rolls > 0 and rolls <= @max_rolls do
+ {results, acc} = Enum.map_reduce(Range.new(1, rolls), 0, fn(i, acc) ->
+ random = :crypto.rand_uniform(1, faces+1)
+ {random, acc + random}
+ end)
+ results = Enum.join(results, "; ")
+ message.replyfun.("#{message.sender.nick} dice: [#{acc}] #{results}")
+ end
+
+ defp roll(_, _, _, _, _), do: nil
+
+end
diff --git a/lib/lsg_irc/finance_plugin.ex b/lib/lsg_irc/finance_plugin.ex
new file mode 100644
index 0000000..7266a5e
--- /dev/null
+++ b/lib/lsg_irc/finance_plugin.ex
@@ -0,0 +1,189 @@
+defmodule LSG.IRC.FinancePlugin do
+ require Logger
+
+ @moduledoc """
+ # finance
+
+ Données de [alphavantage.co](https://alphavantage.co).
+
+ ## forex / monnaies / crypto-monnaies
+
+ * **`!forex <MONNAIE1> [MONNAIE2]`**: taux de change entre deux monnaies.
+ * **`!forex <MONTANT> <MONNAIE1> <MONNAIE2>`**: converti `montant` entre deux monnaies
+ * **`?currency <recherche>`**: recherche une monnaie
+
+ Utiliser le symbole des monnaies (EUR, USD, ...).
+
+ ## bourses
+
+ * **`!stocks <SYMBOLE>`**
+ * **`?stocks <recherche>`** cherche un symbole
+
+ Pour les symboles non-US, ajouter le suffixe (RNO Paris: RNO.PAR).
+
+ """
+
+ @currency_list "https://www.alphavantage.co/physical_currency_list/"
+ @crypto_list "https://www.alphavantage.co/digital_currency_list/"
+
+ HTTPoison.start()
+ load_currency = fn(url) ->
+ resp = HTTPoison.get!(url)
+ resp.body
+ |> String.strip()
+ |> String.split("\n")
+ |> Enum.drop(1)
+ |> Enum.map(fn(line) ->
+ [symbol, name] = line
+ |> String.strip()
+ |> String.split(",", parts: 2)
+ {symbol, name}
+ end)
+ |> Enum.into(Map.new)
+ end
+ fiat = load_currency.(@currency_list)
+ crypto = load_currency.(@crypto_list)
+ @currencies Map.merge(fiat, crypto)
+
+ def irc_doc, do: @moduledoc
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:forex", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:currency", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:stocks", [])
+ {:ok, nil}
+ end
+
+
+ def handle_info({:irc, :trigger, "stocks", message = %{trigger: %{type: :query, args: args = search}}}, state) do
+ search = Enum.join(search, "%20")
+ url = "https://www.alphavantage.co/query?function=SYMBOL_SEARCH&keywords=#{search}&apikey=#{api_key()}"
+ case HTTPoison.get(url) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: data}} ->
+ data = Poison.decode!(data)
+ if error = Map.get(data, "Error Message") do
+ Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
+ message.replyfun.("stocks: requête invalide")
+ else
+ items = for item <- Map.get(data, "bestMatches") do
+ symbol = Map.get(item, "1. symbol")
+ name = Map.get(item, "2. name")
+ type = Map.get(item, "3. type")
+ region = Map.get(item, "4. region")
+ currency = Map.get(item, "8. currency")
+ "#{symbol}: #{name} (#{region}; #{currency}; #{type})"
+ end
+ |> Enum.join(", ")
+ items = if items == "" do
+ "no results!"
+ else
+ items
+ end
+ message.replyfun.(items)
+ end
+ {:ok, resp = %HTTPoison.Response{status_code: code}} ->
+ Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
+ message.replyfun.("forex: erreur (api #{code})")
+ {:error, %HTTPoison.Error{reason: error}} ->
+ Logger.error "AlphaVantage HTTP error: #{inspect error}"
+ message.replyfun.("forex: erreur (http #{inspect error})")
+ end
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "stocks", message = %{trigger: %{type: :bang, args: args = [symbol]}}}, state) do
+ url = "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=#{symbol}&apikey=#{api_key()}"
+ case HTTPoison.get(url) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: data}} ->
+ data = Poison.decode!(data)
+ if error = Map.get(data, "Error Message") do
+ Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
+ message.replyfun.("stocks: requête invalide")
+ else
+ data = Map.get(data, "Global Quote")
+ open = Map.get(data, "02. open")
+ high = Map.get(data, "03. high")
+ low = Map.get(data, "04. low")
+ price = Map.get(data, "05. price")
+ volume = Map.get(data, "06. volume")
+ prev_close = Map.get(data, "08. previous close")
+ change = Map.get(data, "09. change")
+ change_pct = Map.get(data, "10. change percent")
+
+ msg = "#{symbol}: #{price} #{change} [#{change_pct}] (high: #{high}, low: #{low}, open: #{open}, prev close: #{prev_close}) (volume: #{volume})"
+ message.replyfun.(msg)
+ end
+ {:ok, resp = %HTTPoison.Response{status_code: code}} ->
+ Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
+ message.replyfun.("stocks: erreur (api #{code})")
+ {:error, %HTTPoison.Error{reason: error}} ->
+ Logger.error "AlphaVantage HTTP error: #{inspect error}"
+ message.replyfun.("stocks: erreur (http #{inspect error})")
+ end
+ {:noreply, state}
+ end
+
+
+ def handle_info({:irc, :trigger, "forex", message = %{trigger: %{type: :bang, args: args = [_ | _]}}}, state) do
+ {amount, from, to} = case args do
+ [amount, from, to] ->
+ {amount, _} = Float.parse(amount)
+ {amount, from, to}
+ [from, to] ->
+ {1, from, to}
+ [from] ->
+ {1, from, "EUR"}
+ end
+ url = "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=#{from}&to_currency=#{to}&apikey=#{api_key()}"
+ case HTTPoison.get(url) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: data}} ->
+ data = Poison.decode!(data)
+ if error = Map.get(data, "Error Message") do
+ Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
+ message.replyfun.("forex: requête invalide")
+ else
+ data = Map.get(data, "Realtime Currency Exchange Rate")
+ from_name = Map.get(data, "2. From_Currency Name")
+ to_name = Map.get(data, "4. To_Currency Name")
+ rate = Map.get(data, "5. Exchange Rate")
+ {rate, _} = Float.parse(rate)
+ value = amount*rate
+ message.replyfun.("#{amount} #{from} (#{from_name}) -> #{value} #{to} (#{to_name}) (#{rate})")
+ end
+ {:ok, resp = %HTTPoison.Response{status_code: code}} ->
+ Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
+ message.replyfun.("forex: erreur (api #{code})")
+ {:error, %HTTPoison.Error{reason: error}} ->
+ Logger.error "AlphaVantage HTTP error: #{inspect error}"
+ message.replyfun.("forex: erreur (http #{inspect error})")
+ end
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "currency", message = %{trigger: %{type: :query, args: args = search}}}, state) do
+ search = Enum.join(search, " ")
+ results = Enum.filter(@currencies, fn({symbol, name}) ->
+ String.contains?(String.downcase(name), String.downcase(search)) || String.contains?(String.downcase(symbol), String.downcase(search))
+ end)
+ |> Enum.map(fn({symbol, name}) ->
+ "#{symbol}: #{name}"
+ end)
+ |> Enum.join(", ")
+
+ if results == "" do
+ message.replyfun.("no results!")
+ else
+ message.replyfun.(results)
+ end
+ {:noreply, state}
+ end
+
+ defp api_key() do
+ Application.get_env(:lsg, :alphavantage, [])
+ |> Keyword.get(:api_key, "demo")
+ end
+
+end
diff --git a/lib/lsg_irc/kick_roulette_handler.ex b/lib/lsg_irc/kick_roulette_handler.ex
deleted file mode 100644
index ece1b95..0000000
--- a/lib/lsg_irc/kick_roulette_handler.ex
+++ /dev/null
@@ -1,32 +0,0 @@
-defmodule LSG.IRC.KickRouletteHandler do
- @moduledoc """
- # kick roulette
-
- * **!kick**
- """
-
- def irc_doc, do: @moduledoc
- def start_link(client) do
- GenServer.start_link(__MODULE__, [client])
- end
-
- def init([client]) do
- ExIRC.Client.add_handler client, self
- {:ok, client}
- end
-
- def handle_info({:received, "!kick", sender, chan}, client) do
- if 5 == :crypto.rand_uniform(1, 6) do
- spawn(fn() ->
- :timer.sleep(:crypto.rand_uniform(200, 10_000))
- ExIRC.Client.kick(client, chan, sender.nick, "perdu")
- end)
- end
- {:noreply, client}
- end
-
- def handle_info(msg, client) do
- {:noreply, client}
- end
-
-end
diff --git a/lib/lsg_irc/kick_roulette_plugin.ex b/lib/lsg_irc/kick_roulette_plugin.ex
new file mode 100644
index 0000000..83efcb0
--- /dev/null
+++ b/lib/lsg_irc/kick_roulette_plugin.ex
@@ -0,0 +1,32 @@
+defmodule LSG.IRC.KickRoulettePlugin do
+ @moduledoc """
+ # kick roulette
+
+ * **!kick**, tentez votre chance…
+ """
+
+ def irc_doc, do: @moduledoc
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:kick", [])
+ {:ok, nil}
+ end
+
+ def handle_info({:irc, :trigger, "kick", message = %{trigger: %{type: :bang, args: []}}}, _) do
+ if 5 == :crypto.rand_uniform(1, 6) do
+ spawn(fn() ->
+ :timer.sleep(:crypto.rand_uniform(200, 10_000))
+ message.replyfun.({:kick, message.sender.nick, "perdu"})
+ end)
+ end
+ {:noreply, nil}
+ end
+
+ def handle_info(msg, _) do
+ {:noreply, nil}
+ end
+
+end
diff --git a/lib/lsg_irc/last_fm_handler.ex b/lib/lsg_irc/last_fm_plugin.ex
index 4eef24e..2446473 100644
--- a/lib/lsg_irc/last_fm_handler.ex
+++ b/lib/lsg_irc/last_fm_plugin.ex
@@ -1,4 +1,4 @@
-defmodule LSG.IRC.LastFmHandler do
+defmodule LSG.IRC.LastFmPlugin do
require Logger
@moduledoc """
@@ -9,54 +9,54 @@ defmodule LSG.IRC.LastFmHandler do
* **+lastfm `<username last.fm>`, -lastfm**
"""
- defstruct client: nil, dets: nil
+ defstruct dets: nil
def irc_doc, do: @moduledoc
- def start_link(client) do
- GenServer.start_link(__MODULE__, [client])
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
end
- def init([client]) do
- ExIRC.Client.add_handler(client, self())
+ def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:lastfm", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:lastfmall", [])
dets_filename = (LSG.data_path() <> "/" <> "lastfm.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [])
- {:ok, %__MODULE__{client: client, dets: dets}}
+ {:ok, %__MODULE__{dets: dets}}
end
- def handle_info({:received, "+lastfm " <> username, sender, chan}, state) do
+ def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :plus, args: [username]}}}, state) do
username = String.strip(username)
- :ok = :dets.insert(state.dets, {String.downcase(sender.nick), username})
- ExIRC.Client.msg(state.client, :privmsg, chan, "#{sender.nick}: nom d'utilisateur last.fm configuré: \"#{username}\"")
+ :ok = :dets.insert(state.dets, {String.downcase(message.sender.nick), username})
+ message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm configuré: \"#{username}\".")
{:noreply, state}
end
- def handle_info({:received, "-lastfm", sender, chan}, state) do
- text = case :dets.lookup(state.dets, sender.nick) do
+ def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :minus, args: []}}}, state) do
+ text = case :dets.lookup(state.dets, message.sender.nick) do
[{_nick, username}] ->
- :dets.delete(state.dets, String.downcase(sender.nick))
- "#{sender.nick}: nom d'utilisateur last.fm enlevé"
- _ -> ""
+ :dets.delete(state.dets, String.downcase(message.sender.nick))
+ message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm enlevé.")
+ _ -> nil
end
- ExIRC.Client.msg(state.client, :privmsg, chan, text)
{:noreply, state}
end
- def handle_info({:received, "!lastfm", sender, chan}, state) do
- irc_now_playing(sender.nick, chan, state)
+ def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :bang, args: []}}}, state) do
+ irc_now_playing(message.sender.nick, message, state)
{:noreply, state}
end
- def handle_info({:received, "!lastfm " <> nick_or_user, sender, chan}, state) do
- irc_now_playing(nick_or_user, chan, state)
+ def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :bang, args: [nick_or_user]}}}, state) do
+ irc_now_playing(nick_or_user, message, state)
{:noreply, state}
end
- def handle_info({:received, "!lastfmall", sender, chan}, state) do
+ def handle_info({:irc, :trigger, "lastfmall", message = %{trigger: %{type: :bang}}}, state) do
foldfun = fn({_nick, user}, acc) -> [user|acc] end
usernames = :dets.foldl(foldfun, [], state.dets)
|> Enum.uniq
- for u <- usernames, do: irc_now_playing(u, chan, state)
+ for u <- usernames, do: irc_now_playing(u, message, state)
{:noreply, state}
end
@@ -72,7 +72,7 @@ defmodule LSG.IRC.LastFmHandler do
:ok
end
- defp irc_now_playing(nick_or_user, chan, state) do
+ defp irc_now_playing(nick_or_user, message, state) do
nick_or_user = String.strip(nick_or_user)
username = case :dets.lookup(state.dets, String.downcase(nick_or_user)) do
[{^nick_or_user, username}] -> username
@@ -80,19 +80,23 @@ defmodule LSG.IRC.LastFmHandler do
end
case now_playing(username) do
- {:error, text} when is_binary(text) -> ExIRC.Client.msg(state.client, :privmsg, chan, text)
+ {:error, text} when is_binary(text) ->
+ message.replyfun.(text)
{:ok, map} when is_map(map) ->
text = format_now_playing(map)
user = lookup_nick(username, state)
- if user && text, do: ExIRC.Client.msg(state.client, :privmsg, chan, "#{user} #{text}")
+ if user && text do
+ message.replyfun.("#{user} #{text}")
+ else
+ message.replyfun.("#{username}: pas de résultat")
+ end
other ->
- IO.inspect(other)
- nil
+ message.replyfun.("erreur http")
end
end
defp now_playing(user) do
- api = Application.get_env(:lsg, __MODULE__)[:api_key]
+ api = Application.get_env(:lsg, :lastfm)[:api_key]
url = "http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&format=json&limit=1" <> "&api_key=" <> api <> "&user="<> user
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} -> Jason.decode(body)
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
diff --git a/lib/lsg_irc/link_plugin/imgur.ex b/lib/lsg_irc/link_plugin/imgur.ex
new file mode 100644
index 0000000..9ce3cf3
--- /dev/null
+++ b/lib/lsg_irc/link_plugin/imgur.ex
@@ -0,0 +1,84 @@
+defmodule LSG.IRC.LinkPlugin.Imgur do
+ @behaviour LSG.IRC.LinkPlugin
+
+ @moduledoc """
+ # Imgur link preview
+
+ No options.
+
+ Needs to have a Imgur API key configured:
+
+ ```
+ config :lsg, :imgur,
+ client_id: "xxxxxxxx",
+ client_secret: "xxxxxxxxxxxxxxxxxxxx"
+ ```
+ """
+
+ def match(uri = %URI{host: "imgur.com", path: "/a/"<>album_id}, _) do
+ {true, %{album_id: album_id}}
+ end
+ def match(uri = %URI{host: "imgur.com", path: "/gallery/"<>album_id}, _) do
+ {true, %{album_id: album_id}}
+ end
+ def match(uri = %URI{host: "i.imgur.com", path: "/"<>image}, _) do
+ [hash, _] = String.split(image, ".", parts: 2)
+ {true, %{image_id: hash}}
+ end
+ def match(_, _), do: false
+
+ def expand(_uri, %{album_id: album_id}, opts) do
+ expand_imgur_album(album_id, opts)
+ end
+
+ def expand(_uri, %{image_id: image_id}, opts) do
+ expand_imgur_image(image_id, opts)
+ end
+
+ def expand_imgur_image(image_id, opts) do
+ client_id = Keyword.get(Application.get_env(:lsg, :imgur, []), :client_id, "42")
+ headers = [{"Authorization", "Client-ID #{client_id}"}]
+ options = []
+ case HTTPoison.get("https://api.imgur.com/3/image/#{image_id}", headers, options) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ {:ok, json} = Jason.decode(body)
+ data = json["data"]
+ IO.puts inspect(json)
+ title = String.slice(data["title"] || data["description"], 0, 180)
+ nsfw = if data["nsfw"], do: "(NSFW) - ", else: ""
+ {:ok, "#{nsfw}#{title}"}
+ other ->
+ :error
+ end
+ end
+
+ def expand_imgur_album(album_id, opts) do
+ client_id = Keyword.get(Application.get_env(:lsg, :imgur, []), :client_id, "42")
+ headers = [{"Authorization", "Client-ID #{client_id}"}]
+ options = []
+ case HTTPoison.get("https://api.imgur.com/3/album/#{album_id}", headers, options) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ {:ok, json} = Jason.decode(body)
+ data = json["data"]
+ title = data["title"]
+ nsfw = data["nsfw"]
+ nsfw = if nsfw, do: "(NSFW) - ", else: ""
+ if data["images_count"] == 1 do
+ [image] = data["images"]
+ title = if title || data["title"] do
+ title = [title, data["title"]] |> Enum.filter(fn(x) -> x end) |> Enum.uniq() |> Enum.join(" — ")
+ "#{title} — "
+ else
+ ""
+ end
+ {:ok, "#{nsfw}#{title}#{image["link"]}"}
+ else
+ title = if title, do: title, else: "Untitled album"
+ {:ok, "#{nsfw}#{title} - #{data["images_count"]} images"}
+ end
+ other ->
+ :error
+ end
+ end
+
+end
diff --git a/lib/lsg_irc/link_plugin/twitter.ex b/lib/lsg_irc/link_plugin/twitter.ex
new file mode 100644
index 0000000..04dea7c
--- /dev/null
+++ b/lib/lsg_irc/link_plugin/twitter.ex
@@ -0,0 +1,91 @@
+defmodule LSG.IRC.LinkPlugin.Twitter do
+ @behaviour LSG.IRC.LinkPlugin
+
+ @moduledoc """
+ # Twitter Link Preview
+
+ Configuration:
+
+ needs an API key and auth tokens:
+
+ ```
+ config :extwitter, :oauth, [
+ consumer_key: "zzzzz",
+ consumer_secret: "xxxxxxx",
+ access_token: "yyyyyy",
+ access_token_secret: "ssshhhhhh"
+ ]
+ ```
+
+ options:
+
+ * `expand_quoted`: Add the quoted tweet instead of its URL. Default: true.
+ """
+
+ def match(uri = %URI{host: twitter, path: path}, _opts) when twitter in ["twitter.com", "m.twitter.com", "mobile.twitter.com"] do
+ case String.split(path, "/", parts: 4) do
+ ["", _username, "status", status_id] ->
+ {status_id, _} = Integer.parse(status_id)
+ {true, %{status_id: status_id}}
+ _ -> false
+ end
+ end
+
+ def match(_, _), do: false
+
+ def expand(_uri, %{status_id: status_id}, opts) do
+ expand_tweet(ExTwitter.show(status_id, tweet_mode: "extended"), opts)
+ end
+
+ defp expand_tweet(nil, _opts) do
+ :error
+ end
+
+ defp expand_tweet(tweet, opts) do
+ text = expand_twitter_text(tweet)
+ text = if tweet.quoted_status do
+ quote_url = "https://twitter.com/#{tweet.quoted_status.user.screen_name}/status/#{tweet.quoted_status.id}"
+ String.replace(text, quote_url, "")
+ else
+ text
+ end
+
+ text = IRC.splitlong(text)
+
+ {:ok, at} = Timex.parse(tweet.created_at, "%a %b %e %H:%M:%S %z %Y", :strftime)
+ {:ok, format} = Timex.format(at, "{relative}", :relative)
+
+ quoted = if tweet.quoted_status do
+ quote_url = "https://twitter.com/#{tweet.quoted_status.user.screen_name}/status/#{tweet.quoted_status.id}"
+ full_text = expand_twitter_text(tweet.quoted_status)
+ |> IRC.splitlong_with_prefix(">")
+ ["> #{tweet.quoted_status.user.name} (@#{tweet.quoted_status.user.screen_name}): #{quote_url}"] ++ full_text
+ else
+ []
+ end
+
+ foot = "— #{format} - #{tweet.retweet_count} retweets - #{tweet.favorite_count} likes"
+
+ text = ["#{tweet.user.name} (@#{tweet.user.screen_name}):"] ++ text ++ quoted ++ [foot]
+ {:ok, text}
+ end
+
+ defp expand_twitter_text(tweet) do
+ text = Enum.reduce(tweet.entities.urls, tweet.full_text, fn(entity, text) ->
+ String.replace(text, entity.url, entity.expanded_url)
+ end)
+ extended = tweet.extended_entities || %{media: []}
+ text = Enum.reduce(extended.media, text, fn(entity, text) ->
+ url = Enum.filter(extended.media, fn(e) -> entity.url == e.url end)
+ |> Enum.map(fn(e) ->
+ cond do
+ e.type == "video" -> e.expanded_url
+ true -> e.media_url_https
+ end
+ end)
+ |> Enum.join(" ")
+ String.replace(text, entity.url, url)
+ end)
+ end
+
+end
diff --git a/lib/lsg_irc/link_plugin/youtube.ex b/lib/lsg_irc/link_plugin/youtube.ex
new file mode 100644
index 0000000..ea4f213
--- /dev/null
+++ b/lib/lsg_irc/link_plugin/youtube.ex
@@ -0,0 +1,69 @@
+defmodule LSG.IRC.LinkPlugin.YouTube do
+ @behaviour LSG.IRC.LinkPlugin
+
+ @moduledoc """
+ # YouTube link preview
+
+ needs an API key:
+
+ ```
+ config :lsg, :youtube,
+ api_key: "xxxxxxxxxxxxx"
+ ```
+
+ options:
+
+ * `invidious`: Add a link to invidio.us. Default: true.
+ """
+
+ def match(uri = %URI{host: yt, path: "/watch", query: "v="<>video_id}, _opts) when yt in ["youtube.com", "www.youtube.com"] do
+ {true, %{video_id: video_id}}
+ end
+
+ def match(%URI{host: "youtu.be", path: "/"<>video_id}, _opts) do
+ {true, %{video_id: video_id}}
+ end
+
+ def match(_, _), do: false
+
+
+ def expand(uri, %{video_id: video_id}, opts) do
+ key = Application.get_env(:lsg, :youtube)[:api_key]
+ params = %{
+ "part" => "snippet,contentDetails,statistics",
+ "id" => video_id,
+ "key" => key
+ }
+ headers = []
+ options = [params: params]
+ case HTTPoison.get("https://www.googleapis.com/youtube/v3/videos", [], options) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ case Jason.decode(body) do
+ {:ok, json} ->
+ item = List.first(json["items"])
+ if item do
+ snippet = item["snippet"]
+ duration = item["contentDetails"]["duration"] |> String.replace("PT", "") |> String.downcase
+ date = snippet["publishedAt"]
+ |> DateTime.from_iso8601()
+ |> elem(1)
+ |> Timex.format("{relative}", :relative)
+ |> elem(1)
+
+ line = if Keyword.get(opts, :invidious, true) do
+ ["-> https://invidio.us/watch?v=#{video_id}"]
+ else
+ []
+ end
+ {:ok, line ++ ["#{snippet["title"]}", "— #{duration} — uploaded by #{snippet["channelTitle"]} — #{date}"
+ <> " — #{item["statistics"]["viewCount"]} views, #{item["statistics"]["likeCount"]} likes,"
+ <> " #{item["statistics"]["dislikeCount"]} dislikes"]}
+ else
+ :error
+ end
+ _ -> :error
+ end
+ end
+ end
+
+end
diff --git a/lib/lsg_irc/outline_plugin.ex b/lib/lsg_irc/outline_plugin.ex
new file mode 100644
index 0000000..7bfaac1
--- /dev/null
+++ b/lib/lsg_irc/outline_plugin.ex
@@ -0,0 +1,72 @@
+defmodule LSG.IRC.OutlinePlugin do
+ @moduledoc """
+ # outline auto-link
+
+ Envoie un lien vers Outline quand un lien est envoyé.
+
+ * **+outline `<host>`** active outline pour `<host>`.
+ * **-outline `<host>`** désactive outline pour `<host>`.
+ """
+ def short_irc_doc, do: false
+ def irc_doc, do: @moduledoc
+ require Logger
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ defstruct [:file, :hosts]
+
+ def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:outline", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "message", [])
+ file = Path.join(LSG.data_path, "/outline.txt")
+ hosts = case File.read(file) do
+ {:error, :enoent} ->
+ []
+ {:ok, lines} ->
+ String.split(lines, "\n", trim: true)
+ end
+ {:ok, %__MODULE__{file: file, hosts: hosts}}
+ end
+
+ def handle_info({:irc, :trigger, "outline", message = %IRC.Message{trigger: %IRC.Trigger{type: :plus, args: [host]}}}, state) do
+ state = %{state | hosts: [host | state.hosts]}
+ save(state)
+ message.replyfun.("ok")
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "outline", message = %IRC.Message{trigger: %IRC.Trigger{type: :minus, args: [host]}}}, state) do
+ state = %{state | hosts: List.delete(state.hosts, host)}
+ save(state)
+ message.replyfun.("ok")
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :text, message = %IRC.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
+ if Enum.any?(state.hosts, fn(host) -> String.ends_with?(uri.host, host) end) do
+ line = "-> https://outline.com/#{word}"
+ message.replyfun.(line)
+ end
+ end
+ end
+ end)
+ {:noreply, state}
+ end
+
+ def handle_info(msg, state) do
+ {:noreply, state}
+ end
+
+ def save(state = %{file: file, hosts: hosts}) do
+ string = Enum.join(hosts, "\n")
+ File.write(file, string)
+ end
+
+end
diff --git a/lib/lsg_irc/preums_plugin.ex b/lib/lsg_irc/preums_plugin.ex
new file mode 100644
index 0000000..98cd539
--- /dev/null
+++ b/lib/lsg_irc/preums_plugin.ex
@@ -0,0 +1,108 @@
+defmodule LSG.IRC.PreumsPlugin do
+ @moduledoc """
+ # preums !!!
+
+ * `!preums`: affiche le preums du jour
+ * `.preums`: stats des preums
+ """
+
+ @perfects [~r/preum(s|)/i]
+
+ # dets {{chan, day = {yyyy, mm, dd}}, nick, now, perfect?, text}
+
+ def irc_doc, do: @moduledoc
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "message", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "triggers", [])
+ dets_filename = (LSG.data_path() <> "/preums.dets") |> String.to_charlist()
+ {:ok, dets} = :dets.open_file(dets_filename, [])
+ {:ok, %{dets: dets}}
+ end
+
+ # Latest
+ def handle_info({:irc, :trigger, "preums", m = %IRC.Message{channel: channel, trigger: %IRC.Trigger{type: :bang}}}, state) do
+ state = handle_preums(m, state)
+ tz = timezone(channel)
+ {:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
+ date = {now.year, now.month, now.day}
+ key = {channel, date}
+ chan_cache = Map.get(state, channel, %{})
+ item = if i = Map.get(chan_cache, date) do
+ i
+ else
+ case :dets.lookup(state.dets, key) do
+ [item = {^key, _nick, _now, _perfect, _text}] -> item
+ _ -> nil
+ end
+ end
+
+ if item do
+ {_, nick, date, _perfect, text} = item
+ h = "#{date.hour}:#{date.minute}:#{date.second}"
+ m.replyfun.("preums: #{nick} à #{h}: “#{text}”")
+ end
+ {:noreply, state}
+ end
+
+ # Stats
+ def handle_info({:irc, :trigger, "preums", m = %IRC.Message{channel: channel, trigger: %IRC.Trigger{type: :dot}}}, state) do
+ state = handle_preums(m, state)
+ {:noreply, state}
+ end
+
+ # Help
+ def handle_info({:irc, :trigger, "preums", m = %IRC.Message{channel: channel, trigger: %IRC.Trigger{type: :query}}}, state) do
+ state = handle_preums(m, state)
+ {:noreply, state}
+ end
+
+ # Trigger fallback
+ def handle_info({:irc, :trigger, _, m = %IRC.Message{}}, state) do
+ state = handle_preums(m, state)
+ {:noreply, state}
+ end
+
+ # Message fallback
+ def handle_info({:irc, :text, m = %IRC.Message{}}, state) do
+ {:noreply, handle_preums(m, state)}
+ end
+
+ defp timezone(channel) do
+ env = Application.get_env(:lsg, LSG.IRC.PreumsPlugin, [])
+ channels = Keyword.get(env, :channels, %{})
+ channel_settings = Map.get(channels, channel, [])
+ default = Keyword.get(env, :default_tz, "Europe/Paris")
+ Keyword.get(channel_settings, :tz, default) || default
+ end
+
+ defp handle_preums(m = %IRC.Message{channel: channel, text: text, sender: sender}, state) do
+ tz = timezone(channel)
+ {:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
+ date = {now.year, now.month, now.day}
+ key = {channel, date}
+ chan_cache = Map.get(state, channel, %{})
+ unless i = Map.get(chan_cache, date) do
+ case :dets.lookup(state.dets, key) do
+ [item = {^key, _nick, _now, _perfect, _text}] ->
+ # Preums lost, but wasn't cached
+ state = Map.put(state, channel, %{date => item})
+ state
+ _ ->
+ # Preums won!
+ perfect? = Enum.any?(@perfects, fn(perfect) -> Regex.match?(perfect, text) end)
+ item = {key, sender.nick, now, perfect?, text}
+ :dets.insert(state.dets, item)
+ :dets.sync(state.dets)
+ state = Map.put(state, channel, %{date => item})
+ state
+ end
+ else
+ state
+ end
+ end
+
+end
diff --git a/lib/lsg_irc/quatre_cent_vingt_plugin.ex b/lib/lsg_irc/quatre_cent_vingt_plugin.ex
index 7c3067c..f6f8a63 100644
--- a/lib/lsg_irc/quatre_cent_vingt_plugin.ex
+++ b/lib/lsg_irc/quatre_cent_vingt_plugin.ex
@@ -5,6 +5,7 @@ defmodule LSG.IRC.QuatreCentVingtPlugin do
# 420
* **!420**: recorde un nouveau 420.
+ * **!420*x**: recorde un nouveau 420*x (*2 = 840, ...) (à vous de faire la multiplication).
* **!420 pseudo**: stats du pseudo.
"""
diff --git a/lib/lsg_irc/txt_handler.ex b/lib/lsg_irc/txt_plugin.ex
index 032c11c..6b7edbd 100644
--- a/lib/lsg_irc/txt_handler.ex
+++ b/lib/lsg_irc/txt_plugin.ex
@@ -1,101 +1,110 @@
-defmodule LSG.IRC.TxtHandler do
+defmodule LSG.IRC.TxtPlugin do
alias IRC.UserTrack
require Logger
@moduledoc """
# [txt](/irc/txt)
- * **!txt**: liste des fichiers et statistiques.
+ * **.txt**: liste des fichiers et statistiques.
Les fichiers avec une `*` sont vérrouillés.
[Voir sur le web](/irc/txt).
+ * **!txt**: lis aléatoirement une ligne dans tous les fichiers.
+ * **!txt `<recherche>`**: recherche une ligne dans tous les fichiers.
+
+ * **~txt**: essaie de générer une phrase (markov).
+ * **~txt `<début>`**: essaie de générer une phrase commencant par `<debut>`.
+
* **!`FICHIER`**: lis aléatoirement une ligne du fichier `FICHIER`.
* **!`FICHIER` `<chiffre>`**: lis la ligne `<chiffre>` du fichier `FICHIER`.
- * **!`FILE` `<recherche>`**: recherche une ligne contenant `<recherche>` dans `FICHIER`.
+ * **!`FICHIER` `<recherche>`**: recherche une ligne contenant `<recherche>` dans `FICHIER`.
* **+txt `<file`>**: crée le fichier `<file>`.
* **+`FICHIER` `<texte>`**: ajoute une ligne `<texte>` dans le fichier `FICHIER`.
* **-`FICHIER` `<chiffre>`**: supprime la ligne `<chiffre>` du fichier `FICHIER`.
- * **+txtro, +txtrw**. op seulement. active/désactive le mode lecture seule.
+ * **-txtrw, +txtrw**. op seulement. active/désactive le mode lecture seule.
* **+txtlock `<fichier>`, -txtlock `<fichier>`**. op seulement. active/désactive le verrouillage d'un fichier.
+
+ Insérez `\\\\` pour faire un saut de ligne.
"""
def short_irc_doc, do: "!txt https://sys.115ans.net/irc/txt "
def irc_doc, do: @moduledoc
- def start_link(client) do
- GenServer.start_link(__MODULE__, [client])
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
end
- defstruct client: nil, triggers: %{}, rw: true, locks: nil, markov: nil
+ defstruct triggers: %{}, rw: true, locks: nil, markov_handler: nil, markov: nil
- def init([client]) do
+ def init([]) do
dets_locks_filename = (LSG.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist
{:ok, locks} = :dets.open_file(dets_locks_filename, [])
- {:ok, markov} = ExChain.MarkovModel.start_link
- state = %__MODULE__{client: client, locks: locks, markov: markov}
- ExIRC.Client.add_handler(client, self())
- {:ok, %__MODULE__{state | triggers: load()}}
+ markov_handler = Keyword.get(Application.get_env(:lsg, __MODULE__, []), :markov_handler, LSG.IRC.TxtPlugin.Markov.Native)
+ {:ok, markov} = markov_handler.start_link()
+ {:ok, _} = Registry.register(IRC.PubSub, "triggers", [])
+ {:ok, %__MODULE__{locks: locks, markov_handler: markov_handler, markov: markov, triggers: load()}}
end
def handle_info({:received, "!reload", _, chan}, state) do
{:noreply, %__MODULE__{state | triggers: load()}}
end
- ## -- ADMIN RO/RW
- def handle_info({:received, "+txtrw", %ExIRC.SenderInfo{nick: nick}, chan}, state = %__MODULE__{rw: false}) do
- if UserTrack.operator?(chan, nick) do
- say(state, chan, "txt: écriture réactivée")
+ #
+ # ADMIN: RW/RO
+ #
+
+ def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :plus}}}, state = %{rw: false}) do
+ if channel && UserTrack.operator?(channel, msg.sender.nick) do
+ msg.replyfun.("txt: écriture réactivée")
{:noreply, %__MODULE__{state | rw: true}}
else
{:noreply, state}
end
end
- def handle_info({:received, "+txtro", %ExIRC.SenderInfo{nick: nick}, chan}, state = %__MODULE__{rw: true}) do
- if UserTrack.operator?(chan, nick) do
- say(state, chan, "txt: écriture désactivée")
+ def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :minus}}}, state = %{rw: true}) do
+ if channel && UserTrack.operator?(channel, msg.sender.nick) do
+ msg.replyfun.("txt: écriture désactivée")
{:noreply, %__MODULE__{state | rw: false}}
else
{:noreply, state}
end
end
- def handle_info({:received, "+txtro", _, _}, state) do
- {:noreply, state}
- end
- def handle_info({:received, "+txtrw", _, _}, state) do
- {:noreply, state}
- end
+ #
+ # ADMIN: LOCKS
+ #
- ## -- ADMIN LOCKS
- def handle_info({:received, "+txtlock " <> trigger, %ExIRC.SenderInfo{nick: nick}, chan}, state) do
+ def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
- true <- UserTrack.operator?(chan, nick)
+ true <- UserTrack.operator?(msg.channel, msg.sender.nick)
do
:dets.insert(state.locks, {trigger})
- say(state, chan, "txt: #{trigger} verrouillé")
+ msg.replyfun.("txt: #{trigger} verrouillé")
end
{:noreply, state}
end
- def handle_info({:received, "-txtlock " <> trigger, %ExIRC.SenderInfo{nick: nick}, chan}, state) do
+ def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :minus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
- true <- UserTrack.operator?(chan, nick),
+ true <- UserTrack.operator?(msg.channel, msg.sender.nick),
true <- :dets.member(state.locks, trigger)
do
:dets.delete(state.locks, trigger)
- say(state, chan, "txt: #{trigger} déverrouillé")
+ msg.replyfun.("txt: #{trigger} déverrouillé")
end
{:noreply, state}
end
- # -- ADD
+ #
+ # FILE LIST
+ #
- def handle_info({:received, "!txt", _, chan}, state) do
+ def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :dot}}}, state) do
map = Enum.map(state.triggers, fn({key, data}) ->
locked? = case :dets.lookup(state.locks, key) do
[{trigger}] -> "*"
@@ -113,103 +122,147 @@ defmodule LSG.IRC.TxtHandler do
ro = if !state.rw, do: " (lecture seule activée)", else: ""
(detail<>total<>ro)
- |> String.codepoints
- |> Enum.chunk_every(440)
- |> Enum.map(&Enum.join/1)
- |> Enum.map(fn(line) -> ExIRC.Client.msg(state.client, :privmsg, chan, line) end)
+ |> msg.replyfun.()
{:noreply, state}
end
- def handle_info({:received, "~txt", _, chan}, state) do
- case ExChain.SentenceGenerator.create_filtered_sentence(state.markov) do
- {:ok, line, _, _} ->
- ExIRC.Client.msg(state.client, :privmsg, chan, line)
- error ->
- Logger.error "Txt Markov error: "<>inspect error
+ #
+ # GLOBAL: RANDOM
+ #
+
+ def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: []}}}, state) do
+ result = Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
+ Enum.reduce(data, acc, fn({l, _}, acc) ->
+ [{trigger, l} | acc]
+ end)
+ end)
+ |> Enum.shuffle()
+
+ if !Enum.empty?(result) do
+ {source, line} = Enum.random(result)
+ msg.replyfun.("#{source}: #{line}")
end
{:noreply, state}
end
- def handle_info({:received, "~txt "<>complete, _, chan}, state) do
- case ExChain.SentenceGenerator.complete_sentence(state.markov, complete) do
- {line, _} ->
- ExIRC.Client.msg(state.client, :privmsg, chan, line)
+
+ def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: args}}}, state) do
+ grep = Enum.join(args, " ")
+ result = Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
+ Enum.reduce(data, acc, fn({l, _}, acc) ->
+ [{trigger, l} | acc]
+ end)
+ end)
+ |> Enum.filter(fn({_, line}) -> String.contains?(String.downcase(line), String.downcase(grep)) end)
+ |> Enum.shuffle()
+
+ if !Enum.empty?(result) do
+ {source, line} = Enum.random(result)
+ msg.replyfun.("#{source}: #{line}")
+ end
+ {:noreply, state}
+ end
+
+ #
+ # GLOBAL: MARKOV
+ #
+
+ def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :tilde, args: []}}}, state) do
+ case state.markov_handler.sentence(state.markov) do
+ {:ok, line} ->
+ msg.replyfun.(line)
error ->
Logger.error "Txt Markov error: "<>inspect error
end
{:noreply, state}
end
- def handle_info({:received, "!"<>trigger, _, chan}, state) do
- {trigger, opts} = clean_trigger(trigger)
- line = get_random(state.triggers, trigger, opts)
- if line do
- ExIRC.Client.msg(state.client, :privmsg, chan, line)
+ def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :tilde, args: complete}}}, state) do
+ complete = Enum.join(complete, " ")
+ case state.markov_handler.complete_sentence(complete, state.markov) do
+ {:ok, line} ->
+ msg.replyfun.(line)
+ error ->
+ Logger.error "Txt Markov error: "<>inspect error
end
{:noreply, state}
end
- def handle_info({:received, "+txt "<>trigger, sender=%ExIRC.SenderInfo{nick: nick}, chan}, state) do
+ #
+ # TXT CREATE
+ #
+
+ def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
- true <- can_write?(state, chan, sender, trigger),
+ true <- can_write?(state, msg, trigger),
:ok <- create_file(trigger)
do
- ExIRC.Client.msg(state.client, :privmsg, chan, "#{trigger}.txt créé. Ajouter: `+#{trigger} …` ; Lire: `!#{trigger}`")
+ msg.replyfun.("#{trigger}.txt créé. Ajouter: `+#{trigger} …` ; Lire: `!#{trigger}`")
{:noreply, %__MODULE__{state | triggers: load()}}
else
_ -> {:noreply, state}
end
end
- def handle_info({:received, "+"<>trigger_and_content, sender=%ExIRC.SenderInfo{nick: nick}, chan}, state) do
+ #
+ # TXT: RANDOM
+ #
+
+ def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :bang, args: []}}}, state) do
+ {trigger, opts} = clean_trigger(trigger)
+ line = get_random(state.triggers, trigger, opts)
+ if line do
+ msg.replyfun.(line)
+ end
+ {:noreply, state}
+ end
+
+ #
+ # TXT: ADD
+ #
+
+ def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :plus, args: content}}}, state) do
with \
- {trigger, _} <- clean_trigger(trigger_and_content),
- true <- can_write?(state, chan, sender, trigger),
- {:ok, idx} <- add(state.triggers, trigger_and_content)
+ true <- can_write?(state, msg, trigger),
+ {:ok, idx} <- add(state.triggers, msg.text)
do
- ExIRC.Client.msg(state.client, :privmsg, chan, "#{nick}: ajouté à #{trigger}. (#{idx})")
+ msg.replyfun.("#{msg.sender.nick}: ajouté à #{trigger}. (#{idx})")
{:noreply, %__MODULE__{state | triggers: load()}}
else
- _ -> {:noreply, state}
+ _ ->
+ {:noreply, state}
end
end
- def handle_info({:received, "-"<>trigger_and_id, sender=%ExIRC.SenderInfo{nick: nick}, chan}, state) do
+ #
+ # TXT: DELETE
+ #
+
+ def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :minus, args: [id]}}}, state) do
with \
- [trigger, id] <- String.split(trigger_and_id, " ", parts: 2),
- {trigger, _} = clean_trigger(trigger),
- true <- can_write?(state, chan, sender, trigger),
+ true <- can_write?(state, msg, trigger),
data <- Map.get(state.triggers, trigger),
{id, ""} <- Integer.parse(id),
{text, _id} <- Enum.find(data, fn({_, idx}) -> id-1 == idx end)
do
data = data |> Enum.into(Map.new)
data = Map.delete(data, text)
- ExIRC.Client.msg(state.client, :privmsg, chan, "#{trigger}.txt##{id} supprimée: #{text}")
+ msg.replyfun.("#{msg.sender.nick}: #{trigger}.txt##{id} supprimée: #{text}")
dump(trigger, data)
{:noreply, %__MODULE__{state | triggers: load()}}
else
- error ->
- IO.inspect("error " <> inspect(error))
+ _ ->
{:noreply, state}
end
end
def handle_info(:reload_markov, state=%__MODULE__{triggers: triggers, markov: markov}) do
- all_data = triggers
- |> Enum.map(fn({_, data}) ->
- for {line, _idx} <- data, do: line
- end)
- |> List.flatten
-
- for l <- all_data, do: IO.puts(l)
-
- populate = ExChain.MarkovModel.populate_model(markov, all_data)
- IO.puts "populated markov: #{inspect(populate)}"
+ state.markov_handler.reload(state.triggers, state.markov)
{:noreply, state}
end
def handle_info(msg, state) do
+ IO.puts "txt unhandled #{inspect msg}"
{:noreply, state}
end
@@ -334,29 +387,42 @@ defmodule LSG.IRC.TxtHandler do
{trigger, opts}
end
- defp directory() do
+ def directory() do
Application.get_env(:lsg, :data_path) <> "/irc.txt/"
end
- defp can_write?(state = %__MODULE__{rw: rw?, locks: locks}, channel, sender, trigger) do
+ defp can_write?(%{rw: rw?, locks: locks}, msg = %{channel: nil, sender: sender}, trigger) do
admin? = IRC.admin?(sender)
- operator? = IRC.UserTrack.operator?(channel, sender.nick)
locked? = case :dets.lookup(locks, trigger) do
[{trigger}] -> true
_ -> false
end
unlocked? = if rw? == false, do: false, else: !locked?
- can? = admin? || operator? || unlocked?
+
+ can? = unlocked? || admin?
if !can? do
reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé"
- say(state, channel, "#{sender.nick}: permission refusée (#{reason})")
+ msg.replyfun.("#{sender.nick}: permission refusée (#{reason})")
end
can?
end
- defp say(%__MODULE__{client: client}, chan, message) do
- ExIRC.Client.msg(client, :privmsg, chan, message)
+ defp can_write?(state = %__MODULE__{rw: rw?, locks: locks}, msg = %{channel: channel, sender: sender}, trigger) do
+ admin? = IRC.admin?(sender)
+ operator? = IRC.UserTrack.operator?(channel, sender.nick)
+ locked? = case :dets.lookup(locks, trigger) do
+ [{trigger}] -> true
+ _ -> false
+ end
+ unlocked? = if rw? == false, do: false, else: !locked?
+ can? = admin? || operator? || unlocked?
+
+ if !can? do
+ reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé"
+ msg.replyfun.("#{sender.nick}: permission refusée (#{reason})")
+ end
+ can?
end
end
diff --git a/lib/lsg_irc/txt_plugin/markov.ex b/lib/lsg_irc/txt_plugin/markov.ex
new file mode 100644
index 0000000..311138c
--- /dev/null
+++ b/lib/lsg_irc/txt_plugin/markov.ex
@@ -0,0 +1,9 @@
+defmodule LSG.IRC.TxtPlugin.Markov do
+
+ @type state :: any()
+ @callback start_link() :: {:ok, state()}
+ @callback reload(content :: Map.t, state()) :: any()
+ @callback sentence(state()) :: {:ok, String.t} | {:error, String.t}
+ @callback complete_sentence(state()) :: {:ok, String.t} | {:error, String.t}
+
+end
diff --git a/lib/lsg_irc/txt_plugin/markov_native.ex b/lib/lsg_irc/txt_plugin/markov_native.ex
new file mode 100644
index 0000000..524e860
--- /dev/null
+++ b/lib/lsg_irc/txt_plugin/markov_native.ex
@@ -0,0 +1,33 @@
+defmodule LSG.IRC.TxtPlugin.MarkovNative do
+ @behaviour LSG.IRC.TxtPlugin.Markov
+
+ def start_link() do
+ ExChain.MarkovModel.start_link()
+ end
+
+ def reload(data, markov) do
+ data = data
+ |> Enum.map(fn({_, data}) ->
+ for {line, _idx} <- data, do: line
+ end)
+ |> List.flatten
+
+ ExChain.MarkovModel.populate_model(markov, data)
+ :ok
+ end
+
+ def sentence(markov) do
+ case ExChain.SentenceGenerator.create_filtered_sentence(markov) do
+ {:ok, line, _, _} -> {:ok, line}
+ error -> error
+ end
+ end
+
+ def complete_sentence(sentence, markov) do
+ case ExChain.SentenceGenerator.complete_sentence(markov, sentence) do
+ {line, _} -> {:ok, line}
+ error -> error
+ end
+ end
+
+end
diff --git a/lib/lsg_irc/txt_plugin/markov_py_markovify.ex b/lib/lsg_irc/txt_plugin/markov_py_markovify.ex
new file mode 100644
index 0000000..a3838cd
--- /dev/null
+++ b/lib/lsg_irc/txt_plugin/markov_py_markovify.ex
@@ -0,0 +1,39 @@
+defmodule LSG.IRC.TxtPlugin.MarkovPyMarkovify do
+
+ def start_link() do
+ {:ok, nil}
+ end
+
+ def reload(_data, _markov) do
+ :ok
+ end
+
+ def sentence(_) do
+ {:ok, run()}
+ end
+
+ def complete_sentence(sentence, _) do
+ {:ok, run([sentence])}
+ end
+
+ defp run(args \\ []) do
+ {binary, script} = script()
+ args = [script, Path.expand(LSG.IRC.TxtPlugin.directory()) | args]
+ IO.puts "Args #{inspect args}"
+ case MuonTrap.cmd(binary, args) do
+ {response, 0} -> response
+ {response, code} -> "error #{code}: #{response}"
+ end
+ end
+
+ defp script() do
+ default_script = to_string(:code.priv_dir(:lsg)) <> "/irc/txt/markovify.py"
+ env = Application.get_env(:lsg, LSG.IRC.TxtPlugin, [])
+ |> Keyword.get(:py_markovify, [])
+
+ {Keyword.get(env, :python, "python3"), Keyword.get(env, :script, default_script)}
+ end
+
+
+
+end
diff --git a/lib/lsg_irc/wikipedia_plugin.ex b/lib/lsg_irc/wikipedia_plugin.ex
index 4ee95b1..16e7c42 100644
--- a/lib/lsg_irc/wikipedia_plugin.ex
+++ b/lib/lsg_irc/wikipedia_plugin.ex
@@ -19,11 +19,11 @@ defmodule LSG.IRC.WikipediaPlugin do
{:ok, nil}
end
- def handle_info({:irc, :trigger, "wp", message = %IRC.Message{trigger: %IRC.Trigger{args: []}}}, state) do
+ def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do
irc_random(message)
{:noreply, state}
end
- def handle_info({:irc, :trigger, "wp", message = %IRC.Message{trigger: %IRC.Trigger{args: args}}}, state) do
+ def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do
irc_search(Enum.join(args, " "), message)
{:noreply, state}
end
diff --git a/lib/lsg_irc/youtube_handler.ex b/lib/lsg_irc/youtube_handler.ex
deleted file mode 100644
index 51584a2..0000000
--- a/lib/lsg_irc/youtube_handler.ex
+++ /dev/null
@@ -1,75 +0,0 @@
-defmodule LSG.IRC.YouTubeHandler do
- require Logger
-
- @moduledoc """
- # youtube
-
- * **!yt `<recherche>`**, !youtube `<recherche>`: retourne le premier résultat de la `<recherche>` YouTube
- """
-
- defstruct client: nil
-
- def irc_doc, do: @moduledoc
-
- def start_link(client) do
- GenServer.start_link(__MODULE__, [client])
- end
-
- def init([client]) do
- ExIRC.Client.add_handler(client, self())
- {:ok, %__MODULE__{client: client}}
- end
-
- def handle_info({:received, "!youtube " <> query, sender, chan}, state) do
- irc_search(query, chan, state)
- {:noreply, state}
- end
-
- def handle_info({:received, "!yt " <> query, sender, chan}, state) do
- irc_search(query, chan, state)
- {:noreply, state}
- end
-
- def handle_info(info, state) do
- {:noreply, state}
- end
-
- defp irc_search(query, chan, state) do
- case search(query) do
- {:ok, %{"items" => [item | _]}} ->
- title = get_in(item, ["snippet", "title"])
- url = "https://youtube.com/watch?v=" <> get_in(item, ["id", "videoId"])
- msg = "#{title} — #{url}"
- ExIRC.Client.msg(state.client, :privmsg, chan, msg)
- {:error, error} ->
- ExIRC.Client.msg(state.client, :privmsg, chan, "Erreur YouTube: "<>error)
- _ ->
- nil
- end
- end
-
- defp search(query) do
- query = query
- |> String.strip
- key = Application.get_env(:lsg, __MODULE__)[:api_key]
- params = %{
- "key" => key,
- "maxResults" => 1,
- "part" => "snippet",
- "safeSearch" => "none",
- "type" => "video",
- "q" => query,
- }
- url = "https://www.googleapis.com/youtube/v3/search"
- case HTTPoison.get(url, [], params: params) do
- {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> Jason.decode(body)
- {:ok, %HTTPoison.Response{status_code: 400, body: body}} ->
- Logger.error "YouTube HTTP 400: #{inspect body}"
- {:error, "http 400"}
- error ->
- Logger.error "YouTube http error: #{inspect error}"
- :error
- end
- end
-
-end
diff --git a/lib/lsg_irc/youtube_plugin.ex b/lib/lsg_irc/youtube_plugin.ex
new file mode 100644
index 0000000..e0781f0
--- /dev/null
+++ b/lib/lsg_irc/youtube_plugin.ex
@@ -0,0 +1,105 @@
+defmodule LSG.IRC.YouTubePlugin do
+ require Logger
+
+ @moduledoc """
+ # youtube
+
+ * **!yt `<recherche>`**, !youtube `<recherche>`: retourne le premier résultat de la `<recherche>` YouTube
+ """
+
+ defstruct client: nil
+
+ def irc_doc, do: @moduledoc
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:yt", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:youtube", [])
+ {:ok, %__MODULE__{}}
+ end
+
+ def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do
+ irc_search(Enum.join(args, " "), message)
+ {:noreply, state}
+ end
+
+ def handle_info(info, state) do
+ {:noreply, state}
+ end
+
+ defp irc_search(query, message) do
+ case search(query) do
+ {:ok, %{"items" => [item | _]}} ->
+ url = "https://youtube.com/watch?v=" <> item["id"]
+ snippet = item["snippet"]
+ duration = item["contentDetails"]["duration"] |> String.replace("PT", "") |> String.downcase
+ date = snippet["publishedAt"]
+ |> DateTime.from_iso8601()
+ |> elem(1)
+ |> Timex.format("{relative}", :relative)
+ |> elem(1)
+
+ info_line = "— #{duration} — uploaded by #{snippet["channelTitle"]} — #{date}"
+ <> " — #{item["statistics"]["viewCount"]} views, #{item["statistics"]["likeCount"]} likes,"
+ <> " #{item["statistics"]["dislikeCount"]} dislikes"
+ message.replyfun.("#{snippet["title"]} — #{url}")
+ message.replyfun.(info_line)
+ {:error, error} ->
+ message.replyfun.("Erreur YouTube: "<>error)
+ _ ->
+ nil
+ end
+ end
+
+ defp search(query) do
+ query = query
+ |> String.strip
+ key = Application.get_env(:lsg, :youtube)[:api_key]
+ params = %{
+ "key" => key,
+ "maxResults" => 1,
+ "part" => "id",
+ "safeSearch" => "none",
+ "type" => "video",
+ "q" => query,
+ }
+ url = "https://www.googleapis.com/youtube/v3/search"
+ case HTTPoison.get(url, [], params: params) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ {:ok, json} = Jason.decode(body)
+ item = List.first(json["items"])
+ if item do
+ video_id = item["id"]["videoId"]
+ params = %{
+ "part" => "snippet,contentDetails,statistics",
+ "id" => video_id,
+ "key" => key
+ }
+ headers = []
+ options = [params: params]
+ case HTTPoison.get("https://www.googleapis.com/youtube/v3/videos", [], options) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ Jason.decode(body)
+ {:ok, %HTTPoison.Response{status_code: code, body: body}} ->
+ Logger.error "YouTube HTTP #{code}: #{inspect body}"
+ {:error, "http #{code}"}
+ error ->
+ Logger.error "YouTube http error: #{inspect error}"
+ :error
+ end
+ else
+ :error
+ end
+ {:ok, %HTTPoison.Response{status_code: code, body: body}} ->
+ Logger.error "YouTube HTTP #{code}: #{inspect body}"
+ {:error, "http #{code}"}
+ error ->
+ Logger.error "YouTube http error: #{inspect error}"
+ :error
+ end
+ end
+
+end