diff options
author | Jordan Bracco <href@random.sh> | 2022-12-20 00:21:54 +0000 |
---|---|---|
committer | Jordan Bracco <href@random.sh> | 2022-12-20 19:29:41 +0100 |
commit | 2d83df8b32bff7f0028923bb5b64dc0b55f20d03 (patch) | |
tree | 1207e67b5b15f540963db05e7be89f3ca950e724 /lib/lsg_irc | |
parent | Nola rename, the end. pt 6. Refs T77. (diff) |
Nola rename: The Big Move, Refs T77
Diffstat (limited to 'lib/lsg_irc')
43 files changed, 0 insertions, 5916 deletions
diff --git a/lib/lsg_irc/admin_handler.ex b/lib/lsg_irc/admin_handler.ex deleted file mode 100644 index 9a5d557..0000000 --- a/lib/lsg_irc/admin_handler.ex +++ /dev/null @@ -1,41 +0,0 @@ -defmodule Nola.IRC.AdminHandler do - @moduledoc """ - # admin - - !op - op; requiert admin - """ - - 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 = IRC.register("op") - {:ok, client} - end - - def handle_info({:irc, :trigger, "op", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}, sender: sender}}, client) do - if IRC.admin?(sender) do - m.replyfun.({:mode, "+o"}) - else - m.replyfun.({:kick, "non"}) - end - {:noreply, client} - end - - def handle_info({:joined, chan, sender}, client) do - if IRC.admin?(sender) do - ExIRC.Client.mode(client, chan, "+o", sender.nick) - end - {:noreply, client} - end - - def handle_info(msg, client) do - {:noreply, client} - end - -end diff --git a/lib/lsg_irc/alcolog_plugin.ex b/lib/lsg_irc/alcolog_plugin.ex deleted file mode 100644 index 145e4fc..0000000 --- a/lib/lsg_irc/alcolog_plugin.ex +++ /dev/null @@ -1,1229 +0,0 @@ -defmodule Nola.IRC.AlcoologPlugin do - require Logger - - @moduledoc """ - # [alcoolog]({{context_path}}/alcoolog) - - * **!santai `<cl | (calc)>` `<degrés d'alcool> [annotation]`**: enregistre un nouveau verre de `montant` d'une boisson à `degrés d'alcool`. - * **!santai `<cl | (calc)>` `<beer name>`**: enregistre un nouveau verre de `cl` de la bière `beer name`, et checkin sur Untappd.com. - * **!moar `[cl]` : enregistre un verre équivalent au dernier !santai. - * **-santai**: annule la dernière entrée d'alcoolisme. - * **.alcoolisme**: état du channel en temps réel. - * **.alcoolisme `<semaine | Xj>`**: points par jour, sur X j. - * **!alcoolisme `[pseudo]`**: affiche les points d'alcoolisme. - * **!alcoolisme `[pseudo]` `<semaine | Xj>`**: affiche les points d'alcoolisme par jour sur X j. - * **+alcoolisme `<h|f>` `<poids en kg>` `[facteur de perte en mg/l (10, 15, 20, 25)]`**: Configure votre profil d'alcoolisme. - * **.sobre**: affiche quand la sobriété frappera sur le chan. - * **!sobre `[pseudo]`**: affiche quand la sobriété frappera pour `[pseudo]`. - * **!sobrepour `<date>`**: affiche tu pourras être sobre pour `<date>`, et si oui, combien de volumes d'alcool peuvent encore être consommés. - * **!alcoolog**: ([voir]({{context_path}}/alcoolog)) lien pour voir l'état/statistiques et historique de l'alcoolémie du channel. - * **!alcool `<cl>` `<degrés>`**: donne le nombre d'unités d'alcool dans `<cl>` à `<degrés>°`. - * **!soif**: c'est quand l'apéro ? - - 1 point = 1 volume d'alcool. - - Annotation: champ libre! - - --- - - ## `!txt`s - - * status utilisateur: `alcoolog.user_(sober|legal|legalhigh|high|toohigh|sick)(|_rising)` - * mauvaises boissons: `alcoolog.drink_(negative|zero|negative)` - * santo: `alcoolog.santo` - * santai: `alcoolog.santai` - * plus gros, moins gros: `alcoolog.(fatter|thinner)` - - """ - - def irc_doc, do: @moduledoc - - def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) - - # tuple dets: {nick, date, volumes, current_alcohol_level, nom, commentaire} - # tuple ets: {{nick, date}, volumes, current, nom, commentaire} - # tuple meta dets: {nick, map} - # %{:weight => float, :sex => true(h),false(f)} - @pubsub ~w(account) - @pubsub_triggers ~w(santai moar again bis santo santeau alcoolog sobre sobrepour soif alcoolisme alcool) - @default_user_meta %{weight: 77.4, sex: true, loss_factor: 15} - - def data_state() do - dets_filename = (Nola.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist - dets_meta_filename = (Nola.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist - %{dets: dets_filename, meta: dets_meta_filename, ets: __MODULE__.ETS} - end - - def init(_) do - triggers = for(t <- @pubsub_triggers, do: "trigger:"<>t) - for sub <- @pubsub ++ triggers do - {:ok, _} = Registry.register(IRC.PubSub, sub, plugin: __MODULE__) - end - dets_filename = (Nola.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist - {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}]) - ets = :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}]) - dets_meta_filename = (Nola.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist - {:ok, meta} = :dets.open_file(dets_meta_filename, [{:type,:set}]) - traverse_fun = fn(obj, dets) -> - case obj do - object = {nick, naive = %NaiveDateTime{}, volumes, active, cl, deg, name, comment} -> - date = naive - |> DateTime.from_naive!("Etc/UTC") - |> DateTime.to_unix() - new = {nick, date, volumes, active, cl, deg, name, comment, Map.new()} - :dets.delete_object(dets, object) - :dets.insert(dets, new) - :ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, Map.new()}) - dets - - object = {nick, naive = %NaiveDateTime{}, volumes, active, cl, deg, name, comment, meta} -> - date = naive - |> DateTime.from_naive!("Etc/UTC") - |> DateTime.to_unix() - new = {nick, date, volumes, active, cl, deg, name, comment, Map.new()} - :dets.delete_object(dets, object) - :dets.insert(dets, new) - :ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, Map.new()}) - dets - - object = {nick, date, volumes, active, cl, deg, name, comment, meta} -> - :ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, meta}) - dets - - _ -> - dets - end - end - :dets.foldl(traverse_fun, dets, dets) - :dets.sync(dets) - state = %{dets: dets, meta: meta, ets: ets} - {:ok, state} - end - - @eau ["santo", "santeau"] - def handle_info({:irc, :trigger, santeau, m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) when santeau in @eau do - Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.santo") - {:noreply, state} - end - - def handle_info({:irc, :trigger, "soif", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do - now = DateTime.utc_now() - |> Timex.Timezone.convert("Europe/Paris") - apero = format_duration_from_now(%DateTime{now | hour: 18, minute: 0, second: 0}, false) - day_of_week = Date.day_of_week(now) - {txt, apero?} = cond do - now.hour >= 0 && now.hour < 6 -> - {["apéro tardif ? Je dis OUI ! SANTAI !"], true} - now.hour >= 6 && now.hour < 12 -> - if day_of_week >= 6 do - {["de l'alcool pour le petit dej ? le week-end, pas de problème !"], true} - else - {["C'est quand même un peu tôt non ? Prochain apéro #{apero}"], false} - end - now.hour >= 12 && (now.hour < 14) -> - {["oui! c'est l'apéro de midi! (et apéro #{apero})", - "tu peux attendre #{apero} ou y aller, il est midi !" - ], true} - now.hour == 17 -> - {[ - "ÇA APPROCHE !!! Apéro #{apero}", - "BIENTÔT !!! Apéro #{apero}", - "achetez vite les teilles, apéro dans #{apero}!", - "préparez les teilles, apéro dans #{apero}!" - ], false} - now.hour >= 14 && now.hour < 18 -> - weekend = if day_of_week >= 6 do - " ... ou maintenant en fait, c'est le week-end!" - else - "" - end - {["tiens bon! apéro #{apero}#{weekend}", - "courage... apéro dans #{apero}#{weekend}", - "pas encore :'( apéro dans #{apero}#{weekend}" - ], false} - true -> - {[ - "C'EST L'HEURE DE L'APÉRO !!! SANTAIIIIIIIIIIII !!!!" - ], true} - end - - txt = txt - |> Enum.shuffle() - |> Enum.random() - - m.replyfun.(txt) - - stats = get_full_statistics(state, m.account.id) - if !apero? && stats.active > 0.1 do - m.replyfun.("(... ou continue en fait, je suis pas ta mère !)") - end - - {:noreply, state} - end - - def handle_info({:irc, :trigger, "sobrepour", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do - args = Enum.join(args, " ") - {:ok, now} = DateTime.now("Europe/Paris", Tzdata.TimeZoneDatabase) - time = case args do - "demain " <> time -> - {h, m} = case String.split(time, [":", "h"]) do - [hour, ""] -> - IO.puts ("h #{inspect hour}") - {h, _} = Integer.parse(hour) - {h, 0} - [hour, min] when min != "" -> - {h, _} = Integer.parse(hour) - {m, _} = Integer.parse(min) - {h, m} - [hour] -> - IO.puts ("h #{inspect hour}") - {h, _} = Integer.parse(hour) - {h, 0} - _ -> {0, 0} - end - secs = ((60*60)*24) - day = DateTime.add(now, secs, :second, Tzdata.TimeZoneDatabase) - %DateTime{day | hour: h, minute: m, second: 0} - "après demain " <> time -> - secs = 2*((60*60)*24) - DateTime.add(now, secs, :second, Tzdata.TimeZoneDatabase) - datetime -> - case Timex.Parse.DateTime.Parser.parse(datetime, "{}") do - {:ok, dt} -> dt - _ -> nil - end - end - - if time do - meta = get_user_meta(state, m.account.id) - stats = get_full_statistics(state, m.account.id) - - duration = round(DateTime.diff(time, now)/60.0) - - IO.puts "diff #{inspect duration} sober in #{inspect stats.sober_in}" - - if duration < stats.sober_in do - int = stats.sober_in - duration - m.replyfun.("désolé, aucune chance! tu seras sobre #{format_minute_duration(int)} après!") - else - remaining = duration - stats.sober_in - if remaining < 30 do - m.replyfun.("moins de 30 minutes de sobriété, c'est impossible de boire plus") - else - loss_per_minute = ((meta.loss_factor/100)/60) - remaining_gl = (remaining-30)*loss_per_minute - m.replyfun.("marge de boisson: #{inspect remaining} minutes, #{remaining_gl} g/l") - end - end - - end - {:noreply, state} - end - - def handle_info({:irc, :trigger, "alcoolog", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do - {:ok, token} = Nola.Token.new({:alcoolog, :index, m.sender.network, m.channel}) - url = NolaWeb.Router.Helpers.alcoolog_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel), token) - m.replyfun.("-> #{url}") - {:noreply, state} - end - - def handle_info({:irc, :trigger, "alcoolog", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :bang}}}, state) do - url = NolaWeb.Router.Helpers.alcoolog_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel)) - m.replyfun.("-> #{url}") - {:noreply, state} - end - - def handle_info({:irc, :trigger, "alcool", m = %IRC.Message{trigger: %IRC.Trigger{args: args = [cl, deg], type: :bang}}}, state) do - {cl, _} = Util.float_paparse(cl) - {deg, _} = Util.float_paparse(deg) - points = Alcool.units(cl, deg) - meta = get_user_meta(state, m.account.id) - k = if meta.sex, do: 0.7, else: 0.6 - weight = meta.weight - gl = (10*points)/(k*weight) - duration = round(gl/((meta.loss_factor/100)/60))+30 - sober_in_s = if duration > 0 do - duration = Timex.Duration.from_minutes(duration) - Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized) - else - "" - end - - m.replyfun.("Il y a #{Float.round(points+0.0, 4)} unités d'alcool dans #{cl}cl à #{deg}° (#{Float.round(gl + 0.0, 4)} g/l, #{sober_in_s})") - {:noreply, state} - end - - def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: [cl, deg | comment], type: :bang}}}, state) do - santai(m, state, cl, deg, comment) - {:noreply, state} - end - - @moar [ - "{{message.sender.nick}}: la même donc ?", - "{{message.sender.nick}}: et voilà la petite sœur !" - ] - - def handle_info({:irc, :trigger, "bis", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do - handle_info({:irc, :trigger, "moar", m}, state) - end - def handle_info({:irc, :trigger, "again", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do - handle_info({:irc, :trigger, "moar", m}, state) - end - - def handle_info({:irc, :trigger, "moar", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do - case get_statistics_for_nick(state, m.account.id) do - {_, obj = {_, _date, _points, _active, cl, deg, _name, comment, _meta}} -> - cl = case args do - [cls] -> - case Util.float_paparse(cls) do - {cl, _} -> cl - _ -> cl - end - _ -> cl - end - moar = @moar |> Enum.shuffle() |> Enum.random() |> Tmpl.render(m) |> m.replyfun.() - santai(m, state, cl, deg, comment, auto_set: true) - {_, obj = {_, date, points, _last_active, type, descr}} -> - case Regex.named_captures(~r/^(?<cl>\d+[.]\d+)cl\s+(?<deg>\d+[.]\d+)°$/, type) do - nil -> m.replyfun.("suce") - u -> - moar = @moar |> Enum.shuffle() |> Enum.random() |> Tmpl.render(m) |> m.replyfun.() - santai(m, state, u["cl"], u["deg"], descr, auto_set: true) - end - _ -> nil - end - {:noreply, state} - end - - defp santai(m, state, cl, deg, comment, options \\ []) do - comment = cond do - comment == [] -> nil - is_binary(comment) -> comment - comment == nil -> nil - true -> Enum.join(comment, " ") - end - - {cl, cl_extra} = case {Util.float_paparse(cl), cl} do - {{cl, extra}, _} -> {cl, extra} - {:error, "("<>_} -> - try do - {:ok, result} = Abacus.eval(cl) - {result, nil} - rescue - _ -> {nil, "cl: invalid calc expression"} - end - {:error, _} -> {nil, "cl: invalid value"} - end - - {deg, comment, auto_set, beer_id} = case Util.float_paparse(deg) do - {deg, _} -> {deg, comment, Keyword.get(options, :auto_set, false), nil} - :error -> - beername = if(comment, do: "#{deg} #{comment}", else: deg) - case Untappd.search_beer(beername, limit: 1) do - {:ok, %{"response" => %{"beers" => %{"count" => count, "items" => [%{"beer" => beer, "brewery" => brewery} | _]}}}} -> - {Map.get(beer, "beer_abv"), "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")}", true, Map.get(beer, "bid")} - _ -> - {deg, "could not find beer", false, nil} - end - end - - cond do - cl == nil -> m.replyfun.(cl_extra) - deg == nil -> m.replyfun.(comment) - cl >= 500 || deg >= 100 -> Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.drink_toohuge") - cl == 0 || deg == 0 -> Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.drink_zero") - cl < 0 || deg < 0 -> Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.drink_negative") - true -> - points = Alcool.units(cl, deg) - now = m.at || DateTime.utc_now() - |> DateTime.to_unix(:millisecond) - user_meta = get_user_meta(state, m.account.id) - name = "#{cl}cl #{deg}°" - old_stats = get_full_statistics(state, m.account.id) - meta = %{} - meta = Map.put(meta, "timestamp", now) - meta = Map.put(meta, "weight", user_meta.weight) - meta = Map.put(meta, "sex", user_meta.sex) - :ok = :dets.insert(state.dets, {m.account.id, now, points, if(old_stats, do: old_stats.active, else: 0), cl, deg, name, comment, meta}) - true = :ets.insert(state.ets, {{m.account.id, now}, points, if(old_stats, do: old_stats.active, else: 0),cl, deg, name, comment, meta}) - #sante = @santai |> Enum.map(fn(s) -> String.trim(String.upcase(s)) end) |> Enum.shuffle() |> Enum.random() - sante = Nola.IRC.TxtPlugin.random("alcoolog.santai") - k = if user_meta.sex, do: 0.7, else: 0.6 - weight = user_meta.weight - peak = Float.round((10*points||0.0)/(k*weight), 4) - stats = get_full_statistics(state, m.account.id) - sober_add = if old_stats && Map.get(old_stats || %{}, :sober_in) do - mins = round(stats.sober_in - old_stats.sober_in) - " [+#{mins}m]" - else - "" - end - nonow = DateTime.utc_now() - sober = nonow |> DateTime.add(round(stats.sober_in*60), :second) - |> Timex.Timezone.convert("Europe/Paris") - at = if nonow.day == sober.day do - {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr") - detail - else - {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr") - detail - end - - up = if stats.active_drinks > 1 do - " " <> Enum.join(for(_ <- 1..stats.active_drinks, do: "▲")) <> "" - else - "" - end - - since_str = if stats.since && stats.since_min > 180 do - "(depuis: #{stats.since_s}) " - else - "" - end - - msg = fn(nick, extra) -> - "#{sante} #{nick} #{extra}#{up} #{format_points(points)} @#{stats.active}g/l [+#{peak} g/l]" - <> " (15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) #{since_str}(sobriété #{at} (dans #{stats.sober_in_s})#{sober_add}) !" - <> " (aujourd'hui #{stats.daily_volumes} points - #{stats.daily_gl} g/l)" - end - - meta = if beer_id do - Map.put(meta, "untappd:beer_id", beer_id) - else - meta - end - - if beer_id do - spawn(fn() -> - case Untappd.maybe_checkin(m.account, beer_id) do - {:ok, body} -> - badges = get_in(body, ["badges", "items"]) - if badges != [] do - badges_s = Enum.map(badges, fn(badge) -> Map.get(badge, "badge_name") end) - |> Enum.filter(fn(b) -> b end) - |> Enum.intersperse(", ") - |> Enum.join("") - badge = if(length(badges) > 1, do: "badges", else: "badge") - m.replyfun.("\\O/ Unlocked untappd #{badge}: #{badges_s}") - end - :ok - {:error, {:http_error, error}} when is_integer(error) -> m.replyfun.("Checkin to Untappd failed: #{to_string(error)}") - {:error, {:http_error, error}} -> m.replyfun.("Checkin to Untappd failed: #{inspect error}") - _ -> :error - end - end) - end - - local_extra = if auto_set do - if comment do - " #{comment} (#{cl}cl @ #{deg}°)" - else - "#{cl}cl @ #{deg}°" - end - else - "" - end - m.replyfun.(msg.(m.sender.nick, local_extra)) - notify = IRC.Membership.notify_channels(m.account) -- [{m.network,m.channel}] - for {net, chan} <- notify do - user = IRC.UserTrack.find_by_account(net, m.account) - nick = if(user, do: user.nick, else: m.account.name) - extra = " " <> present_type(name, comment) <> "" - IRC.Connection.broadcast_message(net, chan, msg.(nick, extra)) - end - - miss = cond do - points <= 0.6 -> :small - stats.active30m >= 2.9 && stats.active30m < 3 -> :miss3 - stats.active30m >= 1.9 && stats.active30m < 2 -> :miss2 - stats.active30m >= 0.9 && stats.active30m < 1 -> :miss1 - stats.active30m >= 0.45 && stats.active30m < 0.5 -> :miss05 - stats.active30m >= 0.20 && stats.active30m < 0.20 -> :miss025 - stats.active30m >= 3 && stats.active1h < 3.15 -> :small3 - stats.active30m >= 2 && stats.active1h < 2.15 -> :small2 - stats.active30m >= 1.5 && stats.active1h < 1.5 -> :small15 - stats.active30m >= 1 && stats.active1h < 1.15 -> :small1 - stats.active30m >= 0.5 && stats.active1h <= 0.51 -> :small05 - stats.active30m >= 0.25 && stats.active30m <= 0.255 -> :small025 - true -> nil - end - - if miss do - miss = Nola.IRC.TxtPlugin.random("alcoolog.#{to_string(miss)}") - if miss do - for {net, chan} <- IRC.Membership.notify_channels(m.account) do - user = IRC.UserTrack.find_by_account(net, m.account) - nick = if(user, do: user.nick, else: m.account.name) - IRC.Connection.broadcast_message(net, chan, "#{nick}: #{miss}") - end - end - end - end - end - - def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do - m.replyfun.("!santai <cl> <degrés> [commentaire]") - {:noreply, state} - end - - def get_all_stats() do - IRC.Account.all_accounts() - |> Enum.map(fn(account) -> {account.id, get_full_statistics(account.id)} end) - |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end) - |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2) - end - - def get_channel_statistics(account, network, nil) do - IRC.Membership.expanded_members_or_friends(account, network, nil) - |> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(account.id)} end) - |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end) - |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2) - end - - def get_channel_statistics(_, network, channel), do: get_channel_statistics(network, channel) - - def get_channel_statistics(network, channel) do - IRC.Membership.expanded_members(network, channel) - |> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(account.id)} end) - |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end) - |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2) - end - - @spec since() :: %{IRC.Account.id() => DateTime.t()} - @doc "Returns the last time the user was at 0 g/l" - def since() do - :ets.foldr(fn({{acct, timestamp_or_date}, _vol, current, _cl, _deg, _name, _comment, _m}, acc) -> - if !Map.get(acc, acct) && current == 0 do - date = Util.to_date_time(timestamp_or_date) - Map.put(acc, acct, date) - else - acc - end - end, %{}, __MODULE__.ETS) - end - - def get_full_statistics(nick) do - get_full_statistics(data_state(), nick) - end - - defp get_full_statistics(state, nick) do - case get_statistics_for_nick(state, nick) do - {count, {_, last_at, last_points, last_active, last_cl, last_deg, last_type, last_descr, _meta}} -> - {active, active_drinks} = current_alcohol_level(state, nick) - {_, m30} = alcohol_level_rising(state, nick) - {rising, m15} = alcohol_level_rising(state, nick, 15) - {_, m5} = alcohol_level_rising(state, nick, 5) - {_, h1} = alcohol_level_rising(state, nick, 60) - - trend = if rising do - "▲" - else - "▼" - end - user_state = cond do - active <= 0.0 -> :sober - active <= 0.25 -> :low - active <= 0.50 -> :legal - active <= 1.0 -> :legalhigh - active <= 2.5 -> :high - active < 3 -> :toohigh - true -> :sick - end - - rising_file_key = if rising, do: "_rising", else: "" - txt_file = "alcoolog." <> "user_" <> to_string(user_state) <> rising_file_key - user_status = Nola.IRC.TxtPlugin.random(txt_file) - - meta = get_user_meta(state, nick) - minutes_til_sober = h1/((meta.loss_factor/100)/60) - minutes_til_sober = cond do - active < 0 -> 0 - m15 < 0 -> 15 - m30 < 0 -> 30 - h1 < 0 -> 60 - minutes_til_sober > 0 -> - Float.round(minutes_til_sober+60) - true -> 0 - end - - duration = Timex.Duration.from_minutes(minutes_til_sober) - sober_in_s = if minutes_til_sober > 0 do - Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized) - else - nil - end - - since = if active > 0 do - since() - |> Map.get(nick) - end - - since_diff = if since, do: Timex.diff(DateTime.utc_now(), since, :minutes) - since_duration = if since, do: Timex.Duration.from_minutes(since_diff) - since_s = if since, do: Timex.Format.Duration.Formatter.lformat(since_duration, "fr", :humanized) - - {total_volumes, total_gl} = user_stats(state, nick) - - - %{active: active, last_at: last_at, last_cl: last_cl, last_deg: last_deg, last_points: last_points, last_type: last_type, last_descr: last_descr, - trend_symbol: trend, - active5m: m5, active15m: m15, active30m: m30, active1h: h1, - rising: rising, - active_drinks: active_drinks, - user_status: user_status, - daily_gl: total_gl, daily_volumes: total_volumes, - sober_in: minutes_til_sober, sober_in_s: sober_in_s, - since: since, since_min: since_diff, since_s: since_s, - } - _ -> - nil - end - end - - def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :dot}}}, state) do - nicks = IRC.Membership.expanded_members_or_friends(m.account, m.network, m.channel) - |> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(state, account.id)} end) - |> Enum.filter(fn({_nick, status}) -> status && status.sober_in && status.sober_in > 0 end) - |> Enum.sort_by(fn({_, status}) -> status.sober_in end, &</2) - |> Enum.map(fn({nick, stats}) -> - now = DateTime.utc_now() - sober = now |> DateTime.add(round(stats.sober_in*60), :second) - |> Timex.Timezone.convert("Europe/Paris") - at = if now.day == sober.day do - {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr") - detail - else - {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr") - detail - end - "#{nick} sobre #{at} (dans #{stats.sober_in_s})" - end) - |> Enum.intersperse(", ") - |> Enum.join("") - |> (fn(line) -> - case line do - "" -> "tout le monde est sobre......." - line -> line - end - end).() - |> m.replyfun.() - {:noreply, state} - end - - def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do - account = case args do - [nick] -> IRC.Account.find_always_by_nick(m.network, m.channel, nick) - [] -> m.account - end - - if account do - user = IRC.UserTrack.find_by_account(m.network, account) - nick = if(user, do: user.nick, else: account.name) - stats = get_full_statistics(state, account.id) - if stats && stats.sober_in > 0 do - now = DateTime.utc_now() - sober = now |> DateTime.add(round(stats.sober_in*60), :second) - |> Timex.Timezone.convert("Europe/Paris") - at = if now.day == sober.day do - {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr") - detail - else - {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr") - detail - end - m.replyfun.("#{nick} sera sobre #{at} (dans #{stats.sober_in_s})!") - else - m.replyfun.("#{nick} est déjà sobre. aidez le !") - end - else - m.replyfun.("inconnu") - end - {:noreply, state} - end - - def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :dot}}}, state) do - nicks = IRC.Membership.expanded_members_or_friends(m.account, m.network, m.channel) - |> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(state, account.id)} end) - |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end) - |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2) - |> Enum.map(fn({nick, status}) -> - trend_symbol = if status.active_drinks > 1 do - Enum.join(for(_ <- 1..status.active_drinks, do: status.trend_symbol)) - else - status.trend_symbol - end - since_str = if status.since_min > 180 do - "depuis: #{status.since_s} | " - else - "" - end - "#{nick} #{status.user_status} #{trend_symbol} #{Float.round(status.active, 4)} g/l [#{since_str}sobre dans: #{status.sober_in_s}]" - end) - |> Enum.intersperse(", ") - |> Enum.join("") - - msg = if nicks == "" do - "wtf?!?! personne n'a bu!" - else - nicks - end - - m.replyfun.(msg) - {:noreply, state} - end - - def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [time], type: :dot}}}, state) do - time = case time do - "semaine" -> 7 - string -> - case Integer.parse(string) do - {time, "j"} -> time - {time, "J"} -> time - _ -> nil - end - end - - if time do - aday = time*((24 * 60)*60) - now = DateTime.utc_now() - before = now - |> DateTime.add(-aday, :second) - |> DateTime.to_unix(:millisecond) - over_time_stats(before, time, m, state) - else - m.replyfun.(".alcooolisme semaine|Xj") - end - {:noreply, state} - end - - def user_over_time(account, count) do - user_over_time(data_state(), account, count) - end - - def user_over_time(state, account, count) do - delay = count*((24 * 60)*60) - now = DateTime.utc_now() - before = DateTime.utc_now() - |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase) - |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase) - |> DateTime.to_unix(:millisecond) - #[ -# {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_}, -# [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]} - #] - match = [{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_}, - [{:andalso, {:>, :"$2", {:const, before}}, {:==, :"$1", {:const, account.id}}}], [:"$_"]} - ] - :ets.select(state.ets, match) - |> Enum.reduce(Map.new, fn({{_, ts}, vol, _, _, _, _, _, _}, acc) -> - date = DateTime.from_unix!(ts, :millisecond) - |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase) - - date = if date.hour <= 8 do - DateTime.add(date, -(60*(60*(date.hour+1))), :second, Tzdata.TimeZoneDatabase) - else - date - end - |> DateTime.to_date() - - Map.put(acc, date, Map.get(acc, date, 0) + vol) - end) - end - - def user_over_time_gl(account, count) do - state = data_state() - meta = get_user_meta(state, account.id) - delay = count*((24 * 60)*60) - now = DateTime.utc_now() - before = DateTime.utc_now() - |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase) - |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase) - |> DateTime.to_unix(:millisecond) - #[ -# {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_}, -# [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]} - #] - match = [{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_}, - [{:andalso, {:>, :"$2", {:const, before}}, {:==, :"$1", {:const, account.id}}}], [:"$_"]} - ] - :ets.select(state.ets, match) - |> Enum.reduce(Map.new, fn({{_, ts}, vol, _, _, _, _, _, _}, acc) -> - date = DateTime.from_unix!(ts, :millisecond) - |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase) - - date = if date.hour <= 8 do - DateTime.add(date, -(60*(60*(date.hour+1))), :second, Tzdata.TimeZoneDatabase) - else - date - end - |> DateTime.to_date() - weight = meta.weight - k = if meta.sex, do: 0.7, else: 0.6 - gl = (10*vol)/(k*weight) - - Map.put(acc, date, Map.get(acc, date, 0) + gl) - end) - end - - - - defp over_time_stats(before, j, m, state) do - #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _, _, _, _}) when date > before -> obj end) - match = [{{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_}, - [{:>, :"$1", {:const, before}}], [:"$_"]} - ] - # tuple ets: {{nick, date}, volumes, current, nom, commentaire} - members = IRC.Membership.members_or_friends(m.account, m.network, m.channel) - drinks = :ets.select(state.ets, match) - |> Enum.filter(fn({{account, _}, _, _, _, _, _, _, _}) -> Enum.member?(members, account) end) - |> Enum.sort_by(fn({{_, ts}, _, _, _, _, _, _, _}) -> ts end, &>/2) - - top = Enum.reduce(drinks, %{}, fn({{nick, _}, vol, _, _, _, _, _, _}, acc) -> - all = Map.get(acc, nick, 0) - Map.put(acc, nick, all + vol) - end) - |> Enum.sort_by(fn({_nick, count}) -> count end, &>/2) - |> Enum.map(fn({nick, count}) -> - account = IRC.Account.get(nick) - user = IRC.UserTrack.find_by_account(m.network, account) - nick = if(user, do: user.nick, else: account.name) - "#{nick}: #{Float.round(count, 4)}" - end) - |> Enum.intersperse(", ") - - m.replyfun.("sur #{j} jours: #{top}") - {:noreply, state} - end - - def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do - meta = get_user_meta(state, m.account.id) - hf = if meta.sex, do: "h", else: "f" - m.replyfun.("+alcoolisme sexe: #{hf} poids: #{meta.weight} facteur de perte: #{meta.loss_factor}") - {:noreply, state} - end - - def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [h, weight | rest], type: :plus}}}, state) do - h = case h do - "h" -> true - "f" -> false - _ -> nil - end - - weight = case Util.float_paparse(weight) do - {weight, _} -> weight - _ -> nil - end - - {factor} = case rest do - [factor] -> - case Util.float_paparse(factor) do - {float, _} -> {float} - _ -> {@default_user_meta.loss_factor} - end - _ -> {@default_user_meta.loss_factor} - end - - if h == nil || weight == nil do - m.replyfun.("paramètres invalides") - else - old_meta = get_user_meta(state, m.account.id) - meta = Map.merge(@default_user_meta, %{sex: h, weight: weight, loss_factor: factor}) - put_user_meta(state, m.account.id, meta) - cond do - old_meta.weight < meta.weight -> - Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.fatter") - old_meta.weight == meta.weight -> - m.replyfun.("aucun changement!") - true -> - Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.thinner") - end - end - - {:noreply, state} - end - - def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :minus}}}, state) do - case get_statistics_for_nick(state, m.account.id) do - {_, obj = {_, date, points, _last_active, _cl, _deg, type, descr, _meta}} -> - :dets.delete_object(state.dets, obj) - :ets.delete(state.ets, {m.account.id, date}) - m.replyfun.("supprimé: #{m.sender.nick} #{points} #{type} #{descr}") - Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.delete") - notify = IRC.Membership.notify_channels(m.account) -- [{m.network,m.channel}] - for {net, chan} <- notify do - user = IRC.UserTrack.find_by_account(net, m.account) - nick = if(user, do: user.nick, else: m.account.name) - IRC.Connection.broadcast_message(net, chan, "#{nick} -santai #{points} #{type} #{descr}") - end - {:noreply, state} - _ -> - {:noreply, state} - end - end - - - def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do - {account, duration} = case args do - [nick | rest] -> {IRC.Account.find_always_by_nick(m.network, m.channel, nick), rest} - [] -> {m.account, []} - end - if account do - duration = case duration do - ["semaine"] -> 7 - [j] -> - case Integer.parse(j) do - {j, "j"} -> j - _ -> nil - end - _ -> nil - end - user = IRC.UserTrack.find_by_account(m.network, account) - nick = if(user, do: user.nick, else: account.name) - if duration do - if duration > 90 do - m.replyfun.("trop gros, ça rentrera pas") - else - # duration stats - stats = user_over_time(state, account, duration) - |> Enum.sort_by(fn({k,_v}) -> k end, {:asc, Date}) - |> Enum.map(fn({date, count}) -> - "#{date.day}: #{Float.round(count, 2)}" - end) - |> Enum.intersperse(", ") - |> Enum.join("") - - if stats == "" do - m.replyfun.("alcoolisme a zéro sur #{duration}j :/") - else - m.replyfun.("alcoolisme de #{nick}, #{duration} derniers jours: #{stats}") - end - end - else - if stats = get_full_statistics(state, account.id) do - trend_symbol = if stats.active_drinks > 1 do - Enum.join(for(_ <- 1..stats.active_drinks, do: stats.trend_symbol)) - else - stats.trend_symbol - end - # TODO: Lookup nick for account_id - msg = "#{nick} #{stats.user_status} " - <> (if stats.active > 0 || stats.active15m > 0 || stats.active30m > 0 || stats.active1h > 0, do: ": #{trend_symbol} #{Float.round(stats.active, 4)}g/l ", else: "") - <> (if stats.active30m > 0 || stats.active1h > 0, do: "(15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) ", else: "") - <> (if stats.sober_in > 0, do: "— Sobre dans #{stats.sober_in_s} ", else: "") - <> (if stats.since && stats.since_min > 180, do: "— Paitai depuis #{stats.since_s} ", else: "") - <> "— Dernier verre: #{present_type(stats.last_type, stats.last_descr)} [#{Float.round(stats.last_points+0.0, 4)}] " - <> "#{format_duration_from_now(stats.last_at)} " - <> (if stats.daily_volumes > 0, do: "— Aujourd'hui: #{stats.daily_volumes} #{stats.daily_gl}g/l", else: "") - - m.replyfun.(msg) - else - m.replyfun.("honteux mais #{nick} n'a pas l'air alcoolique du tout. /kick") - end - end - else - m.replyfun.("je ne connais pas cet utilisateur") - end - {:noreply, state} - end - - - # Account merge - def handle_info({:account_change, old_id, new_id}, state) do - spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}] - Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) -> - Logger.debug("alcolog/account_change:: merging #{old_id} -> #{new_id}") - rename_object_owner(table, state.ets, obj, old_id, new_id) - end) - case :dets.lookup(state.meta, {:meta, old_id}) do - [{_, meta}] -> - :dets.delete(state.meta, {:meta, old_id}) - :dets.insert(state.meta, {{:meta, new_id}, meta}) - _ -> - :ok - end - {:noreply, state} - end - - def terminate(_, state) do - for dets <- [state.dets, state.meta] do - :dets.sync(dets) - :dets.close(dets) - end - end - - defp rename_object_owner(table, ets, object = {old_id, date, volume, current, cl, deg, name, comment, meta}, old_id, new_id) do - :dets.delete_object(table, object) - :ets.delete(ets, {old_id, date}) - :dets.insert(table, {new_id, date, volume, current, cl, deg, name, comment, meta}) - :ets.insert(ets, {{new_id, date}, volume, current, cl, deg, name, comment, meta}) - end - - # Account: move from nick to account id - def handle_info({:accounts, accounts}, state) do - #for x={:account, _, _, _, _} <- accounts, do: handle_info(x, state) - #{:noreply, state} - mapping = Enum.reduce(accounts, Map.new, fn({:account, _net, _chan, nick, account_id}, acc) -> - Map.put(acc, String.downcase(nick), account_id) - end) - spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [], [:"$_"]}] - Logger.debug("accounts:: mappings #{inspect mapping}") - Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj = {nick, _date, _vol, _cur, _cl, _deg, _name, _comment, _meta}) -> - #Logger.debug("accounts:: item #{inspect(obj)}") - if new_id = Map.get(mapping, nick) do - Logger.debug("alcolog/accounts:: merging #{nick} -> #{new_id}") - rename_object_owner(table, state.ets, obj, nick, new_id) - end - end) - {:noreply, state} - end - - def handle_info({:account, _net, _chan, nick, account_id}, state) do - nick = String.downcase(nick) - spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}] - Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) -> - Logger.debug("alcoolog/account:: merging #{nick} -> #{account_id}") - rename_object_owner(table, state.ets, obj, nick, account_id) - end) - case :dets.lookup(state.meta, {:meta, nick}) do - [{_, meta}] -> - :dets.delete(state.meta, {:meta, nick}) - :dets.insert(state.meta, {{:meta, account_id}, meta}) - _ -> - :ok - end - {:noreply, state} - end - - def handle_info(t, state) do - Logger.debug("AlcoologPlugin: unhandled info #{inspect t}") - {:noreply, state} - end - - def nick_history(account) do - spec = [ - {{{:"$1", :_}, :_, :_, :_, :_, :_, :_, :_}, - [{:==, :"$1", {:const, account.id}}], - [:"$_"]} - ] - :ets.select(data_state().ets, spec) - end - - defp get_statistics_for_nick(state, account_id) do - qvc = :dets.lookup(state.dets, account_id) - |> Enum.sort_by(fn({_, ts, _, _, _, _, _, _, _}) -> ts end, &</2) - count = Enum.reduce(qvc, 0, fn({_nick, _ts, points, _active, _cl, _deg, _type, _descr, _meta}, acc) -> acc + (points||0) 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 is_integer(int) and int > 0 do - "+#{Integer.to_string(int)}" - end - def format_points(int) when is_integer(int) and int < 0 do - Integer.to_string(int) - end - def format_points(int) when is_float(int) and int > 0 do - "+#{Float.to_string(Float.round(int,4))}" - end - def format_points(int) when is_float(int) and int < 0 do - Float.to_string(Float.round(int,4)) - end - def format_points(0), do: "0" - def format_points(0.0), do: "0" - - defp format_relative_timestamp(timestamp) do - alias Timex.Format.DateTime.Formatters - alias Timex.Timezone - date = timestamp - |> DateTime.from_unix!(:millisecond) - |> 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 - - defp put_user_meta(state, account_id, meta) do - :dets.insert(state.meta, {{:meta, account_id}, meta}) - :ok - end - - defp get_user_meta(%{meta: meta}, account_id) do - case :dets.lookup(meta, {:meta, account_id}) do - [{{:meta, _}, meta}] -> - Map.merge(@default_user_meta, meta) - _ -> - @default_user_meta - end - end - # Calcul g/l actuel: - # 1. load user meta - # 2. foldr ets - # for each object - # get_current_alcohol - # ((object g/l) - 0,15/l/60)* minutes_since_drink - # if minutes_since_drink < 10, reduce g/l (?!) - # acc + current_alcohol - # stop folding when ? - # - - def user_stats(account) do - user_stats(data_state(), account.id) - end - - defp user_stats(state = %{ets: ets}, account_id) do - meta = get_user_meta(state, account_id) - aday = (10 * 60)*60 - now = DateTime.utc_now() - before = now - |> DateTime.add(-aday, :second) - |> DateTime.to_unix(:millisecond) - #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end) - match = [ - {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_}, - [ - {:>, :"$2", {:const, before}}, - {:"=:=", {:const, account_id}, :"$1"} - ], [:"$_"]} - ] - # tuple ets: {{nick, date}, volumes, current, nom, commentaire} - drinks = :ets.select(ets, match) - # {date, single_peak} - total_volume = Enum.reduce(drinks, 0.0, fn({{_, date}, volume, _, _, _, _, _, _}, acc) -> - acc + volume - end) - k = if meta.sex, do: 0.7, else: 0.6 - weight = meta.weight - gl = (10*total_volume)/(k*weight) - {Float.round(total_volume + 0.0, 4), Float.round(gl + 0.0, 4)} - end - - defp alcohol_level_rising(state, account_id, minutes \\ 30) do - {now, _} = current_alcohol_level(state, account_id) - soon_date = DateTime.utc_now - |> DateTime.add(minutes*60, :second) - {soon, _} = current_alcohol_level(state, account_id, soon_date) - soon = cond do - soon < 0 -> 0.0 - true -> soon - end - #IO.puts "soon #{soon_date} - #{inspect soon} #{inspect now}" - {soon > now, Float.round(soon+0.0, 4)} - end - - defp current_alcohol_level(state = %{ets: ets}, account_id, now \\ nil) do - meta = get_user_meta(state, account_id) - aday = ((24*7) * 60)*60 - now = if now do - now - else - DateTime.utc_now() - end - before = now - |> DateTime.add(-aday, :second) - |> DateTime.to_unix(:millisecond) - #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end) - match = [ - {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_}, - [ - {:>, :"$2", {:const, before}}, - {:"=:=", {:const, account_id}, :"$1"} - ], [:"$_"]} - ] - # tuple ets: {{nick, date}, volumes, current, nom, commentaire} - drinks = :ets.select(ets, match) - |> Enum.sort_by(fn({{_, date}, _, _, _, _, _, _, _}) -> date end, &</2) - # {date, single_peak} - {all, last_drink_at, gl, active_drinks} = Enum.reduce(drinks, {0.0, nil, [], 0}, fn({{_, date}, volume, _, _, _, _, _, _}, {all, last_at, acc, active_drinks}) -> - k = if meta.sex, do: 0.7, else: 0.6 - weight = meta.weight - peak = (10*volume)/(k*weight) - date = case date do - ts when is_integer(ts) -> DateTime.from_unix!(ts, :millisecond) - date = %NaiveDateTime{} -> DateTime.from_naive!(date, "Etc/UTC") - date = %DateTime{} -> date - end - last_at = last_at || date - mins_since = round(DateTime.diff(now, date)/60.0) - #IO.puts "Drink: #{inspect({date, volume})} - mins since: #{inspect mins_since} - last drink at #{inspect last_at}" - # Apply loss since `last_at` on `all` - # - all = if last_at do - mins_since_last = round(DateTime.diff(date, last_at)/60.0) - loss = ((meta.loss_factor/100)/60)*(mins_since_last) - #IO.puts "Applying last drink loss: from #{all}, loss of #{inspect loss} (mins since #{inspect mins_since_first})" - cond do - (all-loss) > 0 -> all - loss - true -> 0.0 - end - else - all - end - #IO.puts "Applying last drink current before drink: #{inspect all}" - if mins_since < 30 do - per_min = (peak)/30.0 - current = (per_min*mins_since) - #IO.puts "Applying current drink 30m: from #{peak}, loss of #{inspect per_min}/min (mins since #{inspect mins_since})" - {all + current, date, [{date, current} | acc], active_drinks + 1} - else - {all + peak, date, [{date, peak} | acc], active_drinks} - end - end) - #IO.puts "last drink #{inspect last_drink_at}" - mins_since_last = if last_drink_at do - round(DateTime.diff(now, last_drink_at)/60.0) - else - 0 - end - # Si on a déjà bu y'a déjà moins 15 minutes (big up le binge drinking), on applique plus de perte - level = if mins_since_last > 15 do - loss = ((meta.loss_factor/100)/60)*(mins_since_last) - Float.round(all - loss, 4) - else - all - end - #IO.puts "\n LEVEL #{inspect level}\n\n\n\n" - cond do - level < 0 -> {0.0, 0} - true -> {level, active_drinks} - end - end - - defp format_duration_from_now(date, with_detail \\ true) do - date = if is_integer(date) do - date = DateTime.from_unix!(date, :millisecond) - |> Timex.Timezone.convert("Europe/Paris") - else - Util.to_naive_date_time(date) - end - now = DateTime.utc_now() - |> Timex.Timezone.convert("Europe/Paris") - {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(date, "({h24}:{m})", "fr") - - mins_since = round(DateTime.diff(now, date)/60.0) - if ago = format_minute_duration(mins_since) do - word = if mins_since > 0 do - "il y a " - else - "dans " - end - word <> ago <> if(with_detail, do: " #{detail}", else: "") - else - "maintenant #{detail}" - end - end - - defp format_minute_duration(minutes) do - sober_in_s = if (minutes != 0) do - duration = Timex.Duration.from_minutes(minutes) - Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized) - else - nil - end - end - -end diff --git a/lib/lsg_irc/alcoolog_announcer_plugin.ex b/lib/lsg_irc/alcoolog_announcer_plugin.ex deleted file mode 100644 index f90dc42..0000000 --- a/lib/lsg_irc/alcoolog_announcer_plugin.ex +++ /dev/null @@ -1,272 +0,0 @@ -defmodule Nola.IRC.AlcoologAnnouncerPlugin do - require Logger - - @moduledoc """ - Annonce changements d'alcoolog - """ - - @channel "#dmz" - - @seconds 30 - - @apero [ - "C'EST L'HEURE DE L'APÉRRROOOOOOOO !!", - "SAAAAANNNNNNNTTTTTTTTAAAAAAAAIIIIIIIIIIIIIIIIIIIIII", - "APÉRO ? APÉRO !", - {:timed, ["/!\\ à vos tires bouchons…", "… débouchez …", "… BUVEZ! SANTAIII!"]}, - "/!\\ ALERTE APÉRO /!\\", - "CED !!! VASE DE ROUGE !", - "DIDI UN PETIT RICARD™??!", - "ALLEZ GUIGUI UNE PETITE BIERE ?", - {:timed, ["/!\\ à vos tires bouchons…", "… débouchez …", "… BUVEZ! SANTAIII!"]}, - "APPPPAIIIRRREAAUUUUUUUUUUU" - ] - - def irc_doc, do: nil - - def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) - - def log(account) do - dets_filename = (Nola.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist - {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}]) - from = ~U[2020-08-23 19:41:40.524154Z] - to = ~U[2020-08-24 19:41:40.524154Z] - select = [ - {{:"$1", :"$2", :_}, - [ - {:andalso, - {:andalso, {:==, :"$1", {:const, account.id}}, - {:>, :"$2", {:const, DateTime.to_unix(from)}}}, - {:<, :"$2", {:const, DateTime.to_unix(to)}}} - ], [:"$_"]} - ] - res = :dets.select(dets, select) - :dets.close(dets) - res - end - - def init(_) do - {:ok, _} = Registry.register(IRC.PubSub, "account", []) - stats = get_stats() - Process.send_after(self(), :stats, :timer.seconds(30)) - dets_filename = (Nola.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist - {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}]) - ets = nil # :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}]) - #:ok = Nola.IRC.SettingPlugin.declare("alcoolog.alerts", __MODULE__, true, :boolean) - #:ok = Nola.IRC.SettingPlugin.declare("alcoolog.aperoalert", __MODULE__, true, :boolean) - # - {:ok, {stats, now(), dets, ets}}#, {:continue, :traverse}} - end - - def handle_continue(:traverse, state = {_, _, dets, ets}) do - traverse_fun = fn(obj, dets) -> - case obj do - {nick, %DateTime{} = dt, active} -> - :dets.delete_object(dets, obj) - :dets.insert(dets, {nick, DateTime.to_unix(dt), active}) - IO.puts("ok #{inspect obj}") - dets - {nick, ts, value} -> - :ets.insert(ets, { {nick, ts}, value }) - dets - end - end - :dets.foldl(traverse_fun, dets, dets) - :dets.sync(dets) - IO.puts("alcoolog announcer fixed") - {:noreply, state} - end - - def alcohol_reached(old, new, level) do - (old.active < level && new.active >= level) && (new.active5m >= level) - end - - def alcohol_below(old, new, level) do - (old.active > level && new.active <= level) && (new.active5m <= level) - end - - - def handle_info(:stats, {old_stats, old_now, dets, ets}) do - stats = get_stats() - now = now() - - if old_now.hour < 18 && now.hour == 18 do - apero = Enum.shuffle(@apero) - |> Enum.random() - - case apero do - {:timed, list} -> - spawn(fn() -> - for line <- list do - IRC.Connection.broadcast_message("evolu.net", "#dmz", line) - :timer.sleep(:timer.seconds(5)) - end - end) - string -> - IRC.Connection.broadcast_message("evolu.net", "#dmz", string) - end - - end - - #IO.puts "newstats #{inspect stats}" - events = for {acct, old} <- old_stats do - new = Map.get(stats, acct, nil) - #IO.puts "#{acct}: #{inspect(old)} -> #{inspect(new)}" - - now = DateTime.to_unix(DateTime.utc_now()) - if new && new[:active] do - :dets.insert(dets, {acct, now, new[:active]}) - :ets.insert(ets, {{acct, now}, new[:active]}) - else - :dets.insert(dets, {acct, now, 0.0}) - :ets.insert(ets, {{acct, now}, new[:active]}) - end - - event = cond do - old == nil -> nil - (old.active > 0) && (new == nil) -> :sober - new == nil -> nil - alcohol_reached(old, new, 0.5) -> :stopconduire - alcohol_reached(old, new, 1.0) -> :g1 - alcohol_reached(old, new, 2.0) -> :g2 - alcohol_reached(old, new, 3.0) -> :g3 - alcohol_reached(old, new, 4.0) -> :g4 - alcohol_reached(old, new, 5.0) -> :g5 - alcohol_reached(old, new, 6.0) -> :g6 - alcohol_reached(old, new, 7.0) -> :g7 - alcohol_reached(old, new, 10.0) -> :g10 - alcohol_reached(old, new, 13.74) -> :record - alcohol_below(old, new, 0.5) -> :conduire - alcohol_below(old, new, 1.0) -> :fini1g - alcohol_below(old, new, 2.0) -> :fini2g - alcohol_below(old, new, 3.0) -> :fini3g - alcohol_below(old, new, 4.0) -> :fini4g - (old.rising) && (!new.rising) -> :lowering - true -> nil - end - {acct, event} - end - - for {acct, event} <- events do - message = case event do - :g1 -> [ - "[vigicuite jaune] LE GRAMME! LE GRAMME O/", - "début de vigicuite jaune ! LE GRAMME ! \\O/", - "waiiiiiiii le grammmeee", - "bourraiiiiiiiiiiide 1 grammeeeeeeeeeee", - ] - :g2 -> [ - "[vigicuite orange] \\o_YAY 2 GRAMMES ! _o/", - "PAITAIIIIIIIIII DEUX GRAMMEESSSSSSSSSSSSSSSSS", - "bourrrrrraiiiiiiiiiiiiiiiide 2 grammeeeeeeeeeees", - ] - :g3 -> [ - "et un ! et deux ! et TROIS GRAMMEEESSSSSSS", - "[vigicuite rouge] _o/ BOURRAIIDDDEEEE 3 GRAMMESSSSSSSSS \\o/ \\o/" - ] - :g4 -> [ - "[vigicuite écarlate] et un, et deux, et trois, ET QUATRES GRAMMEESSSSSSSSSSSSSSSSSSSssssss" - ] - :g5 -> "[vigicuite écarlate+] PUTAIN 5 GRAMMES !" - :g6 -> "[vigicuite écarlate++] 6 grammes ? Vous pouvez joindre Alcool info service au 0 980 980 930" - :g7 -> "[vigicuite c'est la merde] 7 grammes. Le SAMU, c'est le 15." - :g10 -> "BORDLE 10 GRAMMES" - :record -> "RECORD DU MONDE BATTU ! >13.74g/l !!" - :fini1g -> [ - "fin d'alerte vigicuite jaune, passage en vert (<1g/l)", - "/!\\ alerte moins de 1g/l /!\\" - ] - :fini2g -> [ - "t'as moins de 2 g/l, faut se reprendre là [vigicuite jaune]" - ] - :fini3g -> [ - "fin d'alerte vigicuite rouge, passage en orange (<3g/l)" - ] - :fini4g -> [ - "fin d'alerte vigicuite écarlate, passage en rouge (<4g/l)" - ] - :lowering -> [ - "attention ça baisse!", - "tu vas quand même pas en rester là ?", - "IL FAUT CONTINUER À BOIRE !", - "t'abandonnes déjà ?", - "!santai ?", - "faut pas en rester là", - "il faut se resservir", - "coucou faut reboire", - "encore un petit verre ?", - "abwaaaaaaaaaaaaarrrrrrrrrrrrrr", - "taux d'alcoolémie en chute ! agissez avant qu'il soit trop tard!", - "ÇA BAISSE !!" - ] - :stopconduire -> [ - "0.5g! bientot le gramme?", - "tu peux plus prendre la route... mais... tu peux prendre la route du gramme! !santai !", - "fini la conduite!", - "0.5! continues faut pas en rester là!", - "beau début, continues !", - "ça monte! 0.5g/l!" - ] - :conduire -> [ - "tu peux conduire, ou recommencer à boire! niveau critique!", - "!santai ?", - "tu peux reprendre la route, ou reprendre la route du gramme..", - "attention, niveau critique!", - "il faut boire !!", - "trop de sang dans ton alcool, c'est mauvais pour la santé", - "faut pas en rester là !", - ] - :sober -> [ - "sobre…", - "/!\\ alerte sobriété /!\\", - "... sobre?!?!", - "sobre :(", - "attention, t'es sobre :/", - "danger, alcoolémie à 0.0 !", - "sobre! c'était bien on recommence quand ?", - "sobre ? Faut recommencer...", - "T'es sobre. Ne te laisses pas abattre- ton caviste peut aider.", - "Vous êtes sobre ? Ceci n'est pas une fatalité - resservez vous vite !" - ] - _ -> nil - end - message = case message do - m when is_binary(m) -> m - m when is_list(m) -> m |> Enum.shuffle() |> Enum.random() - nil -> nil - end - if message do - #IO.puts("#{acct}: #{message}") - account = IRC.Account.get(acct) - for {net, chan} <- IRC.Membership.notify_channels(account) do - user = IRC.UserTrack.find_by_account(net, account) - nick = if(user, do: user.nick, else: account.name) - IRC.Connection.broadcast_message(net, chan, "#{nick}: #{message}") - end - end - end - - timer() - - #IO.puts "tick stats ok" - {:noreply, {stats,now,dets,ets}} - end - - def handle_info(_, state) do - {:noreply, state} - end - - defp now() do - DateTime.utc_now() - |> Timex.Timezone.convert("Europe/Paris") - end - - defp get_stats() do - Enum.into(Nola.IRC.AlcoologPlugin.get_all_stats(), %{}) - end - - defp timer() do - Process.send_after(self(), :stats, :timer.seconds(@seconds)) - end - -end diff --git a/lib/lsg_irc/base_plugin.ex b/lib/lsg_irc/base_plugin.ex deleted file mode 100644 index a2b9ffb..0000000 --- a/lib/lsg_irc/base_plugin.ex +++ /dev/null @@ -1,131 +0,0 @@ -defmodule Nola.IRC.BasePlugin do - - def irc_doc, do: nil - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init([]) do - regopts = [plugin: __MODULE__] - {:ok, _} = Registry.register(IRC.PubSub, "trigger:version", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "trigger:help", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "trigger:liquidrender", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "trigger:plugin", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "trigger:plugins", regopts) - {:ok, nil} - end - - def handle_info({:irc, :trigger, "plugins", msg = %{trigger: %{type: :bang, args: []}}}, _) do - enabled_string = IRC.Plugin.enabled() - |> Enum.map(fn(mod) -> - mod - |> Macro.underscore() - |> String.split("/", parts: :infinity) - |> List.last() - |> String.replace("_plugin", "") - |> Enum.sort() - end) - |> Enum.join(", ") - msg.replyfun.("Enabled plugins: #{enabled_string}") - {:noreply, nil} - end - - def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :query, args: [plugin]}} = m}, _) do - module = Module.concat([Nola.IRC, Macro.camelize(plugin<>"_plugin")]) - with true <- Code.ensure_loaded?(module), - pid when is_pid(pid) <- GenServer.whereis(module) - do - m.replyfun.("loaded, active: #{inspect(pid)}") - else - false -> m.replyfun.("not loaded") - nil -> - msg = case IRC.Plugin.get(module) do - :disabled -> "disabled" - {_, false, _} -> "disabled" - _ -> "not active" - end - m.replyfun.(msg) - end - {:noreply, nil} - end - - def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :plus, args: [plugin]}} = m}, _) do - module = Module.concat([Nola.IRC, Macro.camelize(plugin<>"_plugin")]) - with true <- Code.ensure_loaded?(module), - IRC.Plugin.switch(module, true), - {:ok, pid} <- IRC.Plugin.start(module) - do - m.replyfun.("started: #{inspect(pid)}") - else - false -> m.replyfun.("not loaded") - :ignore -> m.replyfun.("disabled or throttled") - {:error, _} -> m.replyfun.("start error") - end - {:noreply, nil} - end - - def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :tilde, args: [plugin]}} = m}, _) do - module = Module.concat([Nola.IRC, Macro.camelize(plugin<>"_plugin")]) - with true <- Code.ensure_loaded?(module), - pid when is_pid(pid) <- GenServer.whereis(module), - :ok <- GenServer.stop(pid), - {:ok, pid} <- IRC.Plugin.start(module) - do - m.replyfun.("restarted: #{inspect(pid)}") - else - false -> m.replyfun.("not loaded") - nil -> m.replyfun.("not active") - end - {:noreply, nil} - end - - - def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :minus, args: [plugin]}} = m}, _) do - module = Module.concat([Nola.IRC, Macro.camelize(plugin<>"_plugin")]) - with true <- Code.ensure_loaded?(module), - pid when is_pid(pid) <- GenServer.whereis(module), - :ok <- GenServer.stop(pid) - do - IRC.Plugin.switch(module, false) - m.replyfun.("stopped: #{inspect(pid)}") - else - false -> m.replyfun.("not loaded") - nil -> m.replyfun.("not active") - end - {:noreply, nil} - end - - def handle_info({:irc, :trigger, "liquidrender", m = %{trigger: %{args: args}}}, _) do - template = Enum.join(args, " ") - m.replyfun.(Tmpl.render(template, m)) - {:noreply, nil} - end - - def handle_info({:irc, :trigger, "help", m = %{trigger: %{type: :bang}}}, _) do - url = NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel)) - m.replyfun.("-> #{url}") - {:noreply, nil} - end - - def handle_info({:irc, :trigger, "version", message = %{trigger: %{type: :bang}}}, _) do - {:ok, vsn} = :application.get_key(:nola, :vsn) - ver = List.to_string(vsn) - url = NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :index) - elixir_ver = Application.started_applications() |> List.keyfind(:elixir, 0) |> elem(2) |> to_string() - otp_ver = :erlang.system_info(:system_version) |> to_string() |> String.trim() - system = :erlang.system_info(:system_architecture) |> to_string() - message.replyfun.([ - <<"🤖 I am a robot running", 2, "beautte, version #{ver}", 2, " — source: #{Nola.source_url()}">>, - "🦾 Elixir #{elixir_ver} #{otp_ver} on #{system}", - "👷♀️ Owner: href <href@random.sh>", - "🌍 Web interface: #{url}" - ]) - {:noreply, nil} - end - - def handle_info(msg, _) do - {:noreply, nil} - end - -end diff --git a/lib/lsg_irc/bourosama_plugin.ex b/lib/lsg_irc/bourosama_plugin.ex deleted file mode 100644 index dd05144..0000000 --- a/lib/lsg_irc/bourosama_plugin.ex +++ /dev/null @@ -1,58 +0,0 @@ -defmodule Nola.IRC.BoursoramaPlugin do - - def irc_doc() do - """ - # bourses - - Un peu comme [finance](#finance), mais en un peu mieux, et un peu moins bien. - - Source: [boursorama.com](https://boursorama.com) - - * **!caca40** affiche l'état du cac40 - """ - end - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - @cac40_url "https://www.boursorama.com/bourse/actions/palmares/france/?france_filter%5Bmarket%5D=1rPCAC&france_filter%5Bsector%5D=&france_filter%5Bvariation%5D=50002&france_filter%5Bperiod%5D=1&france_filter%5Bfilter%5D=" - - def init(_) do - regopts = [plugin: __MODULE__] - {:ok, _} = Registry.register(IRC.PubSub, "trigger:cac40", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "trigger:caca40", regopts) - {:ok, nil} - end - - def handle_info({:irc, :trigger, cac, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) when cac in ["cac40", "caca40"] do - case HTTPoison.get(@cac40_url, [], []) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - html = Floki.parse(body) - board = Floki.find(body, "div.c-tradingboard") - - cac40 = Floki.find(board, ".c-tradingboard__main > .c-tradingboard__infos") - instrument = Floki.find(cac40, ".c-instrument") - last = Floki.find(instrument, "span[data-ist-last]") - |> Floki.text() - |> String.replace(" ", "") - variation = Floki.find(instrument, "span[data-ist-variation]") - |> Floki.text() - - sign = case variation do - "-"<>_ -> "▼" - "+" -> "▲" - _ -> "" - end - - m.replyfun.("caca40: #{sign} #{variation} #{last}") - - {:error, %HTTPoison.Response{status_code: code}} -> - m.replyfun.("caca40: erreur http #{code}") - - _ -> - m.replyfun.("caca40: erreur http") - end - end - -end diff --git a/lib/lsg_irc/buffer_plugin.ex b/lib/lsg_irc/buffer_plugin.ex deleted file mode 100644 index eece34e..0000000 --- a/lib/lsg_irc/buffer_plugin.ex +++ /dev/null @@ -1,44 +0,0 @@ -defmodule Nola.IRC.BufferPlugin do - @table __MODULE__.ETS - def irc_doc, do: nil - - def table(), do: @table - - def select_buffer(network, channel, limit \\ 50) do - import Ex2ms - spec = fun do {{n, c, _}, m} when n == ^network and (c == ^channel or is_nil(c)) -> m end - :ets.select(@table, spec, limit) - end - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init(_) do - for e <- ~w(messages triggers events outputs) do - {:ok, _} = Registry.register(IRC.PubSub, e, plugin: __MODULE__) - end - {:ok, :ets.new(@table, [:named_table, :ordered_set, :protected])} - end - - def handle_info({:irc, :trigger, _, message}, ets), do: handle_message(message, ets) - def handle_info({:irc, :text, message}, ets), do: handle_message(message, ets) - def handle_info({:irc, :event, event}, ets), do: handle_message(event, ets) - - defp handle_message(message = %{network: network}, ets) do - key = {network, Map.get(message, :channel), ts(message.at)} - :ets.insert(ets, {key, message}) - {:noreply, ets} - end - - defp ts(nil), do: ts(NaiveDateTime.utc_now()) - - defp ts(naive = %NaiveDateTime{}) do - ts = naive - |> DateTime.from_naive!("Etc/UTC") - |> DateTime.to_unix() - - -ts - end - -end diff --git a/lib/lsg_irc/calc_plugin.ex b/lib/lsg_irc/calc_plugin.ex deleted file mode 100644 index 264370c..0000000 --- a/lib/lsg_irc/calc_plugin.ex +++ /dev/null @@ -1,37 +0,0 @@ -defmodule Nola.IRC.CalcPlugin do - @moduledoc """ - # calc - - * **!calc `<expression>`**: évalue l'expression mathématique `<expression>`. - """ - - def irc_doc, do: @moduledoc - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init(_) do - {:ok, _} = Registry.register(IRC.PubSub, "trigger:calc", [plugin: __MODULE__]) - {:ok, nil} - end - - def handle_info({:irc, :trigger, "calc", message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: expr_list}}}, state) do - expr = Enum.join(expr_list, " ") - result = try do - case Abacus.eval(expr) do - {:ok, result} -> result - error -> inspect(error) - end - rescue - error -> if(error[:message], do: "#{error.message}", else: "erreur") - end - message.replyfun.("#{message.sender.nick}: #{expr} = #{result}") - {:noreply, state} - end - - def handle_info(msg, state) do - {:noreply, state} - end - -end diff --git a/lib/lsg_irc/coronavirus_plugin.ex b/lib/lsg_irc/coronavirus_plugin.ex deleted file mode 100644 index d04d8f9..0000000 --- a/lib/lsg_irc/coronavirus_plugin.ex +++ /dev/null @@ -1,172 +0,0 @@ -defmodule Nola.IRC.CoronavirusPlugin do - require Logger - NimbleCSV.define(CovidCsv, separator: ",", escape: "\"") - @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__, [], name: __MODULE__) - end - - def init(_) do - {:ok, _} = Registry.register(IRC.PubSub, "trigger:coronavirus", [plugin: __MODULE__]) - {:ok, nil, {:continue, :init}} - :ignore - end - - def handle_continue(:init, _) do - date = Date.add(Date.utc_today(), -2) - {data, _} = fetch_data(%{}, date) - {data, next} = fetch_data(data) - :timer.send_after(next, :update) - {:noreply, %{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"], ["n"], ["nmorts"], ["nsoignés"], ["nconfirmés"]] do - {field, name} = case args do - ["confirmés"] -> {:confirmed, "confirmés"} - ["morts"] -> {:deaths, "morts"} - ["soignés"] -> {:recovered, "soignés"} - ["nmorts"] -> {:new_deaths, "nouveaux morts"} - ["nconfirmés"] -> {:new_confirmed, "nouveaux confirmés"} - ["n"] -> {:new_current, "nouveaux malades"} - ["nsoignés"] -> {:new_recovered, "nouveaux soignés"} - _ -> {:current, "malades"} - end - IO.puts("FIELD #{inspect field}") - field_evol = String.to_atom("new_#{field}") - sorted = state.data - |> Enum.filter(fn({_, %{region: region}}) -> region == true end) - |> Enum.map(fn({location, data}) -> {location, Map.get(data, field, 0), Map.get(data, field_evol, 0)} end) - |> Enum.sort_by(fn({_,count,_}) -> count end, &>=/2) - |> Enum.take(10) - |> Enum.with_index() - |> Enum.map(fn({{location, count, evol}, index}) -> - ev = if String.starts_with?(name, "nouveaux") do - "" - else - " (#{Util.plusminus(evol)})" - end - "##{index+1}: #{location} #{count}#{ev}" - end) - |> Enum.intersperse(" - ") - |> Enum.join() - m.replyfun.("CORONAVIRUS TOP10 #{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.("coronavirus: #{location}: " - <> "#{data.current} malades (#{Util.plusminus(data.new_current)}), " - <> "#{data.confirmed} confirmés (#{Util.plusminus(data.new_confirmed)}), " - <> "#{data.deaths} morts (#{Util.plusminus(data.new_deaths)}), " - <> "#{data.recovered} soignés (#{Util.plusminus(data.new_recovered)}) (@ #{data.update})") - 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 - |> CovidCsv.parse_string() - |> Enum.reduce(%{}, fn(line, acc) -> - case line do - # FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Deaths,Recovered,Active,Combined_Key - #0FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Deaths,Recovered,Active,Combined_Key,Incidence_Rate,Case-Fatality_Ratio - [_, _, state, region, update, _lat, _lng, confirmed, deaths, recovered, _active, _combined_key, _incidence_rate, _fatality_ratio] -> - 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} - - 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 - } - - changes = if old = Map.get(current_data, region) do - %{ - new_confirmed: region_entry.confirmed - old.confirmed, - new_current: region_entry.current - old.current, - new_deaths: region_entry.deaths - old.deaths, - new_recovered: region_entry.recovered - old.recovered, - } - else - %{new_confirmed: 0, new_current: 0, new_deaths: 0, new_recovered: 0} - end - - region_entry = Map.merge(region_entry, changes) - - acc = Map.put(acc, region, region_entry) - - acc = if state && state != "" do - Map.put(acc, state, entry) - else - acc - end - - other -> - Logger.info("Coronavirus line failed: #{inspect line}") - acc - end - 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/correction_plugin.ex b/lib/lsg_irc/correction_plugin.ex deleted file mode 100644 index 5f9b278..0000000 --- a/lib/lsg_irc/correction_plugin.ex +++ /dev/null @@ -1,59 +0,0 @@ -defmodule Nola.IRC.CorrectionPlugin do - @moduledoc """ - # correction - - * `s/pattern/replace` replace `pattern` by `replace` in the last matching message - """ - - def irc_doc, do: @moduledoc - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init(_) do - {:ok, _} = Registry.register(IRC.PubSub, "messages", [plugin: __MODULE__]) - {:ok, _} = Registry.register(IRC.PubSub, "triggers", [plugin: __MODULE__]) - {:ok, %{}} - end - - # Trigger fallback - def handle_info({:irc, :trigger, _, m = %IRC.Message{}}, state) do - {:noreply, correction(m, state)} - end - - def handle_info({:irc, :text, m = %IRC.Message{}}, state) do - {:noreply, correction(m, state)} - end - - def correction(m, state) do - history = Map.get(state, key(m), []) - if String.starts_with?(m.text, "s/") do - case String.split(m.text, "/") do - ["s", match, replace | _] -> - case Regex.compile(match) do - {:ok, reg} -> - repl = Enum.find(history, fn(m) -> Regex.match?(reg, m.text) end) - if repl do - new_text = String.replace(repl.text, reg, replace) - m.replyfun.("correction: <#{repl.sender.nick}> #{new_text}") - end - _ -> - m.replyfun.("correction: invalid regex") - end - _ -> m.replyfun.("correction: invalid regex format") - end - state - else - history = if length(history) > 100 do - {_, history} = List.pop_at(history, 99) - [m | history] - else - [m | history] - end - Map.put(state, key(m), history) - end - end - - defp key(%{network: net, channel: chan}), do: "#{net}/#{chan}" - -end diff --git a/lib/lsg_irc/dice_plugin.ex b/lib/lsg_irc/dice_plugin.ex deleted file mode 100644 index b5e7649..0000000 --- a/lib/lsg_irc/dice_plugin.ex +++ /dev/null @@ -1,66 +0,0 @@ -defmodule Nola.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__, [], name: __MODULE__) - end - - def init([]) do - {:ok, _} = Registry.register(IRC.PubSub, "trigger:dice", [plugin: __MODULE__]) - {: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 deleted file mode 100644 index 16d06ee..0000000 --- a/lib/lsg_irc/finance_plugin.ex +++ /dev/null @@ -1,190 +0,0 @@ -defmodule Nola.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 "http://www.alphavantage.co/physical_currency_list/" - @crypto_list "http://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__, [], name: __MODULE__) - end - - def init([]) do - regopts = [plugin: __MODULE__] - {:ok, _} = Registry.register(IRC.PubSub, "trigger:forex", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "trigger:currency", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "trigger:stocks", regopts) - {: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(:nola, :alphavantage, []) - |> Keyword.get(:api_key, "demo") - end - -end diff --git a/lib/lsg_irc/gpt_plugin.ex b/lib/lsg_irc/gpt_plugin.ex deleted file mode 100644 index 2c8f182..0000000 --- a/lib/lsg_irc/gpt_plugin.ex +++ /dev/null @@ -1,259 +0,0 @@ -defmodule Nola.IRC.GptPlugin do - require Logger - import Irc.Plugin.TempRef - - def irc_doc() do - """ - # OpenAI GPT - - Uses OpenAI's GPT-3 API to bring natural language prompts to your IRC channel. - - _prompts_ are pre-defined prompts and parameters defined in the bot' CouchDB. - - _Runs_ (results of the inference of a _prompt_) are also stored in CouchDB and - may be resumed. - - * **!gpt** list GPT prompts - * **!gpt `[prompt]` `<prompt or args>`** run a prompt - * **+gpt `[short ref|run id]` `<prompt or args>`** continue a prompt - * **?gpt offensive `<content>`** is content offensive ? - * **?gpt show `[short ref|run id]`** run information and web link - * **?gpt `[prompt]`** prompt information and web link - """ - end - - @couch_db "bot-plugin-openai-prompts" - @couch_run_db "bot-plugin-gpt-history" - @trigger "gpt" - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - defstruct [:temprefs] - - def get_result(id) do - Couch.get(@couch_run_db, id) - end - - def get_prompt(id) do - Couch.get(@couch_db, id) - end - - def init(_) do - regopts = [plugin: __MODULE__] - {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{@trigger}", regopts) - {:ok, %__MODULE__{temprefs: new_temp_refs()}} - end - - def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [prompt | args]}}}, state) do - case Couch.get(@couch_db, prompt) do - {:ok, prompt} -> {:noreply, prompt(m, prompt, Enum.join(args, " "), state)} - {:error, :not_found} -> - m.replyfun.("gpt: prompt '#{prompt}' does not exists") - {:noreply, state} - error -> - Logger.info("gpt: prompt load error: #{inspect error}") - m.replyfun.("gpt: database error") - {:noreply, state} - end - end - - def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do - case Couch.get(@couch_db, "_all_docs") do - {:ok, %{"rows" => []}} -> m.replyfun.("gpt: no prompts available") - {:ok, %{"rows" => prompts}} -> - prompts = prompts |> Enum.map(fn(prompt) -> Map.get(prompt, "id") end) |> Enum.join(", ") - m.replyfun.("gpt: prompts: #{prompts}") - error -> - Logger.info("gpt: prompt load error: #{inspect error}") - m.replyfun.("gpt: database error") - end - {:noreply, state} - end - - def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :plus, args: [ref_or_id | args]}}}, state) do - id = lookup_temp_ref(ref_or_id, state.temprefs, ref_or_id) - case Couch.get(@couch_run_db, id) do - {:ok, run} -> - Logger.debug("+gpt run: #{inspect run}") - {:noreply, continue_prompt(m, run, Enum.join(args, " "), state)} - {:error, :not_found} -> - m.replyfun.("gpt: ref or id not found or expired: #{inspect ref_or_id} (if using short ref, try using full id)") - {:noreply, state} - error -> - Logger.info("+gpt: run load error: #{inspect error}") - m.replyfun.("gpt: database error") - {:noreply, state} - end - end - - def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: ["offensive" | text]}}}, state) do - text = Enum.join(text, " ") - {moderate?, moderation} = moderation(text, m.account.id) - reply = cond do - moderate? -> "⚠️ #{Enum.join(moderation, ", ")}" - !moderate? && moderation -> "👍" - !moderate? -> "☠️ error" - end - m.replyfun.(reply) - {:noreply, state} - end - - def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: ["show", ref_or_id]}}}, state) do - id = lookup_temp_ref(ref_or_id, state.temprefs, ref_or_id) - url = if m.channel do - NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :result, m.network, NolaWeb.format_chan(m.channel), id) - else - NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :result, id) - end - m.replyfun.("→ #{url}") - {:noreply, state} - end - - def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: [prompt]}}}, state) do - url = if m.channel do - NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :prompt, m.network, NolaWeb.format_chan(m.channel), prompt) - else - NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :prompt, prompt) - end - m.replyfun.("→ #{url}") - {:noreply, state} - end - - def handle_info(info, state) do - Logger.debug("gpt: unhandled info: #{inspect info}") - {:noreply, state} - end - - defp continue_prompt(msg, run, content, state) do - prompt_id = Map.get(run, "prompt_id") - prompt_rev = Map.get(run, "prompt_rev") - - original_prompt = case Couch.get(@couch_db, prompt_id, rev: prompt_rev) do - {:ok, prompt} -> prompt - _ -> nil - end - - if original_prompt do - continue_prompt = %{"_id" => prompt_id, - "_rev" => prompt_rev, - "type" => Map.get(original_prompt, "type"), - "parent_run_id" => Map.get(run, "_id"), - "openai_params" => Map.get(run, "request") |> Map.delete("prompt")} - - continue_prompt = if prompt_string = Map.get(original_prompt, "continue_prompt") do - full_text = get_in(run, ~w(request prompt)) <> "\n" <> Map.get(run, "response") - continue_prompt - |> Map.put("prompt", prompt_string) - |> Map.put("prompt_format", "liquid") - |> Map.put("prompt_liquid_variables", %{"previous" => full_text}) - else - prompt_content_tag = if content != "", do: " {{content}}", else: "" - string = get_in(run, ~w(request prompt)) <> "\n" <> Map.get(run, "response") <> prompt_content_tag - continue_prompt - |> Map.put("prompt", string) - |> Map.put("prompt_format", "liquid") - end - - prompt(msg, continue_prompt, content, state) - else - msg.replyfun.("gpt: cannot continue this prompt: original prompt not found #{prompt_id}@v#{prompt_rev}") - state - end - end - - defp prompt(msg, prompt = %{"type" => "completions", "prompt" => prompt_template}, content, state) do - Logger.debug("gpt_plugin:prompt/4 #{inspect prompt}") - prompt_text = case Map.get(prompt, "prompt_format", "liquid") do - "liquid" -> Tmpl.render(prompt_template, msg, Map.merge(Map.get(prompt, "prompt_liquid_variables", %{}), %{"content" => content})) - "norender" -> prompt_template - end - - args = Map.get(prompt, "openai_params") - |> Map.put("prompt", prompt_text) - |> Map.put("user", msg.account.id) - - {moderate?, moderation} = moderation(content, msg.account.id) - if moderate?, do: msg.replyfun.("⚠️ offensive input: #{Enum.join(moderation, ", ")}") - - Logger.debug("GPT: request #{inspect args}") - case OpenAi.post("/v1/completions", args) do - {:ok, %{"choices" => [%{"text" => text, "finish_reason" => finish_reason} | _], "usage" => usage, "id" => gpt_id, "created" => created}} -> - text = String.trim(text) - {o_moderate?, o_moderation} = moderation(text, msg.account.id) - if o_moderate?, do: msg.replyfun.("🚨 offensive output: #{Enum.join(o_moderation, ", ")}") - msg.replyfun.(text) - doc = %{"id" => FlakeId.get(), - "prompt_id" => Map.get(prompt, "_id"), - "prompt_rev" => Map.get(prompt, "_rev"), - "network" => msg.network, - "channel" => msg.channel, - "nick" => msg.sender.nick, - "account_id" => (if msg.account, do: msg.account.id), - "request" => args, - "response" => text, - "message_at" => msg.at, - "reply_at" => DateTime.utc_now(), - "gpt_id" => gpt_id, - "gpt_at" => created, - "gpt_usage" => usage, - "type" => "completions", - "parent_run_id" => Map.get(prompt, "parent_run_id"), - "moderation" => %{"input" => %{flagged: moderate?, categories: moderation}, - "output" => %{flagged: o_moderate?, categories: o_moderation} - } - } - Logger.debug("Saving result to couch: #{inspect doc}") - {id, ref, temprefs} = case Couch.post(@couch_run_db, doc) do - {:ok, id, _rev} -> - {ref, temprefs} = put_temp_ref(id, state.temprefs) - {id, ref, temprefs} - error -> - Logger.error("Failed to save to Couch: #{inspect error}") - {nil, nil, state.temprefs} - end - stop = cond do - finish_reason == "stop" -> "" - finish_reason == "length" -> " — truncated" - true -> " — #{finish_reason}" - end - ref_and_prefix = if Map.get(usage, "completion_tokens", 0) == 0 do - "GPT had nothing else to say :( ↪ #{ref || "✗"}" - else - " ↪ #{ref || "✗"}" - end - msg.replyfun.(ref_and_prefix <> - stop <> - " — #{Map.get(usage, "total_tokens", 0)}" <> - " (#{Map.get(usage, "prompt_tokens", 0)}/#{Map.get(usage, "completion_tokens", 0)}) tokens" <> - " — #{id || "save failed"}") - %__MODULE__{state | temprefs: temprefs} - {:error, atom} when is_atom(atom) -> - Logger.error("gpt error: #{inspect atom}") - msg.replyfun.("gpt: ☠️ #{to_string(atom)}") - state - error -> - Logger.error("gpt error: #{inspect error}") - msg.replyfun.("gpt: ☠️ ") - state - end - end - - defp moderation(content, user_id) do - case OpenAi.post("/v1/moderations", %{"input" => content, "user" => user_id}) do - {:ok, %{"results" => [%{"flagged" => true, "categories" => categories} | _]}} -> - cat = categories - |> Enum.filter(fn({_key, value}) -> value end) - |> Enum.map(fn({key, _}) -> key end) - {true, cat} - {:ok, moderation} -> - Logger.debug("gpt: moderation: not flagged, #{inspect moderation}") - {false, true} - error -> - Logger.error("gpt: moderation error: #{inspect error}") - {false, false} - end - end - -end diff --git a/lib/lsg_irc/kick_roulette_plugin.ex b/lib/lsg_irc/kick_roulette_plugin.ex deleted file mode 100644 index 55b7da4..0000000 --- a/lib/lsg_irc/kick_roulette_plugin.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Nola.IRC.KickRoulettePlugin do - @moduledoc """ - # kick roulette - - * **!kick**, tentez votre chance… - """ - - def irc_doc, do: @moduledoc - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init([]) do - {:ok, _} = Registry.register(IRC.PubSub, "trigger:kick", [plugin: __MODULE__]) - {: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_plugin.ex b/lib/lsg_irc/last_fm_plugin.ex deleted file mode 100644 index 03df675..0000000 --- a/lib/lsg_irc/last_fm_plugin.ex +++ /dev/null @@ -1,187 +0,0 @@ -defmodule Nola.IRC.LastFmPlugin do - require Logger - - @moduledoc """ - # last.fm - - * **!lastfm|np `[nick|username]`** - * **.lastfm|np** - * **+lastfm, -lastfm `<username last.fm>; ?lastfm`** Configurer un nom d'utilisateur last.fm - """ - - @single_trigger ~w(lastfm np) - @pubsub_topics ~w(trigger:lastfm trigger:np) - - defstruct dets: nil - - def irc_doc, do: @moduledoc - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init([]) do - regopts = [type: __MODULE__] - for t <- @pubsub_topics, do: {:ok, _} = Registry.register(IRC.PubSub, t, type: __MODULE__) - dets_filename = (Nola.data_path() <> "/" <> "lastfm.dets") |> String.to_charlist - {:ok, dets} = :dets.open_file(dets_filename, []) - {:ok, %__MODULE__{dets: dets}} - end - - def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :plus, args: [username]}}}, state) do - username = String.strip(username) - :ok = :dets.insert(state.dets, {message.account.id, username}) - message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm configuré: \"#{username}\".") - {:noreply, state} - end - - def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :minus, args: []}}}, state) do - text = case :dets.lookup(state.dets, message.account.id) do - [{_nick, _username}] -> - :dets.delete(state.dets, message.account.id) - message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm enlevé.") - _ -> nil - end - {:noreply, state} - end - - def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :query, args: []}}}, state) do - text = case :dets.lookup(state.dets, message.account.id) do - [{_nick, username}] -> - message.replyfun.("#{message.sender.nick}: #{username}.") - _ -> nil - end - {:noreply, state} - end - - def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: []}}}, state) do - irc_now_playing(message.account.id, message, state) - {:noreply, state} - end - - def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: [nick_or_user]}}}, state) do - irc_now_playing(nick_or_user, message, state) - {:noreply, state} - end - - def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :dot}}}, state) do - members = IRC.Membership.members(message.network, message.channel) - foldfun = fn({nick, user}, acc) -> [{nick,user}|acc] end - usernames = :dets.foldl(foldfun, [], state.dets) - |> Enum.uniq() - |> Enum.filter(fn({acct,_}) -> Enum.member?(members, acct) end) - |> Enum.map(fn({_, u}) -> u end) - for u <- usernames, do: irc_now_playing(u, message, state) - {:noreply, state} - end - - def handle_info(info, state) do - {:noreply, state} - end - - def terminate(_reason, state) do - if state.dets do - :dets.sync(state.dets) - :dets.close(state.dets) - end - :ok - end - - defp irc_now_playing(nick_or_user, message, state) do - nick_or_user = String.strip(nick_or_user) - - id_or_user = if account = IRC.Account.get(nick_or_user) || IRC.Account.find_always_by_nick(message.network, message.channel, nick_or_user) do - account.id - else - nick_or_user - end - - username = case :dets.lookup(state.dets, id_or_user) do - [{_, username}] -> username - _ -> id_or_user - end - - case now_playing(username) do - {:error, text} when is_binary(text) -> - message.replyfun.(text) - {:ok, map} when is_map(map) -> - track = fetch_track(username, map) - text = format_now_playing(map, track) - user = if account = IRC.Account.get(id_or_user) do - user = IRC.UserTrack.find_by_account(message.network, account) - if(user, do: user.nick, else: account.name) - else - username - end - if user && text do - message.replyfun.("#{user} #{text}") - else - message.replyfun.("#{username}: pas de résultat") - end - other -> - message.replyfun.("erreur :(") - end - end - - defp now_playing(user) do - api = Application.get_env(:nola, :lastfm)[:api_key] - url = "http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&format=json&limit=1&extended=1" <> "&api_key=" <> api <> "&user="<> user - case HTTPoison.get(url) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> Jason.decode(body) - {:ok, %HTTPoison.Response{status_code: 404}} -> {:error, "last.fm: utilisateur \"#{user}\" inexistant"} - {:ok, %HTTPoison.Response{status_code: code}} -> {:error, "last.fm: erreur #{to_string(code)}"} - error -> - Logger.error "Lastfm http error: #{inspect error}" - :error - end - end - defp fetch_track(user, %{"recenttracks" => %{"track" => [ t = %{"name" => name, "artist" => %{"name" => artist}} | _]}}) do - api = Application.get_env(:nola, :lastfm)[:api_key] - url = "http://ws.audioscrobbler.com/2.0/?method=track.getInfo&format=json" <> "&api_key=" <> api <> "&username="<> user <> "&artist="<>URI.encode(artist)<>"&track="<>URI.encode(name) - case HTTPoison.get(url) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - case Jason.decode(body) do - {:ok, body} -> body["track"] || %{} - _ -> %{} - end - error -> - Logger.error "Lastfm http error: #{inspect error}" - :error - end - end - - defp format_now_playing(%{"recenttracks" => %{"track" => [track = %{"@attr" => %{"nowplaying" => "true"}} | _]}}, et) do - format_track(true, track, et) - end - - defp format_now_playing(%{"recenttracks" => %{"track" => [track | _]}}, et) do - format_track(false, track, et) - end - - defp format_now_playing(%{"error" => err, "message" => message}, _) do - "last.fm error #{err}: #{message}" - end - - defp format_now_playing(miss) do - nil - end - - defp format_track(np, track, extended) do - artist = track["artist"]["name"] - album = if track["album"]["#text"], do: " (" <> track["album"]["#text"] <> ")", else: "" - name = track["name"] <> album - action = if np, do: "écoute ", else: "a écouté" - love = if track["loved"] != "0", do: "❤️" - count = if x = extended["userplaycount"], do: "x#{x} #{love}" - tags = (get_in(extended, ["toptags", "tag"]) || []) - |> Enum.map(fn(tag) -> tag["name"] end) - |> Enum.filter(& &1) - |> Enum.join(", ") - - [action, artist, name, count, tags, track["url"]] - |> Enum.filter(& &1) - |> Enum.map(&String.trim(&1)) - |> Enum.join(" - ") - end - -end diff --git a/lib/lsg_irc/link_plugin.ex b/lib/lsg_irc/link_plugin.ex deleted file mode 100644 index dee78e8..0000000 --- a/lib/lsg_irc/link_plugin.ex +++ /dev/null @@ -1,271 +0,0 @@ -defmodule Nola.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 :nola, Nola.IRC.LinkPlugin, - handlers: [ - Nola.IRC.LinkPlugin.Youtube: [ - invidious: true - ], - Nola.IRC.LinkPlugin.Twitter: [], - Nola.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__, [], name: __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 - @callback post_match(uri :: URI.t, content_type :: binary, headers :: [], opts :: Keyword.t) :: {:body | :file, params :: Map.t} | false - @callback post_expand(uri :: URI.t, body :: binary() | Path.t, params :: Map.t, options :: Keyword.t) :: {:ok, lines :: [] | String.t} | :error - - @optional_callbacks [expand: 3, post_expand: 4] - - defstruct [:client] - - def init([]) do - {:ok, _} = Registry.register(IRC.PubSub, "messages", [plugin: __MODULE__]) - #{:ok, _} = Registry.register(IRC.PubSub, "messages:telegram", [plugin: __MODULE__]) - 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() -> - :timer.kill_after(:timer.seconds(30)) - case expand_link([uri]) do - {:ok, uris, text} -> - text = case uris do - [uri] -> text - [luri | _] -> - if luri.host == uri.host && luri.path == luri.path do - text - else - ["-> #{URI.to_string(luri)}", text] - end - end - if is_list(text) do - for line <- text, do: message.replyfun.(line) - else - message.replyfun.(text) - end - _ -> 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(acc = [_, _, _, _, _ | _]) do - {:ok, acc, "link redirects more than five times"} - end - - def expand_link(acc=[uri | _]) do - Logger.debug("link: expanding: #{inspect uri}") - handlers = Keyword.get(Application.get_env(:nola, __MODULE__, [handlers: []]), :handlers) - handler = Enum.reduce_while(handlers, nil, fn({module, opts}, acc) -> - Logger.debug("link: attempt expanding: #{inspect module} for #{inspect uri}") - 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 - Logger.debug("link: expanding #{inspect uri} with #{inspect module}") - case module.expand(uri, params, opts) do - {:ok, data} -> {:ok, acc, data} - :error -> expand_default(acc) - :skip -> nil - end - rescue - e -> - Logger.error("link: rescued #{inspect uri} with #{inspect module}: #{inspect e}") - Logger.error(Exception.format(:error, e, __STACKTRACE__)) - expand_default(acc) - catch - e, b -> - Logger.error("link: catched #{inspect uri} with #{inspect module}: #{inspect {e, b}}") - expand_default(acc) - end - - defp get(url, headers \\ [], options \\ []) do - get_req(url, :hackney.get(url, headers, <<>>, options)) - end - - defp get_req(_, {:error, reason}) do - {:error, reason} - end - - defp get_req(url, {:ok, 200, headers, client}) do - headers = Enum.reduce(headers, %{}, fn({key, value}, acc) -> - Map.put(acc, String.downcase(key), value) - end) - content_type = Map.get(headers, "content-type", "application/octect-stream") - length = Map.get(headers, "content-length", "0") - {length, _} = Integer.parse(length) - - handlers = Keyword.get(Application.get_env(:nola, __MODULE__, [handlers: []]), :handlers) - handler = Enum.reduce_while(handlers, false, fn({module, opts}, acc) -> - module = Module.concat([module]) - try do - case module.post_match(url, content_type, headers, opts) do - {mode, params} when mode in [:body, :file] -> {:halt, {module, params, opts, mode}} - false -> {:cont, acc} - end - rescue - e -> - Logger.error(inspect(e)) - {:cont, false} - catch - e, b -> - Logger.error(inspect({b})) - {:cont, false} - end - end) - - cond do - handler != false and length <= 30_000_000 -> - case get_body(url, 30_000_000, client, handler, <<>>) do - {:ok, _} = ok -> ok - :error -> - {:ok, "file: #{content_type}, size: #{human_size(length)}"} - end - #String.starts_with?(content_type, "text/html") && length <= 30_000_000 -> - # get_body(url, 30_000_000, client, <<>>) - true -> - :hackney.close(client) - {:ok, "file: #{content_type}, size: #{human_size(length)}"} - end - end - - defp get_req(_, {:ok, redirect, headers, client}) when redirect in 300..399 do - headers = Enum.reduce(headers, %{}, fn({key, value}, acc) -> - Map.put(acc, String.downcase(key), value) - end) - location = Map.get(headers, "location") - - :hackney.close(client) - {:redirect, location} - end - - defp get_req(_, {:ok, status, headers, client}) do - :hackney.close(client) - {:error, status, headers} - end - - defp get_body(url, len, client, {handler, params, opts, mode} = h, acc) when len >= byte_size(acc) do - case :hackney.stream_body(client) do - {:ok, data} -> - get_body(url, len, client, h, << acc::binary, data::binary >>) - :done -> - body = case mode do - :body -> acc - :file -> - {:ok, tmpfile} = Plug.Upload.random_file("linkplugin") - File.write!(tmpfile, acc) - tmpfile - end - handler.post_expand(url, body, params, opts) - {:error, reason} -> - {:ok, "failed to fetch body: #{inspect reason}"} - end - end - - defp get_body(_, len, client, h, _acc) do - :hackney.close(client) - IO.inspect(h) - {:ok, "Error: file over 30"} - end - - def expand_default(acc = [uri = %URI{scheme: scheme} | _]) when scheme in ["http", "https"] do - Logger.debug("link: expanding #{uri} with default") - headers = [{"user-agent", "DmzBot (like TwitterBot)"}] - options = [follow_redirect: false, max_body_length: 30_000_000] - case get(URI.to_string(uri), headers, options) do - {:ok, text} -> - {:ok, acc, text} - {:redirect, link} -> - new_uri = URI.parse(link) - #new_uri = %URI{new_uri | scheme: scheme, authority: uri.authority, host: uri.host, port: uri.port} - expand_link([new_uri | acc]) - {:error, status, _headers} -> - text = Plug.Conn.Status.reason_phrase(status) - {:ok, acc, "Error: HTTP #{text} (#{status})"} - {:error, {:tls_alert, {:handshake_failure, err}}} -> - {:ok, acc, "TLS Error: #{to_string(err)}"} - {:error, reason} -> - {:ok, acc, "Error: #{to_string(reason)}"} - end - end - - # Unsupported scheme, came from a redirect. - def expand_default(acc = [uri | _]) do - {:ok, [uri], "-> #{URI.to_string(uri)}"} - end - - - defp human_size(bytes) do - bytes - |> FileSize.new(:b) - |> FileSize.scale() - |> FileSize.format() - end -end diff --git a/lib/lsg_irc/link_plugin/github.ex b/lib/lsg_irc/link_plugin/github.ex deleted file mode 100644 index 93e0892..0000000 --- a/lib/lsg_irc/link_plugin/github.ex +++ /dev/null @@ -1,49 +0,0 @@ -defmodule Nola.IRC.LinkPlugin.Github do - @behaviour Nola.IRC.LinkPlugin - - @impl true - def match(uri = %URI{host: "github.com", path: path}, _) do - case String.split(path, "/") do - ["", user, repo] -> - {true, %{user: user, repo: repo, path: "#{user}/#{repo}"}} - _ -> - false - end - end - - def match(_, _), do: false - - @impl true - def post_match(_, _, _, _), do: false - - @impl true - def expand(_uri, %{user: user, repo: repo}, _opts) do - case HTTPoison.get("https://api.github.com/repos/#{user}/#{repo}") do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - {:ok, json} = Jason.decode(body) - src = json["source"]["full_name"] - disabled = if(json["disabled"], do: " (disabled)", else: "") - archived = if(json["archived"], do: " (archived)", else: "") - fork = if src && src != json["full_name"] do - " (⑂ #{json["source"]["full_name"]})" - else - "" - end - start = "#{json["full_name"]}#{disabled}#{archived}#{fork} - #{json["description"]}" - tags = for(t <- json["topics"]||[], do: "##{t}") |> Enum.intersperse(", ") |> Enum.join("") - lang = if(json["language"], do: "#{json["language"]} - ", else: "") - issues = if(json["open_issues_count"], do: "#{json["open_issues_count"]} issues - ", else: "") - last_push = if at = json["pushed_at"] do - {:ok, date, _} = DateTime.from_iso8601(at) - " - last pushed #{DateTime.to_string(date)}" - else - "" - end - network = "#{lang}#{issues}#{json["stargazers_count"]} stars - #{json["subscribers_count"]} watchers - #{json["forks_count"]} forks#{last_push}" - {:ok, [start, tags, network]} - other -> - :error - end - end - -end diff --git a/lib/lsg_irc/link_plugin/html.ex b/lib/lsg_irc/link_plugin/html.ex deleted file mode 100644 index 56a8ceb..0000000 --- a/lib/lsg_irc/link_plugin/html.ex +++ /dev/null @@ -1,106 +0,0 @@ -defmodule Nola.IRC.LinkPlugin.HTML do - @behaviour Nola.IRC.LinkPlugin - - @impl true - def match(_, _), do: false - - @impl true - def post_match(_url, "text/html"<>_, _header, _opts) do - {:body, nil} - end - def post_match(_, _, _, _), do: false - - @impl true - def post_expand(url, body, _params, _opts) do - html = Floki.parse(body) - title = collect_title(html) - opengraph = collect_open_graph(html) - itemprops = collect_itemprops(html) - text = if Map.has_key?(opengraph, "title") && Map.has_key?(opengraph, "description") do - sitename = if sn = Map.get(opengraph, "site_name") do - "#{sn}" - else - "" - end - paywall? = if Map.get(opengraph, "article:content_tier", Map.get(itemprops, "article:content_tier", "free")) == "free" do - "" - else - "[paywall] " - end - section = if section = Map.get(opengraph, "article:section", Map.get(itemprops, "article:section", nil)) do - ": #{section}" - else - "" - end - date = case DateTime.from_iso8601(Map.get(opengraph, "article:published_time", Map.get(itemprops, "article:published_time", ""))) do - {:ok, date, _} -> - "#{Timex.format!(date, "%d/%m/%y", :strftime)}. " - _ -> - "" - end - uri = URI.parse(url) - - prefix = "#{paywall?}#{Map.get(opengraph, "site_name", uri.host)}#{section}" - prefix = unless prefix == "" do - "#{prefix} — " - else - "" - end - [clean_text("#{prefix}#{Map.get(opengraph, "title")}")] ++ IRC.splitlong(clean_text("#{date}#{Map.get(opengraph, "description")}")) - else - clean_text(title) - end - {:ok, text} - end - - defp collect_title(html) do - case Floki.find(html, "title") do - [{"title", [], [title]} | _] -> - String.trim(title) - _ -> - nil - end - end - - defp collect_open_graph(html) do - Enum.reduce(Floki.find(html, "head meta"), %{}, fn(tag, acc) -> - case tag do - {"meta", values, []} -> - name = List.keyfind(values, "property", 0, {nil, nil}) |> elem(1) - content = List.keyfind(values, "content", 0, {nil, nil}) |> elem(1) - case name do - "og:" <> key -> - Map.put(acc, key, content) - "article:"<>_ -> - Map.put(acc, name, content) - _other -> acc - end - _other -> acc - end - end) - end - - defp collect_itemprops(html) do - Enum.reduce(Floki.find(html, "[itemprop]"), %{}, fn(tag, acc) -> - case tag do - {"meta", values, []} -> - name = List.keyfind(values, "itemprop", 0, {nil, nil}) |> elem(1) - content = List.keyfind(values, "content", 0, {nil, nil}) |> elem(1) - case name do - "article:" <> key -> - Map.put(acc, name, content) - _other -> acc - end - _other -> acc - end - end) - end - - defp clean_text(text) do - text - |> String.replace("\n", " ") - |> HtmlEntities.decode() - end - - -end diff --git a/lib/lsg_irc/link_plugin/imgur.ex b/lib/lsg_irc/link_plugin/imgur.ex deleted file mode 100644 index 5d74956..0000000 --- a/lib/lsg_irc/link_plugin/imgur.ex +++ /dev/null @@ -1,96 +0,0 @@ -defmodule Nola.IRC.LinkPlugin.Imgur do - @behaviour Nola.IRC.LinkPlugin - - @moduledoc """ - # Imgur link preview - - No options. - - Needs to have a Imgur API key configured: - - ``` - config :nola, :imgur, - client_id: "xxxxxxxx", - client_secret: "xxxxxxxxxxxxxxxxxxxx" - ``` - """ - - @impl true - def match(uri = %URI{host: "imgur.io"}, arg) do - match(%URI{uri | host: "imgur.com"}, arg) - end - def match(uri = %URI{host: "i.imgur.io"}, arg) do - match(%URI{uri | host: "i.imgur.com"}, arg) - end - 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 - - @impl true - def post_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(:nola, :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"] - title = String.slice(data["title"] || data["description"], 0, 180) - nsfw = if data["nsfw"], do: "(NSFW) - ", else: " " - height = Map.get(data, "height") - width = Map.get(data, "width") - size = Map.get(data, "size") - {:ok, "image, #{width}x#{height}, #{size} bytes #{nsfw}#{title}"} - other -> - :error - end - end - - def expand_imgur_album(album_id, opts) do - client_id = Keyword.get(Application.get_env(:nola, :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/pdf.ex b/lib/lsg_irc/link_plugin/pdf.ex deleted file mode 100644 index 5f72ef5..0000000 --- a/lib/lsg_irc/link_plugin/pdf.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Nola.IRC.LinkPlugin.PDF do - require Logger - @behaviour Nola.IRC.LinkPlugin - - @impl true - def match(_, _), do: false - - @impl true - def post_match(_url, "application/pdf"<>_, _header, _opts) do - {:file, nil} - end - - def post_match(_, _, _, _), do: false - - @impl true - def post_expand(url, file, _, _) do - case System.cmd("pdftitle", ["-p", file]) do - {text, 0} -> - text = text - |> String.trim() - - if text == "" do - :error - else - basename = Path.basename(url, ".pdf") - text = "[#{basename}] " <> text - |> String.split("\n") - {:ok, text} - end - {_, 127} -> - Logger.error("dependency `pdftitle` is missing, please install it: `pip3 install pdftitle`.") - :error - {error, code} -> - Logger.warn("command `pdftitle` exited with status code #{code}:\n#{inspect error}") - :error - end - end - -end diff --git a/lib/lsg_irc/link_plugin/redacted.ex b/lib/lsg_irc/link_plugin/redacted.ex deleted file mode 100644 index 7a6229d..0000000 --- a/lib/lsg_irc/link_plugin/redacted.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Nola.IRC.LinkPlugin.Redacted do - @behaviour Nola.IRC.LinkPlugin - - @impl true - def match(uri = %URI{host: "redacted.ch", path: "/torrent.php", query: query = "id="<>id}, _opts) do - %{"id" => id} = URI.decode_query(id) - {true, %{torrent: id}} - end - - def match(_, _), do: false - - @impl true - def post_match(_, _, _, _), do: false - - def expand(_uri, %{torrent: id}, _opts) do - end - -end diff --git a/lib/lsg_irc/link_plugin/reddit.ex b/lib/lsg_irc/link_plugin/reddit.ex deleted file mode 100644 index 79102e0..0000000 --- a/lib/lsg_irc/link_plugin/reddit.ex +++ /dev/null @@ -1,119 +0,0 @@ -defmodule Nola.IRC.LinkPlugin.Reddit do - @behaviour Nola.IRC.LinkPlugin - - @impl true - def match(uri = %URI{host: "reddit.com", path: path}, _) do - case String.split(path, "/") do - ["", "r", sub, "comments", post_id, _slug] -> - {true, %{mode: :post, path: path, sub: sub, post_id: post_id}} - ["", "r", sub, "comments", post_id, _slug, ""] -> - {true, %{mode: :post, path: path, sub: sub, post_id: post_id}} - ["", "r", sub, ""] -> - {true, %{mode: :sub, path: path, sub: sub}} - ["", "r", sub] -> - {true, %{mode: :sub, path: path, sub: sub}} -# ["", "u", user] -> -# {true, %{mode: :user, path: path, user: user}} - _ -> - false - end - end - - def match(uri = %URI{host: host, path: path}, opts) do - if String.ends_with?(host, ".reddit.com") do - match(%URI{uri | host: "reddit.com"}, opts) - else - false - end - end - - @impl true - def post_match(_, _, _, _), do: false - - @impl true - def expand(_, %{mode: :sub, sub: sub}, _opts) do - url = "https://api.reddit.com/r/#{sub}/about" - case HTTPoison.get(url) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - sr = Jason.decode!(body) - |> Map.get("data") - |> IO.inspect(limit: :infinity) - description = Map.get(sr, "public_description")||Map.get(sr, "description", "") - |> String.split("\n") - |> List.first() - name = if title = Map.get(sr, "title") do - Map.get(sr, "display_name_prefixed") <> ": " <> title - else - Map.get(sr, "display_name_prefixed") - end - nsfw = if Map.get(sr, "over18") do - "[NSFW] " - else - "" - end - quarantine = if Map.get(sr, "quarantine") do - "[Quarantined] " - else - "" - end - count = "#{Map.get(sr, "subscribers")} subscribers, #{Map.get(sr, "active_user_count")} active" - preview = "#{quarantine}#{nsfw}#{name} — #{description} (#{count})" - {:ok, preview} - _ -> - :error - end - end - - def expand(_uri, %{mode: :post, path: path, sub: sub, post_id: post_id}, _opts) do - case HTTPoison.get("https://api.reddit.com#{path}?sr_detail=true") do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - json = Jason.decode!(body) - op = List.first(json) - |> Map.get("data") - |> Map.get("children") - |> List.first() - |> Map.get("data") - |> IO.inspect(limit: :infinity) - sr = get_in(op, ["sr_detail", "display_name_prefixed"]) - {self?, url} = if Map.get(op, "selftext") == "" do - {false, Map.get(op, "url")} - else - {true, nil} - end - - self_str = if(self?, do: "text", else: url) - up = Map.get(op, "ups") - down = Map.get(op, "downs") - comments = Map.get(op, "num_comments") - nsfw = if Map.get(op, "over_18") do - "[NSFW] " - else - "" - end - state = cond do - Map.get(op, "hidden") -> "hidden" - Map.get(op, "archived") -> "archived" - Map.get(op, "locked") -> "locked" - Map.get(op, "quarantine") -> "quarantined" - Map.get(op, "removed_by") || Map.get(op, "removed_by_category") -> "removed" - Map.get(op, "banned_by") -> "banned" - Map.get(op, "pinned") -> "pinned" - Map.get(op, "stickied") -> "stickied" - true -> nil - end - flair = if flair = Map.get(op, "link_flair_text") do - "[#{flair}] " - else - "" - end - title = "#{nsfw}#{sr}: #{flair}#{Map.get(op, "title")}" - state_str = if(state, do: "#{state}, ") - content = "by u/#{Map.get(op, "author")} - #{state_str}#{up} up, #{down} down, #{comments} comments - #{self_str}" - - {:ok, [title, content]} - err -> - :error - end - end - -end diff --git a/lib/lsg_irc/link_plugin/twitter.ex b/lib/lsg_irc/link_plugin/twitter.ex deleted file mode 100644 index 640b193..0000000 --- a/lib/lsg_irc/link_plugin/twitter.ex +++ /dev/null @@ -1,158 +0,0 @@ -defmodule Nola.IRC.LinkPlugin.Twitter do - @behaviour Nola.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 - - @impl true - def post_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 link_tweet(tweet_or_screen_id_tuple, opts, force_twitter_com \\ false) - - defp link_tweet({screen_name, id}, opts, force_twitter_com) do - path = "/#{screen_name}/status/#{id}" - nitter = Keyword.get(opts, :nitter) - host = if !force_twitter_com && nitter, do: nitter, else: "twitter.com" - "https://#{host}/#{screen_name}/status/#{id}" - end - - defp link_tweet(tweet, opts, force_twitter_com) do - link_tweet({tweet.user.screen_name, tweet.id}, opts, force_twitter_com) - end - - defp expand_tweet(tweet, opts) do - head = format_tweet_header(tweet, opts) - - # Format tweet text - text = expand_twitter_text(tweet, opts) - text = if tweet.quoted_status do - quote_url = link_tweet(tweet.quoted_status, opts, true) - String.replace(text, quote_url, "") - else - text - end - text = IRC.splitlong(text) - - reply_to = if tweet.in_reply_to_status_id do - reply_url = link_tweet({tweet.in_reply_to_screen_name, tweet.in_reply_to_status_id}, opts) - text = if tweet.in_reply_to_screen_name == tweet.user.screen_name, do: "continued from", else: "replying to" - <<3, 15, " ↪ ", text::binary, " ", reply_url::binary, 3>> - end - - quoted = if tweet.quoted_status do - full_text = tweet.quoted_status - |> expand_twitter_text(opts) - |> IRC.splitlong_with_prefix(">") - - head = format_tweet_header(tweet.quoted_status, opts, details: false, prefix: "↓ quoting") - - [head | full_text] - else - [] - end - - #<<2, "#{tweet.user.name} (@#{tweet.user.screen_name})", 2, " ", 3, 61, "#{foot} #{nitter_link}", 3>>, reply_to] ++ text ++ quoted - - text = [head, reply_to | text] ++ quoted - |> Enum.filter(& &1) - {:ok, text} - end - - defp expand_twitter_text(tweet, _opts) 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) - |> HtmlEntities.decode() - end - - defp format_tweet_header(tweet, opts, format_opts \\ []) do - prefix = Keyword.get(format_opts, :prefix, nil) - details = Keyword.get(format_opts, :details, true) - - padded_prefix = if prefix, do: "#{prefix} ", else: "" - author = <<padded_prefix::binary, 2, "#{tweet.user.name} (@#{tweet.user.screen_name})", 2>> - - link = link_tweet(tweet, opts) - - {:ok, at} = Timex.parse(tweet.created_at, "%a %b %e %H:%M:%S %z %Y", :strftime) - {:ok, formatted_time} = Timex.format(at, "{relative}", :relative) - - nsfw = if tweet.possibly_sensitive, do: <<3, 52, "NSFW", 3>> - - rts = if tweet.retweet_count && tweet.retweet_count > 0, do: "#{tweet.retweet_count} RT" - likes = if tweet.favorite_count && tweet.favorite_count > 0, do: "#{tweet.favorite_count} ❤︎" - qrts = if tweet.quote_count && tweet.quote_count > 0, do: "#{tweet.quote_count} QRT" - replies = if tweet.reply_count && tweet.reply_count > 0, do: "#{tweet.reply_count} Reps" - - dmcad = if tweet.withheld_copyright, do: <<3, 52, "DMCA", 3>> - withheld_local = if tweet.withheld_in_countries && length(tweet.withheld_in_countries) > 0 do - "Withheld in #{length(tweet.withheld_in_countries)} countries" - end - - verified = if tweet.user.verified, do: <<3, 51, "✔", 3>> - - meta = if details do - [verified, nsfw, formatted_time, dmcad, withheld_local, rts, qrts, likes, replies] - else - [verified, nsfw, formatted_time, dmcad, withheld_local] - end - - meta = meta - |> Enum.filter(& &1) - |> Enum.join(" - ") - - meta = <<3, 15, meta::binary, " → #{link}", 3>> - - <<author::binary, " — ", meta::binary>> - end - -end diff --git a/lib/lsg_irc/link_plugin/youtube.ex b/lib/lsg_irc/link_plugin/youtube.ex deleted file mode 100644 index f7c7541..0000000 --- a/lib/lsg_irc/link_plugin/youtube.ex +++ /dev/null @@ -1,72 +0,0 @@ -defmodule Nola.IRC.LinkPlugin.YouTube do - @behaviour Nola.IRC.LinkPlugin - - @moduledoc """ - # YouTube link preview - - needs an API key: - - ``` - config :nola, :youtube, - api_key: "xxxxxxxxxxxxx" - ``` - - options: - - * `invidious`: Add a link to invidious. - """ - - @impl 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 - - @impl true - def post_match(_, _, _, _), do: false - - @impl true - def expand(uri, %{video_id: video_id}, opts) do - key = Application.get_env(:nola, :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 host = Keyword.get(opts, :invidious) do - ["-> https://#{host}/watch?v=#{video_id}"] - else - [] - end - {:ok, line ++ ["#{snippet["title"]}", "— #{duration} — uploaded by #{snippet["channelTitle"]} — #{date}" - <> " — #{item["statistics"]["viewCount"]} views, #{item["statistics"]["likeCount"]} likes"]} - else - :error - end - _ -> :error - end - end - end - -end diff --git a/lib/lsg_irc/logger_plugin.ex b/lib/lsg_irc/logger_plugin.ex deleted file mode 100644 index b13f33a..0000000 --- a/lib/lsg_irc/logger_plugin.ex +++ /dev/null @@ -1,70 +0,0 @@ -defmodule Nola.IRC.LoggerPlugin do - require Logger - - @couch_db "bot-logs" - - def irc_doc(), do: nil - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init([]) do - regopts = [plugin: __MODULE__] - {:ok, _} = Registry.register(IRC.PubSub, "triggers", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "messages", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "irc:outputs", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "messages:private", regopts) - {:ok, nil} - end - - def handle_info({:irc, :trigger, _, m}, state) do - {:noreply, log(m, state)} - end - - def handle_info({:irc, :text, m}, state) do - {:noreply, log(m, state)} - end - - def handle_info(info, state) do - Logger.debug("logger_plugin: unhandled info: #{info}") - {:noreply, state} - end - - def log(entry, state) do - case Couch.post(@couch_db, format_to_db(entry)) do - {:ok, id, _rev} -> - Logger.debug("logger_plugin: saved: #{inspect id}") - state - error -> - Logger.error("logger_plugin: save failed: #{inspect error}") - end - rescue - e -> - Logger.error("logger_plugin: rescued processing for #{inspect entry}: #{inspect e}") - Logger.error(Exception.format(:error, e, __STACKTRACE__)) - state - catch - e, b -> - Logger.error("logger_plugin: catched processing for #{inspect entry}: #{inspect e}") - Logger.error(Exception.format(e, b, __STACKTRACE__)) - state - end - - def format_to_db(msg = %IRC.Message{id: id}) do - msg - |> Poison.encode!() - |> Map.drop("id") - - %{"_id" => id || FlakeId.get(), - "type" => "irc.message/v1", - "object" => msg} - end - - def format_to_db(anything) do - %{"_id" => FlakeId.get(), - "type" => "object", - "object" => anything} - end - -end diff --git a/lib/lsg_irc/lsg_irc.ex b/lib/lsg_irc/lsg_irc.ex deleted file mode 100644 index f64978a..0000000 --- a/lib/lsg_irc/lsg_irc.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Nola.IRC do - require Logger - - def env(), do: Nola.env(:irc) - def env(key, default \\ nil), do: Keyword.get(env(), key, default) - - def application_childs do - import Supervisor.Spec - - IRC.Connection.setup() - IRC.Plugin.setup() - - [ - worker(Registry, [[keys: :duplicate, name: IRC.ConnectionPubSub]], id: :registr_irc_conn), - worker(Registry, [[keys: :duplicate, name: IRC.PubSub]], id: :registry_irc), - worker(IRC.Membership, []), - worker(IRC.Account, []), - worker(IRC.UserTrack.Storage, []), - worker(IRC.Account.AccountPlugin, []), - supervisor(IRC.Plugin.Supervisor, [], [name: IRC.Plugin.Supervisor]), - supervisor(IRC.Connection.Supervisor, [], [name: IRC.Connection.Supervisor]), - supervisor(IRC.PuppetConnection.Supervisor, [], [name: IRC.PuppetConnection.Supervisor]), - ] - end - - # Start plugins first to let them get on connection events. - def after_start() do - Logger.info("Starting plugins") - IRC.Plugin.start_all() - Logger.info("Starting connections") - IRC.Connection.start_all() - end - -end diff --git a/lib/lsg_irc/outline_plugin.ex b/lib/lsg_irc/outline_plugin.ex deleted file mode 100644 index 820500e..0000000 --- a/lib/lsg_irc/outline_plugin.ex +++ /dev/null @@ -1,108 +0,0 @@ -defmodule Nola.IRC.OutlinePlugin do - @moduledoc """ - # outline auto-link - - Envoie un lien vers Outline quand un lien est envoyé. - - * **!outline `<url>`** crée un lien outline pour `<url>`. - * **+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__, [], name: __MODULE__) - end - - defstruct [:file, :hosts] - - def init([]) do - regopts = [plugin: __MODULE__] - {:ok, _} = Registry.register(IRC.PubSub, "trigger:outline", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "messages", regopts) - file = Path.join(Nola.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, :trigger, "outline", message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [url]}}}, state) do - line = "-> #{outline(url)}" - message.replyfun.(line) - 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 - outline_url = outline(word) - line = "-> #{outline_url}" - 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 - - def outline(url) do - unexpanded = "https://outline.com/#{url}" - headers = [ - {"User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:77.0) Gecko/20100101 Firefox/77.0"}, - {"Accept", "*/*"}, - {"Accept-Language", "en-US,en;q=0.5"}, - {"Origin", "https://outline.com"}, - {"DNT", "1"}, - {"Referer", unexpanded}, - {"Pragma", "no-cache"}, - {"Cache-Control", "no-cache"} - ] - params = %{"source_url" => url} - case HTTPoison.get("https://api.outline.com/v3/parse_article", headers, params: params) do - {:ok, %HTTPoison.Response{status_code: 200, body: json}} -> - body = Poison.decode!(json) - if Map.get(body, "success") do - code = get_in(body, ["data", "short_code"]) - "https://outline.com/#{code}" - else - unexpanded - end - error -> - Logger.info("outline.com error: #{inspect error}") - unexpanded - end - end - -end diff --git a/lib/lsg_irc/preums_plugin.ex b/lib/lsg_irc/preums_plugin.ex deleted file mode 100644 index f250e85..0000000 --- a/lib/lsg_irc/preums_plugin.ex +++ /dev/null @@ -1,276 +0,0 @@ -defmodule Nola.IRC.PreumsPlugin do - @moduledoc """ - # preums !!! - - * `!preums`: affiche le preums du jour - * `.preums`: stats des preums - """ - - # WIP Scores - # L'idée c'est de donner un score pour mettre un peu de challenge en pénalisant les preums faciles. - # - # Un preums ne vaut pas 1 point, mais plutôt 0.10 ou 0.05, et on arrondi au plus proche. C'est un jeu sur le long - # terme. Un gros bonus pourrait apporter beaucoup de points. - # - # Il faudrait ces données: - # - moyenne des preums - # - activité récente du channel et par nb actifs d'utilisateurs - # (aggréger memberships+usertrack last_active ?) - # (faire des stats d'activité habituelle (un peu a la pisg) ?) - # - preums consécutifs - # - # Malus: - # - est proche de la moyenne en faible activité - # - trop consécutif de l'utilisateur sauf si activité - # - # Bonus: - # - plus le preums est éloigné de la moyenne - # - après 18h double - # - plus l'activité est élévée, exponentiel selon la moyenne - # - derns entre 4 et 6 (pourrait être adapté selon les stats d'activité) - # - # WIP Badges: - # - derns - # - streaks - # - faciles - # - ? - - require Logger - - @perfects [~r/preum(s|)/i] - - # dets {{chan, day = {yyyy, mm, dd}}, nick, now, perfect?, text} - - def all(dets) do - :dets.foldl(fn(i, acc) -> [i|acc] end, [], dets) - end - - def all(dets, channel) do - fun = fn({{chan, date}, account_id, time, perfect, text}, acc) -> - if channel == chan do - [%{date: date, account_id: account_id, time: time, perfect: perfect, text: text} | acc] - else - acc - end - end - :dets.foldl(fun, [], dets) - end - - def topnicks(dets, channel, options \\ []) do - sort_elem = case Keyword.get(options, :sort_by, :score) do - :score -> 1 - :count -> 0 - end - - fun = fn(x = {{chan, date}, account_id, time, perfect, text}, acc) -> - if (channel == nil and chan) or (channel == chan) do - {count, points} = Map.get(acc, account_id, {0, 0}) - score = score(chan, account_id, time, perfect, text) - Map.put(acc, account_id, {count + 1, points + score}) - else - acc - end - end - :dets.foldl(fun, %{}, dets) - |> Enum.sort_by(fn({_account_id, value}) -> elem(value, sort_elem) end, &>=/2) - end - - def irc_doc, do: @moduledoc - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def dets do - (Nola.data_path() <> "/preums.dets") |> String.to_charlist() - end - - def init([]) do - regopts = [plugin: __MODULE__] - {:ok, _} = Registry.register(IRC.PubSub, "account", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "messages", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "triggers", regopts) - {:ok, dets} = :dets.open_file(dets(), [{:repair, :force}]) - Util.ets_mutate_select_each(:dets, dets, [{:"$1", [], [:"$1"]}], fn(table, obj) -> - {key, nick, now, perfect, text} = obj - case key do - {{net, {bork,chan}}, date} -> - :dets.delete(table, key) - nick = if IRC.Account.get(nick) do - nick - else - if acct = IRC.Account.find_always_by_nick(net, nil, nick) do - acct.id - else - nick - end - end - :dets.insert(table, { { {net,chan}, date }, nick, now, perfect, text}) - {{_net, nil}, _} -> - :dets.delete(table, key) - {{net, chan}, date} -> - if !IRC.Account.get(nick) do - if acct = IRC.Account.find_always_by_nick(net, chan, nick) do - :dets.delete(table, key) - :dets.insert(table, { { {net,chan}, date }, acct.id, now, perfect, text}) - end - end - _ -> - Logger.debug("DID NOT FIX: #{inspect key}") - end - end) - {:ok, %{dets: dets}} - end - - # Latest - def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) do - channelkey = {m.network, m.channel} - state = handle_preums(m, state) - tz = timezone(channelkey) - {:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase) - date = {now.year, now.month, now.day} - key = {channelkey, date} - chan_cache = Map.get(state, channelkey, %{}) - item = if i = Map.get(chan_cache, date) do - i - else - case :dets.lookup(state.dets, key) do - [item = {^key, _account_id, _now, _perfect, _text}] -> item - _ -> nil - end - end - - if item do - {_, account_id, date, _perfect, text} = item - h = "#{date.hour}:#{date.minute}:#{date.second}" - account = IRC.Account.get(account_id) - user = IRC.UserTrack.find_by_account(m.network, account) - nick = if(user, do: user.nick, else: account.name) - m.replyfun.("preums: #{nick} à #{h}: “#{text}”") - end - {:noreply, state} - end - - # Stats - def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :dot}}}, state) do - channel = {m.network, m.channel} - state = handle_preums(m, state) - top = topnicks(state.dets, channel, sort_by: :score) - |> Enum.map(fn({account_id, {count, score}}) -> - account = IRC.Account.get(account_id) - user = IRC.UserTrack.find_by_account(m.network, account) - nick = if(user, do: user.nick, else: account.name) - "#{nick}: #{score} (#{count})" - end) - |> Enum.intersperse(", ") - |> Enum.join("") - msg = unless top == "" do - "top preums: #{top}" - else - "vous êtes tous nuls" - end - m.replyfun.(msg) - {:noreply, state} - end - - # Help - def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :query}}}, state) do - state = handle_preums(m, state) - msg = "!preums - preums du jour, .preums top preumseurs" - m.replymsg.(msg) - {: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 - - # Account - def handle_info({:account_change, old_id, new_id}, state) do - spec = [{{:_, :"$1", :_, :_, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}] - Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) -> - rename_object_owner(table, obj, new_id) - end) - {:noreply, state} - end - - # Account: move from nick to account id - # FIXME: Doesn't seem to work. - def handle_info({:accounts, accounts}, state) do - for x={:account, _net, _chan, _nick, _account_id} <- accounts do - handle_info(x, state) - end - {:noreply, state} - end - def handle_info({:account, _net, _chan, nick, account_id}, state) do - nick = String.downcase(nick) - spec = [{{:_, :"$1", :_, :_, :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}] - Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) -> - Logger.debug("account:: merging #{nick} -> #{account_id}") - rename_object_owner(table, obj, account_id) - end) - {:noreply, state} - end - - def handle_info(_, dets) do - {:noreply, dets} - end - - - defp rename_object_owner(table, object = {key, _, now, perfect, time}, new_id) do - :dets.delete_object(table, key) - :dets.insert(table, {key, new_id, now, perfect, time}) - end - - defp timezone(channel) do - env = Application.get_env(:nola, Nola.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(%IRC.Message{channel: nil}, state) do - state - end - - defp handle_preums(m = %IRC.Message{text: text, sender: sender}, state) do - channel = {m.network, m.channel} - 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 - Map.put(state, channel, %{date => item}) - [] -> - # Preums won! - perfect? = Enum.any?(@perfects, fn(perfect) -> Regex.match?(perfect, text) end) - item = {key, m.account.id, now, perfect?, text} - :dets.insert(state.dets, item) - :dets.sync(state.dets) - Map.put(state, channel, %{date => item}) - {:error, _} = error -> - Logger.error("#{__MODULE__} dets lookup failed: #{inspect error}") - state - end - else - state - end - end - - def score(_chan, _account, _time, _perfect, _text) do - 1 - end - - -end diff --git a/lib/lsg_irc/quatre_cent_vingt_plugin.ex b/lib/lsg_irc/quatre_cent_vingt_plugin.ex deleted file mode 100644 index 8953ea3..0000000 --- a/lib/lsg_irc/quatre_cent_vingt_plugin.ex +++ /dev/null @@ -1,149 +0,0 @@ -defmodule Nola.IRC.QuatreCentVingtPlugin do - require Logger - - @moduledoc """ - # 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. - """ - - @achievements %{ - 1 => ["[le premier… il faut bien commencer un jour]"], - 10 => ["T'en es seulement à 10 ? ╭∩╮(Ο_Ο)╭∩╮"], - 42 => ["Bravo, et est-ce que autant de pétards t'on aidés à trouver la Réponse ? ٩(- ̮̮̃-̃)۶ [42]"], - 100 => ["°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ 100 °º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸"], - 115 => [" ۜ\(סּںסּَ` )/ۜ 115!!"] - } - - @emojis [ - "\\o/", - "~o~", - "~~o∞~~", - "*\\o/*", - "**\\o/**", - "*ô*", - ] - - @coeffs Range.new(1, 100) - - def irc_doc, do: @moduledoc - - def start_link, do: GenServer.start_link(__MODULE__, [], name: __MODULE__) - - def init(_) do - for coeff <- @coeffs do - {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{420*coeff}", [plugin: __MODULE__]) - end - {:ok, _} = Registry.register(IRC.PubSub, "account", [plugin: __MODULE__]) - dets_filename = (Nola.data_path() <> "/420.dets") |> String.to_charlist - {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag},{:repair,:force}]) - {:ok, dets} - :ignore - end - - for coeff <- @coeffs do - qvc = to_string(420 * coeff) - def handle_info({:irc, :trigger, unquote(qvc), m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :bang}}}, dets) do - {count, last} = get_statistics_for_nick(dets, m.account.id) - count = count + unquote(coeff) - text = achievement_text(count) - now = DateTime.to_unix(DateTime.utc_now())-1 # this is ugly - for i <- Range.new(1, unquote(coeff)) do - :ok = :dets.insert(dets, {m.account.id, now+i}) - end - last_s = if last do - last_s = format_relative_timestamp(last) - " (le dernier était #{last_s})" - else - "" - end - m.replyfun.("#{m.sender.nick} 420 +#{unquote(coeff)} #{text}#{last_s}") - {:noreply, dets} - end - end - - def handle_info({:irc, :trigger, "420", m = %IRC.Message{trigger: %IRC.Trigger{args: [nick], type: :bang}}}, dets) do - account = IRC.Account.find_by_nick(m.network, nick) - if account do - text = case get_statistics_for_nick(dets, m.account.id) do - {0, _} -> "#{nick} n'a jamais !420 ... honte à lui." - {count, last} -> - last_s = format_relative_timestamp(last) - "#{nick} 420: total #{count}, le dernier #{last_s}" - end - m.replyfun.(text) - else - m.replyfun.("je connais pas de #{nick}") - end - {:noreply, dets} - end - - # Account - def handle_info({:account_change, old_id, new_id}, dets) do - spec = [{{:"$1", :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}] - Util.ets_mutate_select_each(:dets, dets, spec, fn(table, obj) -> - rename_object_owner(table, obj, new_id) - end) - {:noreply, dets} - end - - # Account: move from nick to account id - def handle_info({:accounts, accounts}, dets) do - for x={:account, _net, _chan, _nick, _account_id} <- accounts do - handle_info(x, dets) - end - {:noreply, dets} - end - def handle_info({:account, _net, _chan, nick, account_id}, dets) do - nick = String.downcase(nick) - spec = [{{:"$1", :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}] - Util.ets_mutate_select_each(:dets, dets, spec, fn(table, obj) -> - Logger.debug("account:: merging #{nick} -> #{account_id}") - rename_object_owner(table, obj, account_id) - end) - {:noreply, dets} - end - - def handle_info(_, dets) do - {:noreply, dets} - end - - defp rename_object_owner(table, object = {_, at}, account_id) do - :dets.delete_object(table, object) - :dets.insert(table, {account_id, at}) - end - - - defp format_relative_timestamp(timestamp) do - alias Timex.Format.DateTime.Formatters - alias Timex.Timezone - date = timestamp - |> DateTime.from_unix! - |> 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 - - defp get_statistics_for_nick(dets, acct) do - qvc = :dets.lookup(dets, acct) |> Enum.sort - count = Enum.reduce(qvc, 0, fn(_, acc) -> acc + 1 end) - {_, last} = List.last(qvc) || {nil, nil} - {count, last} - end - - @achievements_keys Map.keys(@achievements) - defp achievement_text(count) when count in @achievements_keys do - Enum.random(Map.get(@achievements, count)) - end - - defp achievement_text(count) do - emoji = Enum.random(@emojis) - "#{emoji} [#{count}]" - end - -end diff --git a/lib/lsg_irc/radio_france_plugin.ex b/lib/lsg_irc/radio_france_plugin.ex deleted file mode 100644 index c2e966f..0000000 --- a/lib/lsg_irc/radio_france_plugin.ex +++ /dev/null @@ -1,133 +0,0 @@ -defmodule Nola.IRC.RadioFrancePlugin do - require Logger - - def irc_doc() do - """ - # radio france - - Qu'est ce qu'on écoute sur radio france ? - - * **!radiofrance `[station]`, !rf `[station]`** - * **!fip, !inter, !info, !bleu, !culture, !musique, !fip `[sous-station]`, !bleu `[région]`** - """ - end - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - @trigger "radiofrance" - @shortcuts ~w(fip inter info bleu culture musique) - - def init(_) do - regopts = [plugin: __MODULE__] - {:ok, _} = Registry.register(IRC.PubSub, "trigger:radiofrance", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "trigger:rf", regopts) - for s <- @shortcuts do - {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{s}", regopts) - end - {:ok, nil} - end - - def handle_info({:irc, :trigger, "rf", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) do - handle_info({:irc, :trigger, "radiofrance", m}, state) - end - - def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do - m.replyfun.("radiofrance: précisez la station!") - {:noreply, state} - end - - def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do - now(args_to_station(args), m) - {:noreply, state} - end - - def handle_info({:irc, :trigger, trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) when trigger in @shortcuts do - now(args_to_station([trigger | args]), m) - {:noreply, state} - end - - defp args_to_station(args) do - args - |> Enum.map(&unalias/1) - |> Enum.map(&String.downcase/1) - |> Enum.join("_") - end - - def handle_info(info, state) do - Logger.debug("unhandled info: #{inspect info}") - {:noreply, state} - end - - defp now(station, m) when is_binary(station) do - case HTTPoison.get(np_url(station), [], []) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - json = Poison.decode!(body) - song? = !!get_in(json, ["now", "song"]) - station = reformat_station_name(get_in(json, ["now", "stationName"])) - now_title = get_in(json, ["now", "firstLine", "title"]) - now_subtitle = get_in(json, ["now", "secondLine", "title"]) - next_title = get_in(json, ["next", "firstLine", "title"]) - next_subtitle = get_in(json, ["next", "secondLine", "title"]) - next_song? = !!get_in(json, ["next", "song"]) - next_at = get_in(json, ["next", "startTime"]) - - now = format_title(song?, now_title, now_subtitle) - prefix = if song?, do: "🎶", else: "🎤" - m.replyfun.("#{prefix} #{station}: #{now}") - - next = format_title(song?, next_title, next_subtitle) - if next do - next_prefix = if next_at do - next_date = DateTime.from_unix!(next_at) - in_seconds = DateTime.diff(next_date, DateTime.utc_now()) - in_minutes = ceil(in_seconds / 60) - if in_minutes >= 5 do - if next_song?, do: "#{in_minutes}m 🔜", else: "dans #{in_minutes} minutes:" - else - if next_song?, do: "🔜", else: "suivi de:" - end - else - if next_song?, do: "🔜", else: "à suivre:" - end - m.replyfun.("#{next_prefix} #{next}") - end - - {:error, %HTTPoison.Response{status_code: 404}} -> - m.replyfun.("radiofrance: la radio \"#{station}\" n'existe pas") - - {:error, %HTTPoison.Response{status_code: code}} -> - m.replyfun.("radiofrance: erreur http #{code}") - - _ -> - m.replyfun.("radiofrance: ça n'a pas marché, rip") - end - end - - defp np_url(station), do: "https://www.radiofrance.fr/api/v2.0/stations/#{station}/live" - - defp unalias("inter"), do: "franceinter" - defp unalias("info"), do: "franceinfo" - defp unalias("bleu"), do: "francebleu" - defp unalias("culture"), do: "franceculture" - defp unalias("musique"), do: "francemusique" - defp unalias(station), do: station - - defp format_title(_, nil, nil) do - nil - end - defp format_title(true, title, artist) do - [artist, title] |> Enum.filter(& &1) |> Enum.join(" - ") - end - defp format_title(false, show, section) do - [show, section] |> Enum.filter(& &1) |> Enum.join(": ") - end - - defp reformat_station_name(station) do - station - |> String.replace("france", "france ") - |> String.replace("_", " ") - end - -end diff --git a/lib/lsg_irc/say_plugin.ex b/lib/lsg_irc/say_plugin.ex deleted file mode 100644 index 915b0f6..0000000 --- a/lib/lsg_irc/say_plugin.ex +++ /dev/null @@ -1,73 +0,0 @@ -defmodule Nola.IRC.SayPlugin do - - def irc_doc do - """ - # say - - Say something... - - * **!say `<channel>` `<text>`** say something on `channel` - * **!asay `<channel>` `<text>`** same but anonymously - - You must be a member of the channel. - """ - end - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init([]) do - regopts = [type: __MODULE__] - {:ok, _} = Registry.register(IRC.PubSub, "trigger:say", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "trigger:asay", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "messages:private", regopts) - {:ok, nil} - end - - def handle_info({:irc, :trigger, "say", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do - text = Enum.join(text, " ") - say_for(m.account, target, text, true) - {:noreply, state} - end - - def handle_info({:irc, :trigger, "asay", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do - text = Enum.join(text, " ") - say_for(m.account, target, text, false) - {:noreply, state} - end - - def handle_info({:irc, :text, m = %{text: "say "<>rest}}, state) do - case String.split(rest, " ", parts: 2) do - [target, text] -> say_for(m.account, target, text, true) - _ -> nil - end - {:noreply, state} - end - - def handle_info({:irc, :text, m = %{text: "asay "<>rest}}, state) do - case String.split(rest, " ", parts: 2) do - [target, text] -> say_for(m.account, target, text, false) - _ -> nil - end - {:noreply, state} - end - - def handle_info(_, state) do - {:noreply, state} - end - - defp say_for(account, target, text, with_nick?) do - for {net, chan} <- IRC.Membership.of_account(account) do - chan2 = String.replace(chan, "#", "") - if (target == "#{net}/#{chan}" || target == "#{net}/#{chan2}" || target == chan || target == chan2) do - if with_nick? do - IRC.send_message_as(account, net, chan, text) - else - IRC.Connection.broadcast_message(net, chan, text) - end - end - end - end - -end diff --git a/lib/lsg_irc/script_plugin.ex b/lib/lsg_irc/script_plugin.ex deleted file mode 100644 index 94d4edf..0000000 --- a/lib/lsg_irc/script_plugin.ex +++ /dev/null @@ -1,42 +0,0 @@ -defmodule Nola.IRC.ScriptPlugin do - require Logger - - @moduledoc """ - Allows to run outside scripts. Scripts are expected to be long running and receive/send data as JSON over stdin/stdout. - - """ - - @ircdoc """ - # script - - Allows to run an outside script. - - * **+script `<name>` `[command]`** défini/lance un script - * **-script `<name>`** arrête un script - * **-script del `<name>`** supprime un script - """ - - def irc_doc, do: @ircdoc - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init([]) do - {:ok, _} = Registry.register(IRC.PubSub, "trigger:script", [plugin: __MODULE__]) - dets_filename = (Nola.data_path() <> "/" <> "scripts.dets") |> String.to_charlist - {:ok, dets} = :dets.open_file(dets_filename, []) - {:ok, %{dets: dets}} - end - - def handle_info({:irc, :trigger, "script", m = %{trigger: %{type: :plus, args: [name | args]}}}, state) do - end - - def handle_info({:irc, :trigger, "script", m = %{trigger: %{type: :minus, args: args}}}, state) do - case args do - ["del", name] -> :ok #prout - [name] -> :ok#stop - end - end - -end diff --git a/lib/lsg_irc/seen_plugin.ex b/lib/lsg_irc/seen_plugin.ex deleted file mode 100644 index 2a4d0dd..0000000 --- a/lib/lsg_irc/seen_plugin.ex +++ /dev/null @@ -1,59 +0,0 @@ -defmodule Nola.IRC.SeenPlugin do - @moduledoc """ - # seen - - * **!seen `<nick>`** - """ - - def irc_doc, do: @moduledoc - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init([]) do - regopts = [plugin: __MODULE__] - {:ok, _} = Registry.register(IRC.PubSub, "triggers", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "messages", regopts) - dets_filename = (Nola.data_path() <> "/seen.dets") |> String.to_charlist() - {:ok, dets} = :dets.open_file(dets_filename, []) - {:ok, %{dets: dets}} - end - - def handle_info({:irc, :trigger, "seen", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [nick]}}}, state) do - witness(m, state) - m.replyfun.(last_seen(m.channel, nick, state)) - {:noreply, state} - end - - def handle_info({:irc, :trigger, _, m}, state) do - witness(m, state) - {:noreply, state} - end - - def handle_info({:irc, :text, m}, state) do - witness(m, state) - {:noreply, state} - end - - defp witness(%IRC.Message{channel: channel, text: text, sender: %{nick: nick}}, %{dets: dets}) do - :dets.insert(dets, {{channel, nick}, DateTime.utc_now(), text}) - :ok - end - - defp last_seen(channel, nick, %{dets: dets}) do - case :dets.lookup(dets, {channel, nick}) do - [{_, date, text}] -> - diff = round(DateTime.diff(DateTime.utc_now(), date)/60) - cond do - diff >= 30 -> - duration = Timex.Duration.from_minutes(diff) - format = Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized) - "#{nick} a parlé pour la dernière fois il y a #{format}: “#{text}”" - true -> "#{nick} est là..." - end - [] -> - "je ne connais pas de #{nick}" - end - end - -end diff --git a/lib/lsg_irc/sms_plugin.ex b/lib/lsg_irc/sms_plugin.ex deleted file mode 100644 index d8f7387..0000000 --- a/lib/lsg_irc/sms_plugin.ex +++ /dev/null @@ -1,165 +0,0 @@ -defmodule Nola.IRC.SmsPlugin do - @moduledoc """ - ## sms - - * **!sms `<nick>` `<message>`** envoie un SMS. - """ - def short_irc_doc, do: false - def irc_doc, do: @moduledoc - require Logger - - def incoming(from, "enable "<>key) do - key = String.trim(key) - account = IRC.Account.find_meta_account("sms-validation-code", String.downcase(key)) - if account do - net = IRC.Account.get_meta(account, "sms-validation-target") - IRC.Account.put_meta(account, "sms-number", from) - IRC.Account.delete_meta(account, "sms-validation-code") - IRC.Account.delete_meta(account, "sms-validation-number") - IRC.Account.delete_meta(account, "sms-validation-target") - IRC.Connection.broadcast_message(net, account, "SMS Number #{from} added!") - send_sms(from, "Yay! Number linked to account #{account.name}") - end - end - - def incoming(from, message) do - account = IRC.Account.find_meta_account("sms-number", from) - if account do - reply_fun = fn(text) -> - send_sms(from, text) - end - trigger_text = if Enum.any?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(message, trigger) end) do - message - else - "!"<>message - end - message = %IRC.Message{ - id: FlakeId.get(), - transport: :sms, - network: "sms", - channel: nil, - text: message, - account: account, - sender: %ExIRC.SenderInfo{nick: account.name}, - replyfun: reply_fun, - trigger: IRC.Connection.extract_trigger(trigger_text) - } - Logger.debug("converted sms to message: #{inspect message}") - IRC.Connection.publish(message, ["messages:sms"]) - message - end - end - - def my_number() do - Keyword.get(Application.get_env(:nola, :sms, []), :number, "+33000000000") - end - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def path() do - account = Keyword.get(Application.get_env(:nola, :sms), :account) - "https://eu.api.ovh.com/1.0/sms/#{account}" - end - - def path(rest) do - Path.join(path(), rest) - end - - def send_sms(number, text) do - url = path("/virtualNumbers/#{my_number()}/jobs") - body = %{ - "message" => text, - "receivers" => [number], - #"senderForResponse" => true, - #"noStopClause" => true, - "charset" => "UTF-8", - "coding" => "8bit" - } |> Poison.encode!() - headers = [{"content-type", "application/json"}] ++ sign("POST", url, body) - options = [] - case HTTPoison.post(url, body, headers, options) do - {:ok, %HTTPoison.Response{status_code: 200}} -> :ok - {:ok, %HTTPoison.Response{status_code: code} = resp} -> - Logger.error("SMS Error: #{inspect resp}") - {:error, code} - {:error, error} -> {:error, error} - end - end - - def init([]) do - {:ok, _} = Registry.register(IRC.PubSub, "trigger:sms", [plugin: __MODULE__]) - :ok = register_ovh_callback() - {:ok, %{}} - :ignore - end - - def handle_info({:irc, :trigger, "sms", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [nick | text]}}}, state) do - with \ - {:tree, false} <- {:tree, m.sender.nick == "Tree"}, - {_, %IRC.Account{} = account} <- {:account, IRC.Account.find_always_by_nick(m.network, m.channel, nick)}, - {_, number} when not is_nil(number) <- {:number, IRC.Account.get_meta(account, "sms-number")} - do - text = Enum.join(text, " ") - sender = if m.channel do - "#{m.channel} <#{m.sender.nick}> " - else - "<#{m.sender.nick}> " - end - case send_sms(number, sender<>text) do - :ok -> m.replyfun.("sent!") - {:error, error} -> m.replyfun.("not sent, error: #{inspect error}") - end - else - {:tree, _} -> m.replyfun.("Tree: va en enfer") - {:account, _} -> m.replyfun.("#{nick} not known") - {:number, _} -> m.replyfun.("#{nick} have not enabled sms") - end - {:noreply, state} - end - - def handle_info(msg, state) do - {:noreply, state} - end - - defp register_ovh_callback() do - url = path() - body = %{ - "callBack" =>NolaWeb.Router.Helpers.sms_url(NolaWeb.Endpoint, :ovh_callback), - "smsResponse" => %{ - "cgiUrl" => NolaWeb.Router.Helpers.sms_url(NolaWeb.Endpoint, :ovh_callback), - "responseType" => "cgi" - } - } |> Poison.encode!() - headers = [{"content-type", "application/json"}] ++ sign("PUT", url, body) - options = [] - case HTTPoison.put(url, body, headers, options) do - {:ok, %HTTPoison.Response{status_code: 200}} -> - :ok - error -> error - end - end - - defp sign(method, url, body) do - ts = DateTime.utc_now() |> DateTime.to_unix() - as = env(:app_secret) - ck = env(:consumer_key) - sign = Enum.join([as, ck, String.upcase(method), url, body, ts], "+") - sign_hex = :crypto.hash(:sha, sign) |> Base.encode16(case: :lower) - headers = [{"X-OVH-Application", env(:app_key)}, {"X-OVH-Timestamp", ts}, - {"X-OVH-Signature", "$1$"<>sign_hex}, {"X-Ovh-Consumer", ck}] - end - - def parse_number(num) do - {:error, :todo} - end - - defp env() do - Application.get_env(:nola, :sms) - end - - defp env(key) do - Keyword.get(env(), key) - end -end diff --git a/lib/lsg_irc/tell_plugin.ex b/lib/lsg_irc/tell_plugin.ex deleted file mode 100644 index ecc98df..0000000 --- a/lib/lsg_irc/tell_plugin.ex +++ /dev/null @@ -1,106 +0,0 @@ -defmodule Nola.IRC.TellPlugin do - use GenServer - - @moduledoc """ - # Tell - - * **!tell `<nick>` `<message>`**: tell `message` to `nick` when they reconnect. - """ - - def irc_doc, do: @moduledoc - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def dets do - (Nola.data_path() <> "/tell.dets") |> String.to_charlist() - end - - def tell(m, target, message) do - GenServer.cast(__MODULE__, {:tell, m, target, message}) - end - - def init([]) do - regopts = [plugin: __MODULE__] - {:ok, _} = Registry.register(IRC.PubSub, "account", regopts) - {:ok, _} = Registry.register(IRC.PubSub, "trigger:tell", regopts) - {:ok, dets} = :dets.open_file(dets(), [type: :bag]) - {:ok, %{dets: dets}} - end - - def handle_cast({:tell, m, target, message}, state) do - do_tell(state, m, target, message) - {:noreply, state} - end - - def handle_info({:irc, :trigger, "tell", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [target | message]}}}, state) do - do_tell(state, m, target, message) - {:noreply, state} - end - - def handle_info({:account, network, channel, nick, account_id}, state) do - messages = :dets.lookup(state.dets, {network, channel, account_id}) - if messages != [] do - strs = Enum.map(messages, fn({_, from, message, at}) -> - account = IRC.Account.get(from) - user = IRC.UserTrack.find_by_account(network, account) - fromnick = if user, do: user.nick, else: account.name - "#{nick}: <#{fromnick}> #{message}" - end) - Enum.each(strs, fn(s) -> IRC.Connection.broadcast_message(network, channel, s) end) - :dets.delete(state.dets, {network, channel, account_id}) - end - {:noreply, state} - end - - def handle_info({:account_change, old_id, new_id}, state) do - #:ets.fun2ms(fn({ {_net, _chan, target_id}, from_id, _, _} = obj) when (target_id == old_id) or (from_id == old_id) -> obj end) - spec = [{{{:"$1", :"$2", :"$3"}, :"$4", :_, :_}, [{:orelse, {:==, :"$3", {:const, old_id}}, {:==, :"$4", {:const, old_id}}}], [:"$_"]}] - Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) -> - case obj do - { {net, chan, ^old_id}, from_id, message, at } = obj -> - :dets.delete(obj) - :dets.insert(table, {{net, chan, new_id}, from_id, message, at}) - {key, ^old_id, message, at} = obj -> - :dets.delete(table, obj) - :dets.insert(table, {key, new_id, message, at}) - _ -> :ok - end - end) - {:noreply, state} - end - - - def handle_info(info, state) do - {:noreply, state} - end - - def terminate(_, state) do - :dets.close(state.dets) - :ok - end - - defp do_tell(state, m, nick_target, message) do - target = IRC.Account.find_always_by_nick(m.network, m.channel, nick_target) - message = Enum.join(message, " ") - with \ - {:target, %IRC.Account{} = target} <- {:target, target}, - {:same, false} <- {:same, target.id == m.account.id}, - target_user = IRC.UserTrack.find_by_account(m.network, target), - target_nick = if(target_user, do: target_user.nick, else: target.name), - present? = if(target_user, do: Map.has_key?(target_user.last_active, m.channel)), - {:absent, true, _} <- {:absent, !present?, target_nick}, - {:message, message} <- {:message, message} - do - obj = { {m.network, m.channel, target.id}, m.account.id, message, NaiveDateTime.utc_now()} - :dets.insert(state.dets, obj) - m.replyfun.("will tell to #{target_nick}") - else - {:same, _} -> m.replyfun.("are you so stupid that you need a bot to tell yourself things ?") - {:target, _} -> m.replyfun.("#{nick_target} unknown") - {:absent, _, nick} -> m.replyfun.("#{nick} is here, tell yourself!") - {:message, _} -> m.replyfun.("can't tell without a message") - end - end - -end diff --git a/lib/lsg_irc/txt_plugin.ex b/lib/lsg_irc/txt_plugin.ex deleted file mode 100644 index cab912a..0000000 --- a/lib/lsg_irc/txt_plugin.ex +++ /dev/null @@ -1,556 +0,0 @@ -defmodule Nola.IRC.TxtPlugin do - alias IRC.UserTrack - require Logger - - @moduledoc """ - # [txt]({{context_path}}/txt) - - * **.txt**: liste des fichiers et statistiques. - Les fichiers avec une `*` sont vérrouillés. - [Voir sur le web]({{context_path}}/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`. - * **!`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`. - - * **-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() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - defstruct triggers: %{}, rw: true, locks: nil, markov_handler: nil, markov: nil - - def random(file) do - GenServer.call(__MODULE__, {:random, file}) - end - - def reply_random(message, file) do - if line = random(file) do - line - |> format_line(nil, message) - |> message.replyfun.() - - line - end - end - - def init([]) do - dets_locks_filename = (Nola.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist - {:ok, locks} = :dets.open_file(dets_locks_filename, []) - markov_handler = Keyword.get(Application.get_env(:nola, __MODULE__, []), :markov_handler, Nola.IRC.TxtPlugin.Markov.Native) - {:ok, markov} = markov_handler.start_link() - {:ok, _} = Registry.register(IRC.PubSub, "triggers", [plugin: __MODULE__]) - {: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: RW/RO - # - - def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :plus}}}, state = %{rw: false}) do - if channel && UserTrack.operator?(msg.network, 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({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :minus}}}, state = %{rw: true}) do - if channel && UserTrack.operator?(msg.network, channel, msg.sender.nick) do - msg.replyfun.("txt: écriture désactivée") - {:noreply, %__MODULE__{state | rw: false}} - else - {:noreply, state} - end - end - - # - # ADMIN: LOCKS - # - - def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do - with \ - {trigger, _} <- clean_trigger(trigger), - true <- UserTrack.operator?(msg.network, msg.channel, msg.sender.nick) - do - :dets.insert(state.locks, {trigger}) - msg.replyfun.("txt: #{trigger} verrouillé") - end - {:noreply, state} - end - - def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :minus, args: [trigger]}}}, state) do - with \ - {trigger, _} <- clean_trigger(trigger), - true <- UserTrack.operator?(msg.network, msg.channel, msg.sender.nick), - true <- :dets.member(state.locks, trigger) - do - :dets.delete(state.locks, trigger) - msg.replyfun.("txt: #{trigger} déverrouillé") - end - {:noreply, state} - end - - # - # FILE LIST - # - - def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :dot}}}, state) do - map = Enum.map(state.triggers, fn({key, data}) -> - ignore? = String.contains?(key, ".") - locked? = case :dets.lookup(state.locks, key) do - [{trigger}] -> "*" - _ -> "" - end - - unless ignore?, do: "#{key}: #{to_string(Enum.count(data))}#{locked?}" - end) - |> Enum.filter(& &1) - total = Enum.reduce(state.triggers, 0, fn({_, data}, acc) -> - acc + Enum.count(data) - end) - detail = Enum.join(map, ", ") - total = ". total: #{Enum.count(state.triggers)} fichiers, #{to_string(total)} lignes. Détail: https://sys.115ans.net/irc/txt" - - ro = if !state.rw, do: " (lecture seule activée)", else: "" - - (detail<>total<>ro) - |> msg.replyfun.() - {:noreply, state} - end - - # - # 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.(format_line(line, "#{source}: ", msg)) - end - {:noreply, state} - end - - def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: args}}}, state) do - grep = Enum.join(args, " ") - |> String.downcase - |> :unicode.characters_to_nfd_binary() - - result = with_stateful_results(msg, {:bang,"txt",msg.network,msg.channel,grep}, fn() -> - Enum.reduce(state.triggers, [], fn({trigger, data}, acc) -> - if !String.contains?(trigger, ".") do - Enum.reduce(data, acc, fn({l, _}, acc) -> - [{trigger, l} | acc] - end) - else - acc - end - end) - |> Enum.filter(fn({_, line}) -> - line - |> String.downcase() - |> :unicode.characters_to_nfd_binary() - |> String.contains?(grep) - end) - |> Enum.shuffle() - end) - - if result do - {source, line} = result - msg.replyfun.(["#{source}: " | line]) - end - {:noreply, state} - end - - def with_stateful_results(msg, key, initfun) do - me = self() - scope = {msg.network, msg.channel || msg.sender.nick} - key = {__MODULE__, me, scope, key} - with_stateful_results(key, initfun) - end - - def with_stateful_results(key, initfun) do - pid = case :global.whereis_name(key) do - :undefined -> - start_stateful_results(key, initfun.()) - pid -> pid - end - if pid, do: wait_stateful_results(key, initfun, pid) - end - - def start_stateful_results(key, []) do - nil - end - - def start_stateful_results(key, list) do - me = self() - {pid, _} = spawn_monitor(fn() -> - Process.monitor(me) - stateful_results(me, list) - end) - :yes = :global.register_name(key, pid) - pid - end - - def wait_stateful_results(key, initfun, pid) do - send(pid, :get) - receive do - {:stateful_results, line} -> - line - {:DOWN, _ref, :process, ^pid, reason} -> - with_stateful_results(key, initfun) - after - 5000 -> - nil - end - end - - defp stateful_results(owner, []) do - send(owner, :empty) - :ok - end - - @stateful_results_expire :timer.minutes(30) - defp stateful_results(owner, [line | rest] = acc) do - receive do - :get -> - send(owner, {:stateful_results, line}) - stateful_results(owner, rest) - {:DOWN, _ref, :process, ^owner, _} -> - :ok - after - @stateful_results_expire -> :ok - end - 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({: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 - - # - # 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, msg, trigger), - :ok <- create_file(trigger) - do - msg.replyfun.("#{trigger}.txt créé. Ajouter: `+#{trigger} …` ; Lire: `!#{trigger}`") - {:noreply, %__MODULE__{state | triggers: load()}} - else - _ -> {:noreply, state} - end - end - - # - # TXT: RANDOM - # - - def handle_info({:irc, :trigger, trigger, m = %{trigger: %{type: :query, args: opts}}}, state) do - {trigger, _} = clean_trigger(trigger) - if Map.get(state.triggers, trigger) do - url = if m.channel do - NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :txt, m.network, NolaWeb.format_chan(m.channel), trigger) - else - NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :txt, trigger) - end - m.replyfun.("-> #{url}") - end - {:noreply, state} - end - - def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :bang, args: opts}}}, state) do - {trigger, _} = clean_trigger(trigger) - line = get_random(msg, state.triggers, trigger, String.trim(Enum.join(opts, " "))) - if line do - msg.replyfun.(format_line(line, nil, msg)) - end - {:noreply, state} - end - - # - # TXT: ADD - # - - def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :plus, args: content}}}, state) do - with \ - true <- can_write?(state, msg, trigger), - {:ok, idx} <- add(state.triggers, msg.text) - do - msg.replyfun.("#{msg.sender.nick}: ajouté à #{trigger}. (#{idx})") - {:noreply, %__MODULE__{state | triggers: load()}} - else - {:error, {:jaro, string, idx}} -> - msg.replyfun.("#{msg.sender.nick}: doublon #{trigger}##{idx}: #{string}") - error -> - Logger.debug("txt add failed: #{inspect error}") - {:noreply, state} - end - end - - # - # TXT: DELETE - # - - def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :minus, args: [id]}}}, state) do - with \ - 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) - msg.replyfun.("#{msg.sender.nick}: #{trigger}.txt##{id} supprimée: #{text}") - dump(trigger, data) - {:noreply, %__MODULE__{state | triggers: load()}} - else - _ -> - {:noreply, state} - end - end - - def handle_info(:reload_markov, state=%__MODULE__{triggers: triggers, markov: markov}) do - state.markov_handler.reload(state.triggers, state.markov) - {:noreply, state} - end - - def handle_info(msg, state) do - {:noreply, state} - end - - def handle_call({:random, file}, _from, state) do - random = get_random(nil, state.triggers, file, []) - {:reply, random, state} - end - - def terminate(_reason, state) do - if state.locks do - :dets.sync(state.locks) - :dets.close(state.locks) - end - :ok - end - - # Load/Reloads text files from disk - defp load() do - triggers = Path.wildcard(directory() <> "/*.txt") - |> Enum.reduce(%{}, fn(path, m) -> - file = Path.basename(path) - key = String.replace(file, ".txt", "") - data = directory() <> file - |> File.read! - |> String.split("\n") - |> Enum.reject(fn(line) -> - cond do - line == "" -> true - !line -> true - true -> false - end - end) - |> Enum.with_index - Map.put(m, key, data) - end) - |> Enum.sort - |> Enum.into(Map.new) - - send(self(), :reload_markov) - triggers - end - - defp dump(trigger, data) do - data = data - |> Enum.sort_by(fn({_, idx}) -> idx end) - |> Enum.map(fn({text, _}) -> text end) - |> Enum.join("\n") - File.write!(directory() <> "/" <> trigger <> ".txt", data<>"\n", []) - end - - defp get_random(msg, triggers, trigger, []) do - if data = Map.get(triggers, trigger) do - {data, _idx} = Enum.random(data) - data - else - nil - end - end - - defp get_random(msg, triggers, trigger, opt) do - arg = case Integer.parse(opt) do - {pos, ""} -> {:index, pos} - {_pos, _some_string} -> {:grep, opt} - _error -> {:grep, opt} - end - get_with_param(msg, triggers, trigger, arg) - end - - defp get_with_param(msg, triggers, trigger, {:index, pos}) do - data = Map.get(triggers, trigger, %{}) - case Enum.find(data, fn({_, index}) -> index+1 == pos end) do - {text, _} -> text - _ -> nil - end - end - - defp get_with_param(msg, triggers, trigger, {:grep, query}) do - out = with_stateful_results(msg, {:grep, trigger, query}, fn() -> - data = Map.get(triggers, trigger, %{}) - regex = Regex.compile!("#{query}", "i") - Enum.filter(data, fn({txt, _}) -> Regex.match?(regex, txt) end) - |> Enum.map(fn({txt, _}) -> txt end) - |> Enum.shuffle() - end) - if out, do: out - end - - defp create_file(name) do - File.touch!(directory() <> "/" <> name <> ".txt") - :ok - end - - defp add(triggers, trigger_and_content) do - case String.split(trigger_and_content, " ", parts: 2) do - [trigger, content] -> - {trigger, _} = clean_trigger(trigger) - - - if Map.has_key?(triggers, trigger) do - jaro = Enum.find(triggers[trigger], fn({string, idx}) -> String.jaro_distance(content, string) > 0.9 end) - - if jaro do - {string, idx} = jaro - {:error, {:jaro, string, idx}} - else - File.write!(directory() <> "/" <> trigger <> ".txt", content<>"\n", [:append]) - idx = Enum.count(triggers[trigger])+1 - {:ok, idx} - end - else - {:error, :notxt} - end - _ -> {:error, :badarg} - end - end - - # fixme: this is definitely the ugliest thing i've ever done - defp clean_trigger(trigger) do - [trigger | opts] = trigger - |> String.strip - |> String.split(" ", parts: 2) - - trigger = trigger - |> String.downcase - |> :unicode.characters_to_nfd_binary() - |> String.replace(~r/[^a-z0-9._]/, "") - |> String.trim(".") - |> String.trim("_") - - {trigger, opts} - end - - def format_line(line, prefix, msg) do - prefix = unless(prefix, do: "", else: prefix) - prefix <> line - |> String.split("\\\\") - |> Enum.map(fn(line) -> - String.split(line, "\\\\\\\\") - end) - |> List.flatten() - |> Enum.map(fn(line) -> - String.trim(line) - |> Tmpl.render(msg) - end) - end - - def directory() do - Application.get_env(:nola, :data_path) <> "/irc.txt/" - end - - defp can_write?(%{rw: rw?, locks: locks}, msg = %{channel: nil, sender: sender}, trigger) do - admin? = IRC.admin?(sender) - locked? = case :dets.lookup(locks, trigger) do - [{trigger}] -> true - _ -> false - end - unlocked? = if rw? == false, do: false, else: !locked? - - can? = unlocked? || admin? - - if !can? do - reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé" - msg.replyfun.("#{sender.nick}: permission refusée (#{reason})") - end - can? - end - - defp can_write?(state = %__MODULE__{rw: rw?, locks: locks}, msg = %{channel: channel, sender: sender}, trigger) do - admin? = IRC.admin?(sender) - operator? = IRC.UserTrack.operator?(msg.network, 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 deleted file mode 100644 index 2e30dfa..0000000 --- a/lib/lsg_irc/txt_plugin/markov.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Nola.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 deleted file mode 100644 index 4c403c2..0000000 --- a/lib/lsg_irc/txt_plugin/markov_native.ex +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Nola.IRC.TxtPlugin.MarkovNative do - @behaviour Nola.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 deleted file mode 100644 index b610ea8..0000000 --- a/lib/lsg_irc/txt_plugin/markov_py_markovify.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Nola.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(Nola.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(:nola)) <> "/irc/txt/markovify.py" - env = Application.get_env(:nola, Nola.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/untappd_plugin.ex b/lib/lsg_irc/untappd_plugin.ex deleted file mode 100644 index 50b0c4d..0000000 --- a/lib/lsg_irc/untappd_plugin.ex +++ /dev/null @@ -1,66 +0,0 @@ -defmodule Nola.IRC.UntappdPlugin do - - def irc_doc() do - """ - # [Untappd](https://untappd.com) - - * `!beer <beer name>` Information about the first beer matching `<beer name>` - * `?beer <beer name>` List the 10 firsts beer matching `<beer name>` - - _Note_: The best way to search is always "Brewery Name + Beer Name", such as "Dogfish 60 Minute". - - Link your Untappd account to the bot (for automated checkins on [alcoolog](#alcoolog), ...) with the `enable-untappd` command, in private. - """ - end - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init(_) do - {:ok, _} = Registry.register(IRC.PubSub, "trigger:beer", [plugin: __MODULE__]) - {:ok, %{}} - end - - def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %{type: :bang, args: args}}}, state) do - case Untappd.search_beer(Enum.join(args, " "), limit: 1) do - {:ok, %{"response" => %{"beers" => %{"count" => count, "items" => [result | _]}}}} -> - %{"beer" => beer, "brewery" => brewery} = result - description = Map.get(beer, "beer_description") - |> String.replace("\n", " ") - |> String.replace("\r", " ") - |> String.trim() - beer_s = "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")} - #{Map.get(beer, "beer_abv")}°" - city = get_in(brewery, ["location", "brewery_city"]) - location = [Map.get(brewery, "brewery_type"), city, Map.get(brewery, "country_name")] - |> Enum.filter(fn(x) -> x end) - |> Enum.join(", ") - extra = "#{Map.get(beer, "beer_style")} - IBU: #{Map.get(beer, "beer_ibu")} - #{location}" - m.replyfun.([beer_s, extra, description]) - err -> - m.replyfun.("Error") - end - {:noreply, state} - end - - - def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %{type: :query, args: args}}}, state) do - case Untappd.search_beer(Enum.join(args, " ")) do - {:ok, %{"response" => %{"beers" => %{"count" => count, "items" => results}}}} -> - beers = for %{"beer" => beer, "brewery" => brewery} <- results do - "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")} - #{Map.get(beer, "beer_abv")}°" - end - |> Enum.intersperse(", ") - |> Enum.join("") - m.replyfun.("#{count}. #{beers}") - err -> - m.replyfun.("Error") - end - {:noreply, state} - end - - def handle_info(info, state) do - {:noreply, state} - end - -end diff --git a/lib/lsg_irc/user_mention_plugin.ex b/lib/lsg_irc/user_mention_plugin.ex deleted file mode 100644 index eb230fd..0000000 --- a/lib/lsg_irc/user_mention_plugin.ex +++ /dev/null @@ -1,52 +0,0 @@ -defmodule Nola.IRC.UserMentionPlugin do - @moduledoc """ - # mention - - * **@`<nick>` `<message>`**: notifie si possible le nick immédiatement via Telegram, SMS, ou équivalent à `!tell`. - """ - - require Logger - - def short_irc_doc, do: false - def irc_doc, do: @moduledoc - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init(_) do - {:ok, _} = Registry.register(IRC.PubSub, "triggers", plugin: __MODULE__) - {:ok, nil} - end - - def handle_info({:irc, :trigger, nick, message = %IRC.Message{sender: sender, account: account, network: network, channel: channel, trigger: %IRC.Trigger{type: :at, args: content}}}, state) do - nick = nick - |> String.trim(":") - |> String.trim(",") - target = IRC.Account.find_always_by_nick(network, channel, nick) - if target do - telegram = IRC.Account.get_meta(target, "telegram-id") - sms = IRC.Account.get_meta(target, "sms-number") - text = "#{channel} <#{sender.nick}> #{Enum.join(content, " ")}" - - cond do - telegram -> - Nola.Telegram.send_message(telegram, "`#{channel}` <**#{sender.nick}**> #{Enum.join(content, " ")}") - sms -> - case Nola.IRC.SmsPlugin.send_sms(sms, text) do - {:error, code} -> message.replyfun("#{sender.nick}: erreur #{code} (sms)") - end - true -> - Nola.IRC.TellPlugin.tell(message, nick, content) - end - else - message.replyfun.("#{nick} m'est inconnu") - end - {:noreply, state} - end - - def handle_info(_, state) do - {:noreply, state} - end - -end diff --git a/lib/lsg_irc/wikipedia_plugin.ex b/lib/lsg_irc/wikipedia_plugin.ex deleted file mode 100644 index 3202e13..0000000 --- a/lib/lsg_irc/wikipedia_plugin.ex +++ /dev/null @@ -1,90 +0,0 @@ -defmodule Nola.IRC.WikipediaPlugin do - require Logger - - @moduledoc """ - # wikipédia - - * **!wp `<recherche>`**: retourne le premier résultat de la `<recherche>` Wikipedia - * **!wp**: un article Wikipédia au hasard - """ - - def irc_doc, do: @moduledoc - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init(_) do - {:ok, _} = Registry.register(IRC.PubSub, "trigger:wp", [plugin: __MODULE__]) - {:ok, nil} - end - - 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, _, 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("", message), do: irc_random(message) - defp irc_search(query, message) do - params = %{ - "action" => "query", - "list" => "search", - "srsearch" => String.strip(query), - "srlimit" => 1, - } - case query_wikipedia(params) do - {:ok, %{"query" => %{"search" => [item | _]}}} -> - title = item["title"] - url = "https://fr.wikipedia.org/wiki/" <> String.replace(title, " ", "_") - msg = "Wikipédia: #{title} — #{url}" - message.replyfun.(msg) - _ -> - nil - end - end - - defp irc_random(message) do - params = %{ - "action" => "query", - "generator" => "random", - "grnnamespace" => 0, - "prop" => "info" - } - case query_wikipedia(params) do - {:ok, %{"query" => %{"pages" => map = %{}}}} -> - [{_, item}] = Map.to_list(map) - title = item["title"] - url = "https://fr.wikipedia.org/wiki/" <> String.replace(title, " ", "_") - msg = "Wikipédia: #{title} — #{url}" - message.replyfun.(msg) - _ -> - nil - end - end - - defp query_wikipedia(params) do - url = "https://fr.wikipedia.org/w/api.php" - params = params - |> Map.put("format", "json") - |> Map.put("utf8", "") - - 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 "Wikipedia HTTP 400: #{inspect body}" - {:error, "http 400"} - error -> - Logger.error "Wikipedia http error: #{inspect error}" - {:error, "http client error"} - end - end - -end diff --git a/lib/lsg_irc/wolfram_alpha_plugin.ex b/lib/lsg_irc/wolfram_alpha_plugin.ex deleted file mode 100644 index 6ee06f0..0000000 --- a/lib/lsg_irc/wolfram_alpha_plugin.ex +++ /dev/null @@ -1,47 +0,0 @@ -defmodule Nola.IRC.WolframAlphaPlugin do - use GenServer - require Logger - - @moduledoc """ - # wolfram alpha - - * **`!wa <requête>`** lance `<requête>` sur WolframAlpha - """ - - def irc_doc, do: @moduledoc - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init(_) do - {:ok, _} = Registry.register(IRC.PubSub, "trigger:wa", [plugin: __MODULE__]) - {:ok, nil} - end - - def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: query}}}, state) do - query = Enum.join(query, " ") - params = %{ - "appid" => Keyword.get(Application.get_env(:nola, :wolframalpha, []), :app_id, "NO_APP_ID"), - "units" => "metric", - "i" => query - } - url = "https://www.wolframalpha.com/input/?i=" <> URI.encode(query) - case HTTPoison.get("http://api.wolframalpha.com/v1/result", [], [params: params]) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> - m.replyfun.(["#{query} -> #{body}", url]) - {:ok, %HTTPoison.Response{status_code: code, body: body}} -> - error = case {code, body} do - {501, b} -> "input invalide: #{body}" - {code, error} -> "erreur #{code}: #{body || ""}" - end - m.replyfun.("wa: #{error}") - {:error, %HTTPoison.Error{reason: reason}} -> - m.replyfun.("wa: erreur http: #{to_string(reason)}") - _ -> - m.replyfun.("wa: erreur http") - end - {:noreply, state} - end - -end diff --git a/lib/lsg_irc/youtube_plugin.ex b/lib/lsg_irc/youtube_plugin.ex deleted file mode 100644 index fb9bea2..0000000 --- a/lib/lsg_irc/youtube_plugin.ex +++ /dev/null @@ -1,104 +0,0 @@ -defmodule Nola.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__, [], name: __MODULE__) - end - - def init([]) do - for t <- ["trigger:yt", "trigger:youtube"], do: {:ok, _} = Registry.register(IRC.PubSub, t, [plugin: __MODULE__]) - {: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(:nola, :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 |