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 | 
