summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.tool-versions2
-rw-r--r--config/config.exs43
-rw-r--r--lib/alcool.ex10
-rw-r--r--lib/couch.ex108
-rw-r--r--lib/irc.ex14
-rw-r--r--lib/irc/admin_handler.ex11
-rw-r--r--lib/irc/connection.ex392
-rw-r--r--lib/irc/message.ex13
-rw-r--r--lib/irc/puppet_connection.ex236
-rw-r--r--lib/matrix.ex56
-rw-r--r--lib/matrix/plug.ex6
-rw-r--r--lib/matrix/room.ex161
-rw-r--r--lib/nola.ex2
-rw-r--r--lib/nola/account.ex58
-rw-r--r--lib/nola/application.ex50
-rw-r--r--lib/nola/auth_token.ex21
-rw-r--r--lib/nola/icecast.ex100
-rw-r--r--lib/nola/icecast_agent.ex6
-rw-r--r--lib/nola/membership.ex43
-rw-r--r--lib/nola/message.ex21
-rw-r--r--lib/nola/plugins.ex44
-rw-r--r--lib/nola/subnet.ex67
-rw-r--r--lib/nola/token.ex24
-rw-r--r--lib/nola/trigger.ex1
-rw-r--r--lib/nola/user_track.ex323
-rw-r--r--lib/open_ai.ex38
-rw-r--r--lib/plugins/account.ex326
-rw-r--r--lib/plugins/alcoolog.ex1689
-rw-r--r--lib/plugins/alcoolog_announcer.ex354
-rw-r--r--lib/plugins/base.ex90
-rw-r--r--lib/plugins/boursorama.ex37
-rw-r--r--lib/plugins/buffer.ex16
-rw-r--r--lib/plugins/calc.ex26
-rw-r--r--lib/plugins/coronavirus.ex266
-rw-r--r--lib/plugins/correction.ex30
-rw-r--r--lib/plugins/dice.ex29
-rw-r--r--lib/plugins/finance.ex209
-rw-r--r--lib/plugins/gpt.ex355
-rw-r--r--lib/plugins/helpers/temp_ref.ex27
-rw-r--r--lib/plugins/image.ex246
-rw-r--r--lib/plugins/kick_roulette.ex9
-rw-r--r--lib/plugins/last_fm.ex167
-rw-r--r--lib/plugins/link.ex292
-rw-r--r--lib/plugins/link/data/quirks_rewrite_host.txt27
-rw-r--r--lib/plugins/link/data/quirks_telegram_bot_user_agent.txt30
-rw-r--r--lib/plugins/link/github.ex93
-rw-r--r--lib/plugins/link/html.ex194
-rw-r--r--lib/plugins/link/image.ex110
-rw-r--r--lib/plugins/link/img_debrid_link.ex35
-rw-r--r--lib/plugins/link/imgur.ex37
-rw-r--r--lib/plugins/link/pdf.ex24
-rw-r--r--lib/plugins/link/quirks.ex45
-rw-r--r--lib/plugins/link/redacted.ex6
-rw-r--r--lib/plugins/link/reddit.ex157
-rw-r--r--lib/plugins/link/scraper.ex66
-rw-r--r--lib/plugins/link/store.ex95
-rw-r--r--lib/plugins/link/twitter.ex127
-rw-r--r--lib/plugins/link/youtube.ex53
-rw-r--r--lib/plugins/logger.ex39
-rw-r--r--lib/plugins/preums.ex148
-rw-r--r--lib/plugins/quatre_cent_vingt.ex107
-rw-r--r--lib/plugins/radio_france.ex59
-rw-r--r--lib/plugins/say.ex26
-rw-r--r--lib/plugins/script.ex16
-rw-r--r--lib/plugins/seen.ex16
-rw-r--r--lib/plugins/sms.ex121
-rw-r--r--lib/plugins/tell.ex62
-rw-r--r--lib/plugins/txt.ex410
-rw-r--r--lib/plugins/txt/markov.ex8
-rw-r--r--lib/plugins/txt/markov_native.ex33
-rw-r--r--lib/plugins/txt/markov_py_markovify.ex13
-rw-r--r--lib/plugins/untappd.ex55
-rw-r--r--lib/plugins/user_mention.ex32
-rw-r--r--lib/plugins/wikipedia.ex40
-rw-r--r--lib/plugins/wolfram_alpha.ex27
-rw-r--r--lib/plugins/youtube.ex68
-rw-r--r--lib/telegram.ex307
-rw-r--r--lib/telegram/room.ex183
-rw-r--r--lib/tmpl.ex42
-rw-r--r--lib/untappd.ex71
-rw-r--r--lib/util.ex29
-rw-r--r--lib/web.ex15
-rw-r--r--lib/web/channels/user_socket.ex2
-rw-r--r--lib/web/components/component.ex5
-rw-r--r--lib/web/components/event_component.ex2
-rw-r--r--lib/web/components/message_component.ex1
-rw-r--r--lib/web/context_plug.ex108
-rw-r--r--lib/web/controllers/alcoolog_controller.ex418
-rw-r--r--lib/web/controllers/gpt_controller.ex33
-rw-r--r--lib/web/controllers/icecast_see_controller.ex12
-rw-r--r--lib/web/controllers/irc_auth_sse_controller.ex56
-rw-r--r--lib/web/controllers/irc_controller.ex124
-rw-r--r--lib/web/controllers/network_controller.ex3
-rw-r--r--lib/web/controllers/open_id_controller.ex39
-rw-r--r--lib/web/controllers/page_controller.ex37
-rw-r--r--lib/web/controllers/sms_controller.ex3
-rw-r--r--lib/web/controllers/untappd_controller.ex8
-rw-r--r--lib/web/endpoint.ex40
-rw-r--r--lib/web/live/chat_live.ex116
-rw-r--r--lib/web/live/chat_live.html.heex4
-rw-r--r--lib/web/router.ex124
-rw-r--r--lib/web/templates/layout/app.html.eex2
-rw-r--r--lib/web/views/alcoolog_view.ex2
-rw-r--r--lib/web/views/error_helpers.ex4
-rw-r--r--lib/web/views/error_view.ex2
-rw-r--r--lib/web/views/layout_view.ex123
-rw-r--r--lib/web/views/network_view.ex1
-rw-r--r--lib/web/views/open_id_view.ex1
-rw-r--r--mix.exs59
-rw-r--r--mix.lock110
110 files changed, 6362 insertions, 4121 deletions
diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 0000000..09b7b82
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1,2 @@
+erlang 28.0.1
+elixir 1.18.4-otp-28
diff --git a/config/config.exs b/config/config.exs
index bf52838..6468a16 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,6 +1,7 @@
import Config
config :logger, level: :debug
+
config :logger, :console,
format: "$date $time [$level] $metadata$message\n",
metadata: :all
@@ -35,8 +36,7 @@ config :nola, NolaWeb.Endpoint,
render_errors: [view: NolaWeb.ErrorView, accepts: ~w(html json)],
server: true,
live_view: [signing_salt: "CHANGE_ME_FFS"],
- pubsub: [name: NolaWeb.PubSub,
- adapter: Phoenix.PubSub.PG2]
+ pubsub: [name: NolaWeb.PubSub, adapter: Phoenix.PubSub.PG2]
config :mime, :types, %{"text/event-stream" => ["sse"]}
@@ -49,8 +49,43 @@ config :nola, :youtube,
invidious: "yewtu.be"
config :mnesia,
- dir: '.mnesia/#{Mix.env}/#{node()}'
+ dir: '.mnesia/#{Mix.env()}/#{node()}'
+
+config :nola, Nola.Plugins.Link,
+ proxy: nil,
+ scraper: [
+ service: "usescraper",
+ config: [
+ api_key: "xxxx",
+ http_options: [
+ timeout: :timer.seconds(120),
+ recv_timeout: :timer.seconds(120)
+ ]
+ ]
+ ],
+ store: [
+ ttl: :timer.hours(24),
+ inhibit: :timer.hours(16),
+ interval: :timer.minutes(30)
+ ],
+ handlers: [
+ "Nola.Plugins.Link.Image": [],
+ "Nola.Plugins.Link.HTML": [],
+ "Nola.Plugins.Link.PDF": [],
+ "Nola.Plugins.Link.YouTube": [
+ invidious: true
+ ],
+ "Nola.Plugins.Link.Twitter": [
+ expand_quoted: true
+ ],
+ "Nola.Plugins.Link.Imgur": [],
+ "Nola.Plugins.Link.Github": [],
+ "Nola.Plugins.Link.Reddit": [],
+ "Nola.Plugins.Link.ImgDebridLink": []
+ ]
+
+config :floki, :html_parser, Floki.HTMLParser.FastHtml
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
-import_config "#{Mix.env}.exs"
+import_config "#{Mix.env()}.exs"
diff --git a/lib/alcool.ex b/lib/alcool.ex
index 40384ba..044e13b 100644
--- a/lib/alcool.ex
+++ b/lib/alcool.ex
@@ -1,16 +1,14 @@
defmodule Alcool do
-
- @spec units(Float.t, Float.t) :: Float.t
+ @spec units(Float.t(), Float.t()) :: Float.t()
def units(cl, degrees) do
- kg(cl, degrees)*100
+ kg(cl, degrees) * 100
end
def grams(cl, degrees) do
- kg(cl, degrees)*1000
+ kg(cl, degrees) * 1000
end
def kg(cl, degrees) do
- (((cl/100) * 0.8) * (degrees/100))
+ cl / 100 * 0.8 * (degrees / 100)
end
-
end
diff --git a/lib/couch.ex b/lib/couch.ex
index 6b39100..8180e6f 100644
--- a/lib/couch.ex
+++ b/lib/couch.ex
@@ -3,17 +3,24 @@ defmodule Couch do
Simple, no-frills, CouchDB client
"""
- @type base_error :: {:error, :bad_request} | {:error, :unauthorized} |
- {:error, :server_error} | {:error, :service_unavailable} |
- {:error, :bad_response} | {:error, HTTPoison.Error.t()}
+ @type base_error ::
+ {:error, :bad_request}
+ | {:error, :unauthorized}
+ | {:error, :server_error}
+ | {:error, :service_unavailable}
+ | {:error, :bad_response}
+ | {:error, HTTPoison.Error.t()}
def new(db, params \\ []) do
{url, headers, options} = prepare_request([db], [], params)
+
case HTTPoison.put(url, headers, options) do
{:ok, %HTTPoison.Response{status_code: ok, body: body}} when ok in [201, 202] ->
:ok
+
{:ok, %HTTPoison.Response{status_code: 412}} ->
{:error, :exists}
+
error ->
handle_generic_response(error)
end
@@ -24,48 +31,70 @@ defmodule Couch do
`params` are [documented here](https://docs.couchdb.org/en/3.2.2-docs/api/document/common.html)
"""
- @spec get(String.t(), String.t() | :all_docs, Keyword.t()) :: {:ok, Map.t()} | {:error, :not_found} | {:error, any()}
+ @spec get(String.t(), String.t() | :all_docs, Keyword.t()) ::
+ {:ok, Map.t()} | {:error, :not_found} | {:error, any()}
def get(db, doc, params \\ [])
def get(db, :all_docs, params), do: get(db, "_all_docs", params)
+
def get(db, doc, params) do
{url, headers, options} = prepare_request([db, doc], [], params)
+
case HTTPoison.get(url, headers, options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, Poison.decode!(body)}
- error -> handle_generic_response(error)
+
+ error ->
+ handle_generic_response(error)
end
end
- @spec post(String.t(), Map.t(), Keyword.t()) :: {:error, :operation_failed} | {:error, :not_found} | {:error, :exists} | {:error, any()}
+ @spec post(String.t(), Map.t(), Keyword.t()) ::
+ {:error, :operation_failed} | {:error, :not_found} | {:error, :exists} | {:error, any()}
def post(db, data, params \\ []) do
- {url, headers, options} = prepare_request([db], [{"content-type", "application/json"}], params)
- with \
- {:ok, %HTTPoison.Response{status_code: ok, body: body}} when ok in [201, 202] <- HTTPoison.post(url, Poison.encode!(data), headers, options),
- {:json, {:ok, %{"ok" => true, "id" => id, "rev" => rev}}} <- {:json, Poison.decode(body)} do
- {:ok, id, rev}
+ {url, headers, options} =
+ prepare_request([db], [{"content-type", "application/json"}], params)
+
+ with {:ok, %HTTPoison.Response{status_code: ok, body: body}} when ok in [201, 202] <-
+ HTTPoison.post(url, Poison.encode!(data), headers, options),
+ {:json, {:ok, %{"ok" => true, "id" => id, "rev" => rev}}} <- {:json, Poison.decode(body)} do
+ {:ok, id, rev}
else
- {:ok, %HTTPoison.Response{status_code: 409}} -> {:error, :exists}
+ {:ok, %HTTPoison.Response{status_code: 409}} ->
+ {:error, :exists}
+
{:json, {:ok, body}} ->
- Logger.error("couch: operation failed: #{inspect body}")
+ Logger.error("couch: operation failed: #{inspect(body)}")
{:error, :operation_failed}
- error -> handle_generic_response(error)
+
+ error ->
+ handle_generic_response(error)
end
end
- @spec put(String.t(), Map.t(), Keyword.t()) :: {:error, :operation_failed} | {:error, :not_found} | {:error, :conflict} | {:error, any()}
+ @spec put(String.t(), Map.t(), Keyword.t()) ::
+ {:error, :operation_failed}
+ | {:error, :not_found}
+ | {:error, :conflict}
+ | {:error, any()}
def put(db, doc = %{"_id" => id, "_rev" => _}, params \\ []) do
- {url, headers, options} = prepare_request([db, id], [{"content-type", "application/json"}], params)
+ {url, headers, options} =
+ prepare_request([db, id], [{"content-type", "application/json"}], params)
+
case HTTPoison.put(url, Poison.encode!(doc), headers, options) do
{:ok, %HTTPoison.Response{status_code: ok, body: body}} when ok in [201, 202] ->
body = Poison.decode!(body)
+
if Map.get(body, "ok") do
{:ok, Map.get(body, "id"), Map.get(body, "rev")}
else
{:error, :operation_failed}
end
+
{:ok, %HTTPoison.Response{status_code: 209}} ->
{:error, :conflict}
- error -> handle_generic_response(error)
+
+ error ->
+ handle_generic_response(error)
end
end
@@ -74,22 +103,30 @@ defmodule Couch do
base_url = Keyword.get(config, :url, "http://localhost:5984")
- path = path
- |> Enum.filter(& &1)
- |> Enum.map(&to_string/1)
- |> Enum.map(fn(part) -> part
- |> String.replace("/", "%2F")
- |> String.replace("#", "%23")
- end)
- |> Path.join()
-
- url = base_url
- |> URI.merge(path)
- |> to_string()
-
- headers = headers ++ [{"accept", "application/json"}, {"user-agent", "#{Nola.brand(:name)} v#{Nola.version()}"}]
-
- params = Enum.map(params, fn({k, v}) -> {to_string(k), v} end)
+ path =
+ path
+ |> Enum.filter(& &1)
+ |> Enum.map(&to_string/1)
+ |> Enum.map(fn part ->
+ part
+ |> String.replace("/", "%2F")
+ |> String.replace("#", "%23")
+ end)
+ |> Path.join()
+
+ url =
+ base_url
+ |> URI.merge(path)
+ |> to_string()
+
+ headers =
+ headers ++
+ [
+ {"accept", "application/json"},
+ {"user-agent", "#{Nola.brand(:name)} v#{Nola.version()}"}
+ ]
+
+ params = Enum.map(params, fn {k, v} -> {to_string(k), v} end)
client_options = Keyword.get(config, :client_options, [])
options = [params: params] ++ options ++ client_options
@@ -102,7 +139,8 @@ defmodule Couch do
{url, headers, options}
end
- defp handle_generic_response({:ok, %HTTPoison.Response{status_code: code}}), do: {:error, Plug.Conn.Status.reason_atom(code)}
- defp handle_generic_response({:error, %HTTPoison.Error{reason: reason}}), do: {:error, reason}
+ defp handle_generic_response({:ok, %HTTPoison.Response{status_code: code}}),
+ do: {:error, Plug.Conn.Status.reason_atom(code)}
+ defp handle_generic_response({:error, %HTTPoison.Error{reason: reason}}), do: {:error, reason}
end
diff --git a/lib/irc.ex b/lib/irc.ex
index 2c7a468..26e3843 100644
--- a/lib/irc.ex
+++ b/lib/irc.ex
@@ -6,6 +6,7 @@ defmodule Nola.Irc do
def send_message_as(account, network, channel, text, force_puppet \\ false, meta \\ []) do
connection = Nola.Irc.Connection.get_network(network)
+
if connection && (force_puppet || Nola.Irc.PuppetConnection.whereis(account, connection)) do
Nola.Irc.PuppetConnection.start_and_send_message(account, connection, channel, text, meta)
else
@@ -21,7 +22,7 @@ defmodule Nola.Irc do
for {n, u, h} <- Nola.Irc.env(:admins, []) do
admin_part_match?(n, nick) && admin_part_match?(u, user) && admin_part_match?(h, host)
end
- |> Enum.any?
+ |> Enum.any?()
end
defp admin_part_match?(:_, _), do: true
@@ -34,9 +35,13 @@ defmodule Nola.Irc do
Nola.Irc.Connection.setup()
[
- worker(Registry, [[keys: :duplicate, name: Nola.Irc.ConnectionPubSub]], id: :registr_irc_conn),
- supervisor(Nola.Irc.Connection.Supervisor, [], [name: Nola.Irc.Connection.Supervisor]),
- supervisor(Nola.Irc.PuppetConnection.Supervisor, [], [name: Nola.Irc.PuppetConnection.Supervisor]),
+ worker(Registry, [[keys: :duplicate, name: Nola.Irc.ConnectionPubSub]],
+ id: :registr_irc_conn
+ ),
+ supervisor(Nola.Irc.Connection.Supervisor, [], name: Nola.Irc.Connection.Supervisor),
+ supervisor(Nola.Irc.PuppetConnection.Supervisor, [],
+ name: Nola.Irc.PuppetConnection.Supervisor
+ )
]
end
@@ -45,5 +50,4 @@ defmodule Nola.Irc do
Logger.info("Starting connections")
Nola.Irc.Connection.start_all()
end
-
end
diff --git a/lib/irc/admin_handler.ex b/lib/irc/admin_handler.ex
index cb953db..e4834fa 100644
--- a/lib/irc/admin_handler.ex
+++ b/lib/irc/admin_handler.ex
@@ -13,17 +13,22 @@ defmodule Nola.Irc.AdminHandler do
end
def init([client]) do
- ExIRC.Client.add_handler client, self
+ ExIRC.Client.add_handler(client, self())
{:ok, _} = Registry.register(Nola.PubSub, "op", [])
{:ok, client}
end
- def handle_info({:irc, :trigger, "op", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang}, sender: sender}}, client) do
+ def handle_info(
+ {:irc, :trigger, "op",
+ m = %Nola.Message{trigger: %Nola.Trigger{type: :bang}, sender: sender}},
+ client
+ ) do
if Nola.Irc.admin?(sender) do
m.replyfun.({:mode, "+o"})
else
m.replyfun.({:kick, "non"})
end
+
{:noreply, client}
end
@@ -31,11 +36,11 @@ defmodule Nola.Irc.AdminHandler do
if Nola.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/irc/connection.ex b/lib/irc/connection.ex
index 87da466..4de76cd 100644
--- a/lib/irc/connection.ex
+++ b/lib/irc/connection.ex
@@ -32,18 +32,18 @@ defmodule Nola.Irc.Connection do
def irc_doc, do: nil
@min_backoff :timer.seconds(5)
- @max_backoff :timer.seconds(2*60)
+ @max_backoff :timer.seconds(2 * 60)
embedded_schema do
- field :network, :string
- field :host, :string
- field :port, :integer
- field :nick, :string
- field :user, :string
- field :name, :string
- field :pass, :string
- field :tls, :boolean, default: false
- field :channels, {:array, :string}, default: []
+ field(:network, :string)
+ field(:host, :string)
+ field(:port, :integer)
+ field(:nick, :string)
+ field(:user, :string)
+ field(:name, :string)
+ field(:pass, :string)
+ field(:tls, :boolean, default: false)
+ field(:channels, {:array, :string}, default: [])
end
defmodule Supervisor do
@@ -54,7 +54,12 @@ defmodule Nola.Irc.Connection do
end
def start_child(%Nola.Irc.Connection{} = conn) do
- spec = %{id: conn.id, start: {Nola.Irc.Connection, :start_link, [conn]}, restart: :transient}
+ spec = %{
+ id: conn.id,
+ start: {Nola.Irc.Connection, :start_link, [conn]},
+ restart: :transient
+ }
+
DynamicSupervisor.start_child(__MODULE__, spec)
end
@@ -68,7 +73,6 @@ defmodule Nola.Irc.Connection do
end
end
-
def changeset(params) do
import Ecto.Changeset
@@ -79,11 +83,23 @@ defmodule Nola.Irc.Connection do
end
def to_tuple(%__MODULE__{} = conn) do
- {conn.id, conn.network, conn.host, conn.port, conn.nick, conn.user, conn.name, conn.pass, conn.tls, conn.channels, nil}
+ {conn.id, conn.network, conn.host, conn.port, conn.nick, conn.user, conn.name, conn.pass,
+ conn.tls, conn.channels, nil}
end
def from_tuple({id, network, host, port, nick, user, name, pass, tls, channels, _}) do
- %__MODULE__{id: id, network: network, host: host, port: port, nick: nick, user: user, name: name, pass: pass, tls: tls, channels: channels}
+ %__MODULE__{
+ id: id,
+ network: network,
+ host: host,
+ port: port,
+ nick: nick,
+ user: user,
+ name: name,
+ pass: pass,
+ tls: tls,
+ channels: channels
+ }
end
## -- MANAGER API
@@ -102,7 +118,7 @@ defmodule Nola.Irc.Connection do
end
def connections() do
- :dets.foldl(fn(object, acc) -> [from_tuple(object) | acc] end, [], dets())
+ :dets.foldl(fn object, acc -> [from_tuple(object) | acc] end, [], dets())
end
def start_all() do
@@ -110,23 +126,29 @@ defmodule Nola.Irc.Connection do
end
def get_network(network, channel \\ nil) do
- spec = [{{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
- [{:==, :"$1", {:const, network}}], [:"$_"]}]
- results = Enum.map(:dets.select(dets(), spec), fn(object) -> from_tuple(object) end)
+ spec = [
+ {{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, network}}],
+ [:"$_"]}
+ ]
+
+ results = Enum.map(:dets.select(dets(), spec), fn object -> from_tuple(object) end)
+
if channel do
- Enum.find(results, fn(conn) -> Enum.member?(conn.channels, channel) end)
+ Enum.find(results, fn conn -> Enum.member?(conn.channels, channel) end)
else
List.first(results)
end
end
def get_host_nick(host, port, nick) do
- spec = [{{:_, :_, :"$1", :"$2", :"$3", :_, :_, :_, :_, :_, :_},
- [{:andalso,
- {:andalso, {:==, :"$1", {:const, host}}, {:==, :"$2", {:const, port}}},
- {:==, :"$3", {:const, nick}}}],
- [:"$_"]}
+ spec = [
+ {{:_, :_, :"$1", :"$2", :"$3", :_, :_, :_, :_, :_, :_},
+ [
+ {:andalso, {:andalso, {:==, :"$1", {:const, host}}, {:==, :"$2", {:const, port}}},
+ {:==, :"$3", {:const, nick}}}
+ ], [:"$_"]}
]
+
case :dets.select(dets(), spec) do
[object] -> from_tuple(object)
[] -> nil
@@ -147,7 +169,9 @@ defmodule Nola.Irc.Connection do
case :global.whereis_name(id) do
pid when is_pid(pid) ->
GenServer.stop(pid, :normal)
- _ -> :error
+
+ _ ->
+ :error
end
end
@@ -160,7 +184,9 @@ defmodule Nola.Irc.Connection do
:dets.insert(dets(), to_tuple(conn))
Nola.Irc.Connection.Supervisor.start_child(conn)
end
- error -> error
+
+ error ->
+ error
end
end
@@ -175,11 +201,12 @@ defmodule Nola.Irc.Connection do
def broadcast_message(net, chan, message, meta \\ []) do
dispatch("conn", {:broadcast, net, chan, message}, meta, Nola.Irc.ConnectionPubSub)
end
- #def broadcast_message(list, message, meta \\ []) when is_list(list) do
+
+ # def broadcast_message(list, message, meta \\ []) when is_list(list) do
# for {net, chan} <- list do
# broadcast_message(net, chan, message, meta)
# end
- #send
+ # send
def privmsg(channel, line) do
GenServer.cast(__MODULE__, {:privmsg, channel, line})
@@ -187,13 +214,22 @@ defmodule Nola.Irc.Connection do
def init([conn]) do
Logger.metadata(conn: conn.id)
- backoff = :backoff.init(@min_backoff, @max_backoff)
- |> :backoff.type(:jitter)
- {:ok, %{client: nil,
- ignore_channel_for_a_while: conn.network == "snoonet",
- backoff: backoff,
- channels: Map.new,
- conn: conn, connected_server: nil, connected_port: nil, network: conn.network}, {:continue, :connect}}
+
+ backoff =
+ :backoff.init(@min_backoff, @max_backoff)
+ |> :backoff.type(:jitter)
+
+ {:ok,
+ %{
+ client: nil,
+ ignore_channel_for_a_while: conn.network == "snoonet",
+ backoff: backoff,
+ channels: Map.new(),
+ conn: conn,
+ connected_server: nil,
+ connected_port: nil,
+ network: conn.network
+ }, {:continue, :connect}}
end
@triggers %{
@@ -214,31 +250,50 @@ defmodule Nola.Irc.Connection do
}
def handle_continue(:connect, state) do
- client_opts = []
- |> Keyword.put(:network, state.conn.network)
+ client_opts =
+ []
+ |> Keyword.put(:network, state.conn.network)
+
{:ok, _} = Registry.register(Nola.Irc.ConnectionPubSub, "conn", [])
- client = if state.client && Process.alive?(state.client) do
- Logger.info("Reconnecting client")
- state.client
- else
- Logger.info("Connecting")
- {:ok, client} = ExIRC.Client.start_link(debug: false)
- ExIRC.Client.add_handler(client, self())
- client
- end
+
+ client =
+ if state.client && Process.alive?(state.client) do
+ Logger.info("Reconnecting client")
+ state.client
+ else
+ Logger.info("Connecting")
+ {:ok, client} = ExIRC.Client.start_link(debug: false)
+ ExIRC.Client.add_handler(client, self())
+ client
+ end
opts = [{:nodelay, true}]
- conn_fun = if state.conn.tls, do: :connect_ssl!, else: :connect!
- apply(ExIRC.Client, conn_fun, [client, to_charlist(state.conn.host), state.conn.port, opts])
+
+ {conn_fun, opts2} =
+ if state.conn.tls,
+ do: {:connect_ssl!, [{:cacerts, :certifi.cacerts()}]},
+ else: {:connect!, []}
+
+ conn_result =
+ apply(ExIRC.Client, conn_fun, [
+ client,
+ to_charlist(state.conn.host),
+ state.conn.port,
+ opts ++ opts2
+ ])
+
+ Logger.warn(
+ "Connecting to #{state.conn.host}:#{state.conn.port} using #{conn_fun} => #{inspect(conn_result)}"
+ )
{:noreply, %{state | client: client}}
end
def handle_info(:disconnected, state) do
{delay, backoff} = :backoff.fail(state.backoff)
- Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect delay}ms")
+ Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect(delay)}ms")
Process.send_after(self(), :connect, delay)
- {:noreply, %{state | channels: Map.new, backoff: backoff}}
+ {:noreply, %{state | channels: Map.new(), backoff: backoff}}
end
def handle_info(:connect, state) do
@@ -249,13 +304,29 @@ defmodule Nola.Irc.Connection do
irc_reply(state, {channel, nil}, line)
{:noreply, state}
end
-<<>>
+
+ <<>>
# Connection successful
def handle_info({:connected, server, port}, state) do
- Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect state}")
+ Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect(state)}")
{_, backoff} = :backoff.succeed(state.backoff)
- ExIRC.Client.logon(state.client, state.conn.pass || "", state.conn.nick, state.conn.user, state.conn.name)
- {:noreply, %{state | channels: Map.new, backoff: backoff, connected_server: server, connected_port: port}}
+
+ ExIRC.Client.logon(
+ state.client,
+ state.conn.pass || "",
+ state.conn.nick,
+ state.conn.user,
+ state.conn.name
+ )
+
+ {:noreply,
+ %{
+ state
+ | channels: Map.new(),
+ backoff: backoff,
+ connected_server: server,
+ connected_port: port
+ }}
end
# Logon successful
@@ -269,9 +340,11 @@ defmodule Nola.Irc.Connection do
# ISUP
def handle_info({:isup, network}, state) when is_binary(network) do
Nola.UserTrack.clear_network(state.network)
+
if network != state.network do
Logger.warn("Possibly misconfigured network: #{network} != #{state.network}")
end
+
{:noreply, state}
end
@@ -283,49 +356,88 @@ defmodule Nola.Irc.Connection do
# Received something in a channel
def handle_info({:received, text, sender, chan}, state) do
- channel_state = Map.get(state.channels, chan, Map.new)
- if state.ignore_channel_for_a_while && DateTime.diff(DateTime.utc_now(), Map.get(channel_state, :joined)) < 30 do
+ channel_state = Map.get(state.channels, chan, Map.new())
+
+ if state.ignore_channel_for_a_while &&
+ DateTime.diff(DateTime.utc_now(), Map.get(channel_state, :joined)) < 30 do
:ignore_channel
else
- user = if user = Nola.UserTrack.find_by_nick(state.network, sender.nick) do
- user
- else
- Logger.error("Could not lookup user for message: #{inspect {state.network, chan, sender.nick}}")
- user = Nola.UserTrack.joined(chan, sender, [])
- ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user..
- user
- end
+ user =
+ if user = Nola.UserTrack.find_by_nick(state.network, sender.nick) do
+ user
+ else
+ Logger.error(
+ "Could not lookup user for message: #{inspect({state.network, chan, sender.nick})}"
+ )
+
+ user = Nola.UserTrack.joined(chan, sender, [])
+ # Rewho everything in case of need ? We shouldn't not know that user..
+ ExIRC.Client.who(state.client, chan)
+ user
+ end
+
if !user do
- ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user..
- Logger.error("Could not lookup user nor create it for message: #{inspect {state.network, chan, sender.nick}}")
+ # Rewho everything in case of need ? We shouldn't not know that user..
+ ExIRC.Client.who(state.client, chan)
+
+ Logger.error(
+ "Could not lookup user nor create it for message: #{inspect({state.network, chan, sender.nick})}"
+ )
else
if !Map.get(user.options, :puppet) do
- reply_fun = fn(text) -> irc_reply(state, {chan, sender}, text) end
+ reply_fun = fn text -> irc_reply(state, {chan, sender}, text) end
account = Nola.Account.lookup(sender)
- message = %Nola.Message{id: FlakeId.get(), transport: :irc, at: NaiveDateTime.utc_now(), text: text, network: state.network,
- account: account, sender: sender, channel: chan, replyfun: reply_fun,
- trigger: extract_trigger(text)}
- message = case Nola.UserTrack.messaged(message) do
- :ok -> message
- {:ok, message} -> message
- end
+
+ message = %Nola.Message{
+ id: FlakeId.get(),
+ transport: :irc,
+ at: NaiveDateTime.utc_now(),
+ text: text,
+ network: state.network,
+ account: account,
+ sender: sender,
+ channel: chan,
+ replyfun: reply_fun,
+ trigger: extract_trigger(text)
+ }
+
+ message =
+ case Nola.UserTrack.messaged(message) do
+ :ok -> message
+ {:ok, message} -> message
+ end
+
publish(message, ["#{message.network}/#{chan}:messages"])
end
end
end
+
{:noreply, state}
end
# Received a private message
def handle_info({:received, text, sender}, state) do
- reply_fun = fn(text) -> irc_reply(state, {sender.nick, sender}, text) end
+ reply_fun = fn text -> irc_reply(state, {sender.nick, sender}, text) end
account = Nola.Account.lookup(sender)
- message = %Nola.Message{id: FlakeId.get(), transport: :irc, text: text, network: state.network, at: NaiveDateTime.utc_now(),
- account: account, sender: sender, replyfun: reply_fun, trigger: extract_trigger(text)}
- message = case Nola.UserTrack.messaged(message) do
- :ok -> message
- {:ok, message} -> message
- end
+
+ message = %Nola.Message{
+ id: FlakeId.get(),
+ transport: :irc,
+ text: text,
+ network: state.network,
+ at: NaiveDateTime.utc_now(),
+ account: account,
+ sender: sender,
+ replyfun: reply_fun,
+ trigger: extract_trigger(text)
+ }
+
+ message =
+ case Nola.UserTrack.messaged(message) do
+ :ok -> message
+ {:ok, message} -> message
+ end
+
publish(message, ["messages:private", "#{message.network}/#{account.id}:messages"])
{:noreply, state}
end
@@ -334,38 +446,45 @@ defmodule Nola.Irc.Connection do
def handle_info({:broadcast, net, account = %Nola.Account{}, message}, state) do
if net == state.conn.network do
user = Nola.UserTrack.find_by_account(net, account)
+
if user do
irc_reply(state, {user.nick, nil}, message)
end
end
+
{:noreply, state}
end
+
def handle_info({:broadcast, net, chan, message}, state) do
if net == state.conn.network && Enum.member?(state.conn.channels, chan) do
irc_reply(state, {chan, nil}, message)
end
+
{:noreply, state}
end
## -- UserTrack
def handle_info({:joined, channel}, state) do
- channels = Map.put(state.channels, channel, %{joined: DateTime.utc_now})
+ channels = Map.put(state.channels, channel, %{joined: DateTime.utc_now()})
ExIRC.Client.who(state.client, channel)
{:noreply, %{state | channels: channels}}
end
def handle_info({:who, channel, whos}, state) do
- accounts = Enum.map(whos, fn(who = %ExIRC.Who{nick: nick, operator?: operator}) ->
- priv = if operator, do: [:operator], else: []
- # Don't touch -- on WHO the bot joined, not the users.
- Nola.UserTrack.joined(channel, who, priv, false)
- account = Nola.Account.lookup(who)
- if account do
- {:account, who.network, channel, who.nick, account.id}
- end
- end)
- |> Enum.filter(fn(x) -> x end)
+ accounts =
+ Enum.map(whos, fn who = %ExIRC.Who{nick: nick, operator?: operator} ->
+ priv = if operator, do: [:operator], else: []
+ # Don't touch -- on WHO the bot joined, not the users.
+ Nola.UserTrack.joined(channel, who, priv, false)
+ account = Nola.Account.lookup(who)
+
+ if account do
+ {:account, who.network, channel, who.nick, account.id}
+ end
+ end)
+ |> Enum.filter(fn x -> x end)
+
dispatch("account", {:accounts, accounts})
{:noreply, state}
end
@@ -378,9 +497,11 @@ defmodule Nola.Irc.Connection do
def handle_info({:joined, channel, sender}, state) do
Nola.UserTrack.joined(channel, sender, [])
account = Nola.Account.lookup(sender)
+
if account do
dispatch("account", {:account, sender.network, channel, sender.nick, account.id})
end
+
{:noreply, state}
end
@@ -405,7 +526,7 @@ defmodule Nola.Irc.Connection do
end
def handle_info(unhandled, client) do
- Logger.debug("unhandled: #{inspect unhandled}")
+ Logger.debug("unhandled: #{inspect(unhandled)}")
{:noreply, client}
end
@@ -416,34 +537,50 @@ defmodule Nola.Irc.Connection do
end
def publish(m = %Nola.Message{trigger: t = %Nola.Trigger{trigger: trigger}}, keys) do
- dispatch(["triggers", "#{m.network}/#{m.channel}:triggers", "trigger:"<>trigger], {:irc, :trigger, trigger, m}, Enum.into(m.meta, []))
+ dispatch(
+ ["triggers", "#{m.network}/#{m.channel}:triggers", "trigger:" <> trigger],
+ {:irc, :trigger, trigger, m},
+ Enum.into(m.meta, [])
+ )
end
def publish_event(net, event = %{type: _}) when is_binary(net) do
- event = event
- |> Map.put(:at, NaiveDateTime.utc_now())
- |> Map.put(:network, net)
+ event =
+ event
+ |> Map.put(:at, NaiveDateTime.utc_now())
+ |> Map.put(:network, net)
+
dispatch("#{net}:events", {:irc, :event, event})
end
+
def publish_event({net, chan}, event = %{type: type}) do
- event = event
- |> Map.put(:at, NaiveDateTime.utc_now())
- |> Map.put(:network, net)
- |> Map.put(:channel, chan)
+ event =
+ event
+ |> Map.put(:at, NaiveDateTime.utc_now())
+ |> Map.put(:network, net)
+ |> Map.put(:channel, chan)
+
dispatch("#{net}/#{chan}:events", {:irc, :event, event})
end
def dispatch(keys, content, meta \\ [], sub \\ Nola.PubSub)
- def dispatch(key, content, meta, sub) when is_binary(key), do: dispatch([key], content, meta, sub)
+ def dispatch(key, content, meta, sub) when is_binary(key),
+ do: dispatch([key], content, meta, sub)
+
def dispatch(keys, content, meta, sub) when is_list(keys) do
- Logger.debug("dispatching: #{inspect keys} #{inspect content}")
+ Logger.debug("dispatching: #{inspect(keys)} #{inspect(content)}")
should_dispatch_data = should_dispatch_data(content)
origin = Keyword.get(meta, :origin, :no_origin)
+
for key <- keys do
- spawn(fn() ->
+ spawn(fn ->
Registry.dispatch(sub, key, fn h ->
- for {pid, reg} <- h, do: if(should_dispatch?(key, origin, should_dispatch_data, reg), do: send(pid, content))
+ for {pid, reg} <- h,
+ do:
+ if(should_dispatch?(key, origin, should_dispatch_data, reg),
+ do: send(pid, content)
+ )
end)
end)
end
@@ -452,21 +589,26 @@ defmodule Nola.Irc.Connection do
defp should_dispatch_data({:irc, :trigger, _, msg}) do
should_dispatch_data(msg)
end
+
defp should_dispatch_data({:irc, :text, msg}) do
should_dispatch_data(msg)
end
+
defp should_dispatch_data(%Nola.Message{network: network, channel: channel}) do
Application.get_env(:nola, :channel_pubsub_plugin_ignore_list, %{})
|> Map.get("#{network}/#{channel}", [])
end
+
defp should_dispatch_data(_) do
[]
end
def should_dispatch?(_, _, [], _), do: true
+
def should_dispatch?(key, origin, data, reg) do
if plugin = Keyword.get(reg, :plugin) do
- plugin != origin || !Enum.member?(data, plugin)
+ # plugin != origin && !Enum.member?(data, plugin)
+ !Enum.member?(data, plugin)
else
true
end
@@ -479,7 +621,7 @@ defmodule Nola.Irc.Connection do
def triggers, do: @triggers
for {trigger, name} <- @triggers do
- def extract_trigger(unquote(trigger)<>text) do
+ def extract_trigger(unquote(trigger) <> text) do
text = String.strip(text)
[trigger | args] = String.split(text, " ")
%Nola.Trigger{type: unquote(name), trigger: String.downcase(trigger), args: args}
@@ -494,15 +636,30 @@ defmodule Nola.Irc.Connection do
# irc_reply(ExIRC.Client pid, {channel or nick, ExIRC.Sender}, binary | replies
# replies :: {:kick, reason} | {:kick, nick, reason} | {:mode, mode, nick}
- defp irc_reply(state = %{client: client, network: network}, {target, _}, text) when is_binary(text) or is_list(text) do
- lines = Nola.Irc.Message.splitlong(text)
- |> Enum.map(fn(x) -> if(is_list(x), do: x, else: String.split(x, "\n")) end)
- |> List.flatten()
- outputs = for line <- lines do
- ExIRC.Client.msg(client, :privmsg, target, line)
- {:irc, :out, %Nola.Message{id: FlakeId.get(), transport: :irc, network: network,
- channel: target, text: line, sender: %ExIRC.SenderInfo{nick: state.conn.nick}, at: NaiveDateTime.utc_now(), meta: %{self: true}}}
- end
+ defp irc_reply(state = %{client: client, network: network}, {target, _}, text)
+ when is_binary(text) or is_list(text) do
+ lines =
+ Nola.Irc.Message.splitlong(text)
+ |> Enum.map(fn x -> if(is_list(x), do: x, else: String.split(x, "\n")) end)
+ |> List.flatten()
+
+ outputs =
+ for line <- lines do
+ ExIRC.Client.msg(client, :privmsg, target, line)
+
+ {:irc, :out,
+ %Nola.Message{
+ id: FlakeId.get(),
+ transport: :irc,
+ network: network,
+ channel: target,
+ text: line,
+ sender: %ExIRC.SenderInfo{nick: state.conn.nick},
+ at: NaiveDateTime.utc_now(),
+ meta: %{self: true}
+ }}
+ end
+
for f <- outputs, do: dispatch(["irc:outputs", "#{network}/#{target}:outputs"], f)
end
@@ -547,12 +704,11 @@ defmodule Nola.Irc.Connection do
end
defp track_mode(network, channel, nick, mode) do
- Logger.warn("Unhandled track_mode: #{inspect {nick, mode}}")
+ Logger.warn("Unhandled track_mode: #{inspect({nick, mode})}")
:ok
end
defp server(%{conn: %{host: host, port: port}}) do
host <> ":" <> to_string(port)
end
-
end
diff --git a/lib/irc/message.ex b/lib/irc/message.ex
index 3927079..4b61d36 100644
--- a/lib/irc/message.ex
+++ b/lib/irc/message.ex
@@ -1,28 +1,27 @@
defmodule Nola.Irc.Message do
-
@max_chars 440
def splitlong(string, max_chars \\ 440)
def splitlong(string, max_chars) when is_list(string) do
- Enum.map(string, fn(s) -> splitlong(s, max_chars) end)
+ Enum.map(string, fn s -> splitlong(s, max_chars) end)
|> List.flatten()
end
def splitlong(string, max_chars) do
string
- |> String.codepoints
+ |> String.codepoints()
|> Enum.chunk_every(max_chars)
|> Enum.map(&Enum.join/1)
end
def splitlong_with_prefix(string, prefix, max_chars \\ 440) do
prefix = "#{prefix} "
- max_chars = max_chars - (length(String.codepoints(prefix)))
+ max_chars = max_chars - length(String.codepoints(prefix))
+
string
- |> String.codepoints
+ |> String.codepoints()
|> Enum.chunk_every(max_chars)
- |> Enum.map(fn(line) -> prefix <> Enum.join(line) end)
+ |> Enum.map(fn line -> prefix <> Enum.join(line) end)
end
-
end
diff --git a/lib/irc/puppet_connection.ex b/lib/irc/puppet_connection.ex
index 51b4c8a..d124568 100644
--- a/lib/irc/puppet_connection.ex
+++ b/lib/irc/puppet_connection.ex
@@ -1,9 +1,9 @@
defmodule Nola.Irc.PuppetConnection do
require Logger
@min_backoff :timer.seconds(5)
- @max_backoff :timer.seconds(2*60)
+ @max_backoff :timer.seconds(2 * 60)
@max_idle :timer.hours(12)
- @env Mix.env
+ @env Mix.env()
defmodule Supervisor do
use DynamicSupervisor
@@ -13,7 +13,12 @@ defmodule Nola.Irc.PuppetConnection do
end
def start_child(%Nola.Account{id: account_id}, %Nola.Irc.Connection{id: connection_id}) do
- spec = %{id: {account_id, connection_id}, start: {Nola.Irc.PuppetConnection, :start_link, [account_id, connection_id]}, restart: :transient}
+ spec = %{
+ id: {account_id, connection_id},
+ start: {Nola.Irc.PuppetConnection, :start_link, [account_id, connection_id]},
+ restart: :transient
+ }
+
DynamicSupervisor.start_child(__MODULE__, spec)
end
@@ -27,29 +32,48 @@ defmodule Nola.Irc.PuppetConnection do
end
end
- def whereis(account = %Nola.Account{id: account_id}, connection = %Nola.Irc.Connection{id: connection_id}) do
+ def whereis(
+ account = %Nola.Account{id: account_id},
+ connection = %Nola.Irc.Connection{id: connection_id}
+ ) do
{:global, name} = name(account_id, connection_id)
+
case :global.whereis_name(name) do
:undefined -> nil
pid -> pid
end
end
- def send_message(account = %Nola.Account{id: account_id}, connection = %Nola.Irc.Connection{id: connection_id}, channel, text, meta) do
+ def send_message(
+ account = %Nola.Account{id: account_id},
+ connection = %Nola.Irc.Connection{id: connection_id},
+ channel,
+ text,
+ meta
+ ) do
GenServer.cast(name(account_id, connection_id), {:send_message, self(), channel, text, meta})
end
- def start_and_send_message(account = %Nola.Account{id: account_id}, connection = %Nola.Irc.Connection{id: connection_id}, channel, text, meta) do
+ def start_and_send_message(
+ account = %Nola.Account{id: account_id},
+ connection = %Nola.Irc.Connection{id: connection_id},
+ channel,
+ text,
+ meta
+ ) do
{:global, name} = name(account_id, connection_id)
pid = whereis(account, connection)
- pid = if !pid do
+
+ pid =
+ if !pid do
case Nola.Irc.PuppetConnection.Supervisor.start_child(account, connection) do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
end
- else
- pid
- end
+ else
+ pid
+ end
+
GenServer.cast(pid, {:send_message, self(), channel, text, meta})
end
@@ -58,7 +82,9 @@ defmodule Nola.Irc.PuppetConnection do
end
def start_link(account_id, connection_id) do
- GenServer.start_link(__MODULE__, [account_id, connection_id], name: name(account_id, connection_id))
+ GenServer.start_link(__MODULE__, [account_id, connection_id],
+ name: name(account_id, connection_id)
+ )
end
def name(account_id, connection_id) do
@@ -69,40 +95,61 @@ defmodule Nola.Irc.PuppetConnection do
account = %Nola.Account{} = Nola.Account.get(account_id)
connection = %Nola.Irc.Connection{} = Nola.Irc.Connection.lookup(connection_id)
Logger.metadata(puppet_conn: account.id <> "@" <> connection.id)
- backoff = :backoff.init(@min_backoff, @max_backoff)
- |> :backoff.type(:jitter)
- idle = :erlang.send_after(@max_idle, self, :idle)
- {:ok, %{client: nil, backoff: backoff, idle: idle, connected: false, buffer: [], channels: [], connection_id: connection_id, account_id: account_id, connected_server: nil, connected_port: nil, network: connection.network}, {:continue, :connect}}
+
+ backoff =
+ :backoff.init(@min_backoff, @max_backoff)
+ |> :backoff.type(:jitter)
+
+ idle = :erlang.send_after(@max_idle, self(), :idle)
+
+ {:ok,
+ %{
+ client: nil,
+ backoff: backoff,
+ idle: idle,
+ connected: false,
+ buffer: [],
+ channels: [],
+ connection_id: connection_id,
+ account_id: account_id,
+ connected_server: nil,
+ connected_port: nil,
+ network: connection.network
+ }, {:continue, :connect}}
end
def handle_continue(:connect, state) do
- #ipv6 = if @env == :prod do
+ # ipv6 = if @env == :prod do
# subnet = Nola.Subnet.assign(state.account_id)
# Nola.Account.put_meta(Nola.Account.get(state.account_id), "subnet", subnet)
# ip = Pfx.host(subnet, 1)
# {:ok, ipv6} = :inet_parse.ipv6_address(to_charlist(ip))
# System.cmd("add-ip6", [ip])
# ipv6
- #end
+ # end
conn = Nola.Irc.Connection.lookup(state.connection_id)
- client_opts = []
- |> Keyword.put(:network, conn.network)
- client = if state.client && Process.alive?(state.client) do
- Logger.info("Reconnecting client")
- state.client
- else
- Logger.info("Connecting")
- {:ok, client} = ExIRC.Client.start_link(debug: false)
- ExIRC.Client.add_handler(client, self())
- client
- end
+
+ client_opts =
+ []
+ |> Keyword.put(:network, conn.network)
+
+ client =
+ if state.client && Process.alive?(state.client) do
+ Logger.info("Reconnecting client")
+ state.client
+ else
+ Logger.info("Connecting")
+ {:ok, client} = ExIRC.Client.start_link(debug: false)
+ ExIRC.Client.add_handler(client, self())
+ client
+ end
base_opts = [
{:nodelay, true}
]
- #{ip, opts} = case {ipv6, :inet_res.resolve(to_charlist(conn.host), :in, :aaaa)} do
+ # {ip, opts} = case {ipv6, :inet_res.resolve(to_charlist(conn.host), :in, :aaaa)} do
# {ipv6, {:ok, {:dns_rec, _dns_header, _query, rrs = [{:dns_rr, _, _, _, _, _, _, _, _, _} | _], _, _}}} ->
# ip = rrs
# |> Enum.map(fn({:dns_rr, _, :aaaa, :in, _, _, ipv6, _, _, _}) -> ipv6 end)
@@ -115,56 +162,90 @@ defmodule Nola.Irc.PuppetConnection do
# ]
# {ip, opts}
# _ ->
- {ip, opts} = {to_charlist(conn.host), []}
- #end
+ {ip, opts} = {to_charlist(conn.host), []}
+ # end
+
+ {conn_fun, opts2} =
+ if state.conn.tls,
+ do: {:connect_ssl!, [{:cacerts, :certifi.cacerts()}]},
+ else: {:connect!, []}
- conn_fun = if conn.tls, do: :connect_ssl!, else: :connect!
- apply(ExIRC.Client, conn_fun, [client, ip, conn.port, base_opts ++ opts])
+ apply(ExIRC.Client, conn_fun, [client, ip, conn.port, base_opts ++ opts ++ opts2])
{:noreply, %{state | client: client}}
end
def handle_continue(:connected, state) do
- state = Enum.reduce(Enum.reverse(state.buffer), state, fn(b, state) ->
- {:noreply, state} = handle_cast(b, state)
- state
- end)
+ state =
+ Enum.reduce(Enum.reverse(state.buffer), state, fn b, state ->
+ {:noreply, state} = handle_cast(b, state)
+ state
+ end)
+
{:noreply, %{state | buffer: []}}
end
- def handle_cast(cast = {:send_message, _pid, _channel, _text, _meta}, state = %{connected: false, buffer: buffer}) do
+ def handle_cast(
+ cast = {:send_message, _pid, _channel, _text, _meta},
+ state = %{connected: false, buffer: buffer}
+ ) do
{:noreply, %{state | buffer: [cast | buffer]}}
end
def handle_cast({:send_message, pid, channel, text, pub_meta}, state = %{connected: true}) do
- channels = if !Enum.member?(state.channels, channel) do
- ExIRC.Client.join(state.client, channel)
- [channel | state.channels]
- else
- state.channels
- end
+ channels =
+ if !Enum.member?(state.channels, channel) do
+ ExIRC.Client.join(state.client, channel)
+ [channel | state.channels]
+ else
+ state.channels
+ end
+
ExIRC.Client.msg(state.client, :privmsg, channel, text)
meta = %{puppet: true, from: pid, origin: Keyword.get(pub_meta, :origin, __MODULE__)}
account = Nola.Account.get(state.account_id)
nick = make_nick(state)
- sender = %ExIRC.SenderInfo{network: state.network, nick: suffix_nick(nick), user: nick, host: "puppet."}
- reply_fun = fn(text) ->
+
+ sender = %ExIRC.SenderInfo{
+ network: state.network,
+ nick: suffix_nick(nick),
+ user: nick,
+ host: "puppet."
+ }
+
+ reply_fun = fn text ->
Nola.Irc.Connection.broadcast_message(state.network, channel, text, pub_meta)
end
- message = %Nola.Message{id: FlakeId.get(), at: NaiveDateTime.utc_now(), text: text, network: state.network, account: account, sender: sender, channel: channel, replyfun: reply_fun, trigger: Nola.Irc.Connection.extract_trigger(text), meta: meta}
- message = case Nola.UserTrack.messaged(message) do
- :ok -> message
- {:ok, message} -> message
- end
+
+ message = %Nola.Message{
+ id: FlakeId.get(),
+ at: NaiveDateTime.utc_now(),
+ text: text,
+ network: state.network,
+ account: account,
+ sender: sender,
+ channel: channel,
+ replyfun: reply_fun,
+ trigger: Nola.Irc.Connection.extract_trigger(text),
+ meta: meta
+ }
+
+ message =
+ case Nola.UserTrack.messaged(message) do
+ :ok -> message
+ {:ok, message} -> message
+ end
+
Nola.Irc.Connection.publish(message, ["#{message.network}/#{channel}:messages"])
- idle = if length(state.buffer) == 0 do
- :erlang.cancel_timer(state.idle)
- :erlang.send_after(@max_idle, self(), :idle)
- else
- state.idle
- end
+ idle =
+ if length(state.buffer) == 0 do
+ :erlang.cancel_timer(state.idle)
+ :erlang.send_after(@max_idle, self(), :idle)
+ else
+ state.idle
+ end
{:noreply, %{state | idle: idle, channels: channels}}
end
@@ -177,7 +258,7 @@ defmodule Nola.Irc.PuppetConnection do
def handle_info(:disconnected, state) do
{delay, backoff} = :backoff.fail(state.backoff)
- Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect delay}ms")
+ Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect(delay)}ms")
Process.send_after(self(), :connect, delay)
{:noreply, %{state | connected: false, backoff: backoff}}
end
@@ -188,10 +269,18 @@ defmodule Nola.Irc.PuppetConnection do
# Connection successful
def handle_info({:connected, server, port}, state) do
- Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect state}")
+ Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect(state)}")
{_, backoff} = :backoff.succeed(state.backoff)
base_nick = make_nick(state)
- ExIRC.Client.logon(state.client, "", suffix_nick(base_nick), base_nick, "#{base_nick}'s puppet")
+
+ ExIRC.Client.logon(
+ state.client,
+ "",
+ suffix_nick(base_nick),
+ base_nick,
+ "#{base_nick}'s puppet"
+ )
+
{:noreply, %{state | backoff: backoff, connected_server: server, connected_port: port}}
end
@@ -199,8 +288,17 @@ defmodule Nola.Irc.PuppetConnection do
def handle_info(:logged_in, state) do
Logger.info("#{inspect(self())} Logged in")
{_, backoff} = :backoff.succeed(state.backoff)
+
# Create an UserTrack entry for the client so it's authenticated to the right account_id already.
- Nola.UserTrack.connected(state.network, suffix_nick(make_nick(state)), make_nick(state), "puppet.", state.account_id, %{puppet: true})
+ Nola.UserTrack.connected(
+ state.network,
+ suffix_nick(make_nick(state)),
+ make_nick(state),
+ "puppet.",
+ state.account_id,
+ %{puppet: true}
+ )
+
{:noreply, %{state | backoff: backoff}}
end
@@ -222,17 +320,21 @@ defmodule Nola.Irc.PuppetConnection do
account = Nola.Account.get(state.account_id)
user = Nola.UserTrack.find_by_account(state.network, account)
base_nick = if(user, do: user.nick, else: account.name)
- clean_nick = case String.split(base_nick, ":", parts: 2) do
- ["@"<>nick, _] -> nick
- [nick] -> nick
- end
+
+ clean_nick =
+ case String.split(base_nick, ":", parts: 2) do
+ ["@" <> nick, _] -> nick
+ [nick] -> nick
+ end
+
clean_nick
+ |> :unicode.characters_to_nfd_binary()
+ |> String.replace(~r/[^a-z0-9]/, "")
end
- if Mix.env == :dev do
+ if Mix.env() == :dev do
def suffix_nick(nick), do: "#{nick}[d]"
else
def suffix_nick(nick), do: "#{nick}[p]"
end
-
end
diff --git a/lib/matrix.ex b/lib/matrix.ex
index 0ad0836..384f204 100644
--- a/lib/matrix.ex
+++ b/lib/matrix.ex
@@ -5,7 +5,7 @@ defmodule Nola.Matrix do
@behaviour MatrixAppService.Adapter.Room
@behaviour MatrixAppService.Adapter.Transaction
@behaviour MatrixAppService.Adapter.User
- @env Mix.env
+ @env Mix.env()
def dets(part) do
(Nola.data_path() <> "/matrix-#{to_string(part)}.dets") |> String.to_charlist()
@@ -20,12 +20,13 @@ defmodule Nola.Matrix do
def myself?("@_dev:random.sh"), do: true
def myself?("@_bot:random.sh"), do: true
- def myself?("@_dev."<>_), do: true
- def myself?("@_bot."<>_), do: true
+ def myself?("@_dev." <> _), do: true
+ def myself?("@_bot." <> _), do: true
def myself?(_), do: false
- def mxc_to_http(mxc = "mxc://"<>_) do
+ def mxc_to_http(mxc = "mxc://" <> _) do
uri = URI.parse(mxc)
+
%URI{uri | scheme: "https", path: "/_matrix/media/r0/download/#{uri.authority}#{uri.path}"}
|> URI.to_string()
end
@@ -41,6 +42,7 @@ defmodule Nola.Matrix do
initial_device_display_name: "Application Service",
username: if(@env == :dev, do: "_dev.#{id}", else: "_bot.#{id}")
]
+
Logger.debug("Registering user for #{id}")
{:ok, %{"user_id" => mxid}} = Polyjuice.Client.LowLevel.register(client(), opts)
:dets.insert(dets(:users), {id, mxid})
@@ -54,27 +56,31 @@ defmodule Nola.Matrix do
end
end
- def user_name("@"<>name) do
+ def user_name("@" <> name) do
[username, _] = String.split(name, ":", parts: 2)
username
end
def application_childs() do
import Supervisor.Spec
+
[
- supervisor(Nola.Matrix.Room.Supervisor, [], [name: Nola.Irc.PuppetConnection.Supervisor]),
+ supervisor(Nola.Matrix.Room.Supervisor, [], name: Nola.Irc.PuppetConnection.Supervisor)
]
end
def after_start() do
- rooms = :dets.foldl(fn({id, _, _, _}, acc) -> [id | acc] end, [], dets(:rooms))
+ rooms = :dets.foldl(fn {id, _, _, _}, acc -> [id | acc] end, [], dets(:rooms))
for room <- rooms, do: Nola.Matrix.Room.start(room)
end
def lookup_room(room) do
case :dets.lookup(dets(:rooms), room) do
- [{_, network, channel, opts}] -> {:ok, Map.merge(opts, %{network: network, channel: channel})}
- _ -> {:error, :no_such_room}
+ [{_, network, channel, opts}] ->
+ {:ok, Map.merge(opts, %{network: network, channel: channel})}
+
+ _ ->
+ {:error, :no_such_room}
end
end
@@ -93,11 +99,16 @@ defmodule Nola.Matrix do
end
def create_room(room_alias) do
- Logger.debug("Matrix: creating room #{inspect room_alias}")
+ Logger.debug("Matrix: creating room #{inspect(room_alias)}")
localpart = localpart(room_alias)
+
with {:ok, network, channel} <- extract_network_channel_from_localpart(localpart),
%Nola.Irc.Connection{} <- Nola.Irc.Connection.get_network(network, channel),
- room = [visibility: :public, room_alias_name: localpart, name: if(network == "random", do: channel, else: "#{network}/#{channel}")],
+ room = [
+ visibility: :public,
+ room_alias_name: localpart,
+ name: if(network == "random", do: channel, else: "#{network}/#{channel}")
+ ],
{:ok, %{"room_id" => room_id}} <- Client.Room.create_room(client(), room) do
Logger.info("Matrix: created room #{room_alias} #{room_id}")
:dets.insert(dets(:rooms), {room_id, network, channel, %{}})
@@ -110,14 +121,15 @@ defmodule Nola.Matrix do
end
def localpart(room_alias) do
- [<<"#", localpart :: binary>>, _] = String.split(room_alias, ":", parts: 2)
+ [<<"#", localpart::binary>>, _] = String.split(room_alias, ":", parts: 2)
localpart
end
def extract_network_channel_from_localpart(localpart) do
- s = localpart
- |> String.replace("dev.", "")
- |> String.split("/", parts: 2)
+ s =
+ localpart
+ |> String.replace("dev.", "")
+ |> String.split("/", parts: 2)
case s do
[network, channel] -> {:ok, network, channel}
@@ -132,38 +144,42 @@ defmodule Nola.Matrix do
{:ok, room_id} ->
Nola.Matrix.Room.start(room_id)
:ok
- error -> error
+
+ error ->
+ error
end
end
@impl MatrixAppService.Adapter.Transaction
def new_event(event = %MatrixAppService.Event{}) do
- Logger.debug("New matrix event: #{inspect event}")
+ Logger.debug("New matrix event: #{inspect(event)}")
+
if event.room_id do
Nola.Matrix.Room.start_and_send_matrix_event(event.room_id, event)
end
+
:noop
end
@impl MatrixAppService.Adapter.User
def query_user(user_id) do
- Logger.warn("Matrix lookup user: #{inspect user_id}")
+ Logger.warn("Matrix lookup user: #{inspect(user_id)}")
:error
end
def client(opts \\ []) do
base_url = Application.get_env(:matrix_app_service, :base_url)
access_token = Application.get_env(:matrix_app_service, :access_token)
+
default_opts = [
access_token: access_token,
device_id: "APP_SERVICE",
application_service: true,
user_id: nil
]
+
opts = Keyword.merge(default_opts, opts)
Polyjuice.Client.LowLevel.create(base_url, opts)
end
-
-
end
diff --git a/lib/matrix/plug.ex b/lib/matrix/plug.ex
index c64ed11..e9be4ad 100644
--- a/lib/matrix/plug.ex
+++ b/lib/matrix/plug.ex
@@ -1,11 +1,10 @@
defmodule Nola.Matrix.Plug do
-
defmodule Auth do
- def init(state) do
+ def init(state) do
state
end
- def call(conn, _) do
+ def call(conn, _) do
hs = Application.get_env(:matrix_app_service, :homeserver_token)
MatrixAppServiceWeb.AuthPlug.call(conn, hs)
end
@@ -21,5 +20,4 @@ defmodule Nola.Matrix.Plug do
MatrixAppServiceWeb.SetConfigPlug.call(conn, config)
end
end
-
end
diff --git a/lib/matrix/room.ex b/lib/matrix/room.ex
index e2965a5..b3921f6 100644
--- a/lib/matrix/room.ex
+++ b/lib/matrix/room.ex
@@ -12,7 +12,12 @@ defmodule Nola.Matrix.Room do
end
def start_child(room_id) do
- spec = %{id: room_id, start: {Nola.Matrix.Room, :start_link, [room_id]}, restart: :transient}
+ spec = %{
+ id: room_id,
+ start: {Nola.Matrix.Room, :start_link, [room_id]},
+ restart: :transient
+ }
+
DynamicSupervisor.start_child(__MODULE__, spec)
end
@@ -35,20 +40,23 @@ defmodule Nola.Matrix.Room do
end
def start_and_send_matrix_event(room_id, event) do
- pid = if pid = whereis(room_id) do
- pid
- else
- case __MODULE__.start(room_id) do
- {:ok, pid} -> pid
- {:error, {:already_started, pid}} -> pid
- :ignore -> nil
+ pid =
+ if pid = whereis(room_id) do
+ pid
+ else
+ case __MODULE__.start(room_id) do
+ {:ok, pid} -> pid
+ {:error, {:already_started, pid}} -> pid
+ :ignore -> nil
+ end
end
- end
+
if(pid, do: send(pid, {:matrix_event, event}))
end
def whereis(room_id) do
{:global, name} = name(room_id)
+
case :global.whereis_name(name) do
:undefined -> nil
pid -> pid
@@ -65,45 +73,62 @@ defmodule Nola.Matrix.Room do
Logger.metadata(matrix_room: room_id)
{:ok, _} = Registry.register(Nola.PubSub, "#{state.network}:events", plugin: __MODULE__)
+
for t <- ["messages", "triggers", "outputs", "events"] do
- {:ok, _} = Registry.register(Nola.PubSub, "#{state.network}/#{state.channel}:#{t}", plugin: __MODULE__)
+ {:ok, _} =
+ Registry.register(Nola.PubSub, "#{state.network}/#{state.channel}:#{t}",
+ plugin: __MODULE__
+ )
end
- state = state
- |> Map.put(:id, room_id)
+ state =
+ state
+ |> Map.put(:id, room_id)
+
Logger.info("Started Matrix room #{room_id}")
{:ok, state, {:continue, :update_state}}
+
error ->
- Logger.info("Received event for nonexistent room #{inspect room_id}: #{inspect error}")
+ Logger.info("Received event for nonexistent room #{inspect(room_id)}: #{inspect(error)}")
:ignore
end
end
def handle_continue(:update_state, state) do
{:ok, s} = Client.Room.get_state(client(), state.id)
- members = Enum.reduce(s, [], fn(s, acc) ->
- if s["type"] == "m.room.member" do
- if s["content"]["membership"] == "join" do
- [s["user_id"] | acc]
+
+ members =
+ Enum.reduce(s, [], fn s, acc ->
+ if s["type"] == "m.room.member" do
+ if s["content"]["membership"] == "join" do
+ [s["user_id"] | acc]
+ else
+ # XXX: The user left, remove from Nola.Memberships ?
+ acc
+ end
else
- # XXX: The user left, remove from Nola.Memberships ?
acc
end
- else
- acc
- end
- end)
- |> Enum.filter(& &1)
-
- for m <- members, do: Nola.UserTrack.joined(state.id, %{network: "matrix", nick: m, user: m, host: "matrix."}, [], true)
-
- accounts = Nola.UserTrack.channel(state.network, state.channel)
- |> Enum.filter(& &1)
- |> Enum.map(fn(tuple) -> Nola.UserTrack.User.from_tuple(tuple).account end)
- |> Enum.uniq()
- |> Enum.each(fn(account_id) ->
- introduce_irc_account(account_id, state)
- end)
+ end)
+ |> Enum.filter(& &1)
+
+ for m <- members,
+ do:
+ Nola.UserTrack.joined(
+ state.id,
+ %{network: "matrix", nick: m, user: m, host: "matrix."},
+ [],
+ true
+ )
+
+ accounts =
+ Nola.UserTrack.channel(state.network, state.channel)
+ |> Enum.filter(& &1)
+ |> Enum.map(fn tuple -> Nola.UserTrack.User.from_tuple(tuple).account end)
+ |> Enum.uniq()
+ |> Enum.each(fn account_id ->
+ introduce_irc_account(account_id, state)
+ end)
{:noreply, state}
end
@@ -112,6 +137,7 @@ defmodule Nola.Matrix.Room do
def handle_info({:irc, :out, message}, state), do: handle_irc(message, state)
def handle_info({:irc, :trigger, _, message}, state), do: handle_irc(message, state)
def handle_info({:irc, :event, event}, state), do: handle_irc(event, state)
+
def handle_info({:matrix_event, event}, state) do
if myself?(event.user_id) do
{:noreply, state}
@@ -122,14 +148,17 @@ defmodule Nola.Matrix.Room do
def handle_irc(message = %Nola.Message{account: account}, state) do
unless Map.get(message.meta, :puppet) && Map.get(message.meta, :from) == self() do
- opts = if Map.get(message.meta, :self) || is_nil(account) do
- []
- else
- mxid = Matrix.get_or_create_matrix_user(account.id)
- [user_id: mxid]
- end
- Client.Room.send_message(client(opts),state.id, message.text)
+ opts =
+ if Map.get(message.meta, :self) || is_nil(account) do
+ []
+ else
+ mxid = Matrix.get_or_create_matrix_user(account.id)
+ [user_id: mxid]
+ end
+
+ Client.Room.send_message(client(opts), state.id, message.text)
end
+
{:noreply, state}
end
@@ -138,36 +167,56 @@ defmodule Nola.Matrix.Room do
{:noreply, state}
end
- def handle_irc(%{type: quit_or_part, account_id: account_id}, state) when quit_or_part in [:quit, :part] do
+ def handle_irc(%{type: quit_or_part, account_id: account_id}, state)
+ when quit_or_part in [:quit, :part] do
mxid = Matrix.get_or_create_matrix_user(account_id)
Client.Room.leave(client(user_id: mxid), state.id)
{:noreply, state}
end
-
def handle_irc(event, state) do
- Logger.warn("Skipped irc event #{inspect event}")
+ Logger.warn("Skipped irc event #{inspect(event)}")
{:noreply, state}
end
- def handle_matrix(event = %{type: "m.room.member", user_id: user_id, content: %{"membership" => "join"}}, state) do
+ def handle_matrix(
+ event = %{type: "m.room.member", user_id: user_id, content: %{"membership" => "join"}},
+ state
+ ) do
_account = get_account(event, state)
- Nola.UserTrack.joined(state.id, %{network: "matrix", nick: user_id, user: user_id, host: "matrix."}, [], true)
+
+ Nola.UserTrack.joined(
+ state.id,
+ %{network: "matrix", nick: user_id, user: user_id, host: "matrix."},
+ [],
+ true
+ )
+
{:noreply, state}
end
- def handle_matrix(event = %{type: "m.room.member", user_id: user_id, content: %{"membership" => "leave"}}, state) do
+ def handle_matrix(
+ event = %{type: "m.room.member", user_id: user_id, content: %{"membership" => "leave"}},
+ state
+ ) do
Nola.UserTrack.parted(state.id, %{network: "matrix", nick: user_id})
{:noreply, state}
end
- def handle_matrix(event = %{type: "m.room.message", user_id: user_id, content: %{"msgtype" => "m.text", "body" => text}}, state) do
+ def handle_matrix(
+ event = %{
+ type: "m.room.message",
+ user_id: user_id,
+ content: %{"msgtype" => "m.text", "body" => text}
+ },
+ state
+ ) do
Nola.Irc.send_message_as(get_account(event, state), state.network, state.channel, text, true)
{:noreply, state}
end
def handle_matrix(event, state) do
- Logger.warn("Skipped matrix event #{inspect event}")
+ Logger.warn("Skipped matrix event #{inspect(event)}")
{:noreply, state}
end
@@ -180,17 +229,23 @@ defmodule Nola.Matrix.Room do
account = Nola.Account.get(account_id)
user = Nola.UserTrack.find_by_account(state.network, account)
base_nick = if(user, do: user.nick, else: account.name)
+
case Client.Profile.put_displayname(client(user_id: mxid), base_nick) do
- :ok -> :ok
+ :ok ->
+ :ok
+
error ->
- Logger.warn("Failed to update profile for #{mxid}: #{inspect error}")
+ Logger.warn("Failed to update profile for #{mxid}: #{inspect(error)}")
end
+
case Client.Room.join(client(user_id: mxid), state.id) do
- {:ok, _} -> :ok
+ {:ok, _} ->
+ :ok
+
error ->
- Logger.warn("Failed to join room for #{mxid}: #{inspect error}")
+ Logger.warn("Failed to join room for #{mxid}: #{inspect(error)}")
end
+
:ok
end
-
end
diff --git a/lib/nola.ex b/lib/nola.ex
index 51c2150..a9c7287 100644
--- a/lib/nola.ex
+++ b/lib/nola.ex
@@ -1,5 +1,4 @@
defmodule Nola do
-
@default_brand [
name: "Nola",
source_url: "https://phab.random.sh/source/Nola/",
@@ -26,5 +25,4 @@ defmodule Nola do
def version do
Application.spec(:nola)[:vsn]
end
-
end
diff --git a/lib/nola/account.ex b/lib/nola/account.ex
index 70e9e40..d850a82 100644
--- a/lib/nola/account.ex
+++ b/lib/nola/account.ex
@@ -44,7 +44,7 @@ defmodule Nola.Account do
end
def start_link() do
- GenServer.start_link(__MODULE__, [], [name: __MODULE__])
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
@@ -63,6 +63,7 @@ defmodule Nola.Account do
def get_by_name(name) do
spec = [{{:_, :"$1", :_}, [{:==, :"$1", {:const, name}}], [:"$_"]}]
+
case :dets.select(file("db"), spec) do
[account] -> from_tuple(account)
_ -> nil
@@ -71,7 +72,7 @@ defmodule Nola.Account do
def get_meta(%__MODULE__{id: id}, key, default \\ nil) do
case :dets.lookup(file("meta"), {id, key}) do
- [{_, value}] -> (value || default)
+ [{_, value}] -> value || default
_ -> default
end
end
@@ -86,8 +87,12 @@ defmodule Nola.Account do
@doc "Find an account given a specific meta `key` and `value`."
@spec find_meta_account(String.t(), String.t()) :: t() | nil
def find_meta_account(key, value) do
- #spec = [{{{:"$1", :"$2"}, :"$3"}, [:andalso, {:==, :"$2", {:const, key}}, {:==, :"$3", {:const, value}}], [:"$1"]}]
- spec = [{{{:"$1", :"$2"}, :"$3"}, [{:andalso, {:==, :"$2", {:const, key}}, {:==, {:const, value}, :"$3"}}], [:"$1"]}]
+ # spec = [{{{:"$1", :"$2"}, :"$3"}, [:andalso, {:==, :"$2", {:const, key}}, {:==, :"$3", {:const, value}}], [:"$1"]}]
+ spec = [
+ {{{:"$1", :"$2"}, :"$3"},
+ [{:andalso, {:==, :"$2", {:const, key}}, {:==, {:const, value}, :"$3"}}], [:"$1"]}
+ ]
+
case :dets.select(file("meta"), spec) do
[id] -> get(id)
_ -> nil
@@ -100,7 +105,7 @@ defmodule Nola.Account do
end
def put_user_meta(account = %__MODULE__{}, key, value) do
- put_meta(account, "u:"<>key, value)
+ put_meta(account, "u:" <> key, value)
end
def put_meta(%__MODULE__{id: id}, key, value) do
@@ -112,15 +117,15 @@ defmodule Nola.Account do
end
def all_accounts() do
- :dets.traverse(file("db"), fn(obj) -> {:continue, from_tuple(obj)} end)
+ :dets.traverse(file("db"), fn obj -> {:continue, from_tuple(obj)} end)
end
def all_predicates() do
- :dets.traverse(file("predicates"), fn(obj) -> {:continue, obj} end)
+ :dets.traverse(file("predicates"), fn obj -> {:continue, obj} end)
end
def all_meta() do
- :dets.traverse(file("meta"), fn(obj) -> {:continue, obj} end)
+ :dets.traverse(file("meta"), fn obj -> {:continue, obj} end)
end
def merge_account(old_id, new_id) do
@@ -130,16 +135,19 @@ defmodule Nola.Account do
for pred <- predicates, do: :ok = :dets.insert(file("predicates"), {pred, new_id})
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$1", {:const, old_id}}], [{{:"$2", :"$3"}}]}]
metas = :dets.select(file("meta"), spec)
- for {k,v} <- metas do
+
+ for {k, v} <- metas do
:dets.delete(file("meta"), {{old_id, k}})
:ok = :dets.insert(file("meta"), {{new_id, k}, v})
end
+
:dets.delete(file("db"), old_id)
Nola.Membership.merge_account(old_id, new_id)
Nola.UserTrack.merge_account(old_id, new_id)
Nola.Irc.Connection.dispatch("account", {:account_change, old_id, new_id})
Nola.Irc.Connection.dispatch("conn", {:account_change, old_id, new_id})
end
+
:ok
end
@@ -150,16 +158,15 @@ defmodule Nola.Account do
@doc "Always find an account by nickname, even if offline. Uses predicates and then account name."
def find_always_by_nick(network, chan, nick) do
- with \
- nil <- find_by_nick(network, nick),
+ with nil <- find_by_nick(network, nick),
nil <- do_lookup(%User{network: network, nick: nick}, false),
- nil <- get_by_name(nick)
- do
+ nil <- get_by_name(nick) do
nil
else
%__MODULE__{} = account ->
memberships = Nola.Membership.of_account(account)
- if Enum.any?(memberships, fn({net, ch}) -> (net == network) or (chan && chan == ch) end) do
+
+ if Enum.any?(memberships, fn {net, ch} -> net == network or (chan && chan == ch) end) do
account
else
nil
@@ -173,9 +180,14 @@ defmodule Nola.Account do
def lookup(something, make_default \\ true) do
account = do_lookup(something, make_default)
+
if account && Map.get(something, :nick) do
- Nola.Irc.Connection.dispatch("account", {:account_auth, Map.get(something, :nick), account.id})
+ Nola.Irc.Connection.dispatch(
+ "account",
+ {:account_auth, Map.get(something, :nick), account.id}
+ )
end
+
account
end
@@ -198,7 +210,8 @@ defmodule Nola.Account do
end
end
- defp do_lookup(message = %Nola.Message{account: account_id}, make_default) when is_binary(account_id) do
+ defp do_lookup(message = %Nola.Message{account: account_id}, make_default)
+ when is_binary(account_id) do
get(account_id)
end
@@ -206,8 +219,12 @@ defmodule Nola.Account do
if user = Nola.UserTrack.find_by_nick(sender) do
lookup(user, make_default)
else
- #FIXME this will never work with continued lookup by other methods as Who isn't compatible
- lookup_by_nick(sender, :dets.lookup(file("predicates"), {sender.network,{:nick, sender.nick}}), make_default)
+ # FIXME this will never work with continued lookup by other methods as Who isn't compatible
+ lookup_by_nick(
+ sender,
+ :dets.lookup(file("predicates"), {sender.network, {:nick, sender.nick}}),
+ make_default
+ )
end
end
@@ -220,7 +237,7 @@ defmodule Nola.Account do
end
defp do_lookup(user = %User{network: server, nick: nick}, make_default) do
- lookup_by_nick(user, :dets.lookup(file("predicates"), {server,{:nick, nick}}), make_default)
+ lookup_by_nick(user, :dets.lookup(file("predicates"), {server, {:nick, nick}}), make_default)
end
defp do_lookup(nil, _) do
@@ -232,7 +249,7 @@ defmodule Nola.Account do
end
defp lookup_by_nick(user, _, make_default) do
- #authenticate_by_host(user)
+ # authenticate_by_host(user)
if make_default, do: new_account(user), else: nil
end
@@ -259,5 +276,4 @@ defmodule Nola.Account do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
:dets.select(file("predicates"), spec)
end
-
end
diff --git a/lib/nola/application.ex b/lib/nola/application.ex
index d56d4cb..73cc80b 100644
--- a/lib/nola/application.ex
+++ b/lib/nola/application.ex
@@ -4,39 +4,42 @@ defmodule Nola.Application do
def start(_type, _args) do
import Supervisor.Spec
- Logger.add_backend(Sentry.LoggerBackend)
-
Nola.Plugins.setup()
:ok = Nola.Matrix.setup()
:ok = Nola.TelegramRoom.setup()
# Define workers and child supervisors to be supervised
- children = [
- supervisor(NolaWeb.Endpoint, []),
- worker(Registry, [[keys: :duplicate, name: Nola.BroadcastRegistry]], id: :registry_broadcast),
- worker(Nola.IcecastAgent, []),
- worker(Nola.Token, []),
- worker(Nola.AuthToken, []),
- Nola.Subnet,
- {GenMagic.Pool, [name: Nola.GenMagic, pool_size: 2]},
- worker(Registry, [[keys: :duplicate, name: Nola.PubSub]], id: :registry_nola_pubsub),
- worker(Nola.Membership, []),
- worker(Nola.Account, []),
- worker(Nola.UserTrack.Storage, []),
- worker(Nola.Plugins.Account, []),
- supervisor(Nola.Plugins.Supervisor, [], [name: Nola.Plugins.Supervisor]),
- ] ++ Nola.Irc.application_childs
- ++ Nola.Matrix.application_childs
+ children =
+ [
+ supervisor(NolaWeb.Endpoint, []),
+ worker(Registry, [[keys: :duplicate, name: Nola.BroadcastRegistry]],
+ id: :registry_broadcast
+ ),
+ worker(Nola.IcecastAgent, []),
+ worker(Nola.Token, []),
+ worker(Nola.AuthToken, []),
+ Nola.Subnet,
+ {GenMagic.Pool, [name: Nola.GenMagic, pool_size: 2]},
+ worker(Registry, [[keys: :duplicate, name: Nola.PubSub]], id: :registry_nola_pubsub),
+ worker(Nola.Membership, []),
+ worker(Nola.Account, []),
+ worker(Nola.UserTrack.Storage, []),
+ worker(Nola.Plugins.Account, []),
+ worker(Nola.Plugins.Link.Store, []),
+ supervisor(Nola.Plugins.Supervisor, [], name: Nola.Plugins.Supervisor)
+ ] ++
+ Nola.Irc.application_childs() ++
+ Nola.Matrix.application_childs()
opts = [strategy: :one_for_one, name: Nola.Supervisor]
sup = Supervisor.start_link(children, opts)
-
+
start_telegram()
Nola.Plugins.start_all()
- spawn_link(fn() -> Nola.Irc.after_start() end)
- spawn_link(fn() -> Nola.Matrix.after_start() end)
- spawn_link(fn() -> Nola.TelegramRoom.after_start() end)
+ spawn_link(fn -> Nola.Irc.after_start() end)
+ spawn_link(fn -> Nola.Matrix.after_start() end)
+ spawn_link(fn -> Nola.TelegramRoom.after_start() end)
sup
end
@@ -47,11 +50,12 @@ defmodule Nola.Application do
defp start_telegram() do
token = Keyword.get(Application.get_env(:nola, :telegram, []), :key)
+
options = [
username: Keyword.get(Application.get_env(:nola, :telegram, []), :nick, "beauttebot"),
purge: false
]
+
telegram = Telegram.Bot.ChatBot.Supervisor.start_link({Nola.Telegram, token, options})
end
-
end
diff --git a/lib/nola/auth_token.ex b/lib/nola/auth_token.ex
index 9760ec7..0da4aab 100644
--- a/lib/nola/auth_token.ex
+++ b/lib/nola/auth_token.ex
@@ -2,7 +2,7 @@ defmodule Nola.AuthToken do
use GenServer
def start_link() do
- GenServer.start_link(__MODULE__, [], [name: __MODULE__])
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def lookup(id) do
@@ -13,6 +13,7 @@ defmodule Nola.AuthToken do
case new(account, perks) do
{:ok, id} ->
NolaWeb.Router.Helpers.login_path(NolaWeb.Endpoint, :token, id)
+
error ->
error
end
@@ -22,6 +23,7 @@ defmodule Nola.AuthToken do
case new(account, perks) do
{:ok, id} ->
NolaWeb.Router.Helpers.login_url(NolaWeb.Endpoint, :token, id)
+
error ->
error
end
@@ -32,15 +34,14 @@ defmodule Nola.AuthToken do
end
def init(_) do
- {:ok, Map.new}
+ {:ok, Map.new()}
end
def handle_call({:lookup, id}, _, state) do
IO.inspect(state)
- with \
- {account, date, perks} <- Map.get(state, id),
- d when d > 0 <- DateTime.diff(date, DateTime.utc_now())
- do
+
+ with {account, date, perks} <- Map.get(state, id),
+ d when d > 0 <- DateTime.diff(date, DateTime.utc_now()) do
{:reply, {:ok, account, perks}, Map.delete(state, id)}
else
x ->
@@ -51,9 +52,11 @@ defmodule Nola.AuthToken do
def handle_call({:new, account, perks}, _, state) do
id = Nola.UserTrack.Id.token()
- expire = DateTime.utc_now()
- |> DateTime.add(15*60, :second)
+
+ expire =
+ DateTime.utc_now()
+ |> DateTime.add(15 * 60, :second)
+
{:reply, {:ok, id}, Map.put(state, id, {account, expire, perks})}
end
-
end
diff --git a/lib/nola/icecast.ex b/lib/nola/icecast.ex
index 5a53192..021af0b 100644
--- a/lib/nola/icecast.ex
+++ b/lib/nola/icecast.ex
@@ -23,69 +23,82 @@ defmodule Nola.Icecast do
end
defp poll(state) do
- state = case request(base_url(), :get) do
- {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
- #update_json_stats(Jason.decode(body))
- stats = update_stats(body)
- if state != stats do
- Logger.info "Icecast Update: " <> inspect(stats)
- Nola.IcecastAgent.update(stats)
- Registry.dispatch(Nola.BroadcastRegistry, "icecast", fn ws ->
- for {pid, _} <- ws, do: send(pid, {:icecast, stats})
- end)
- stats
- else
+ state =
+ case request(base_url(), :get) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ # update_json_stats(Jason.decode(body))
+ stats = update_stats(body)
+
+ if state != stats do
+ Logger.info("Icecast Update: " <> inspect(stats))
+ Nola.IcecastAgent.update(stats)
+
+ Registry.dispatch(Nola.BroadcastRegistry, "icecast", fn ws ->
+ for {pid, _} <- ws, do: send(pid, {:icecast, stats})
+ end)
+
+ stats
+ else
+ state
+ end
+
+ error ->
+ Logger.error("Icecast HTTP Error: #{inspect(error)}")
state
- end
- error ->
- Logger.error "Icecast HTTP Error: #{inspect error}"
- state
- end
+ end
+
interval = Application.get_env(:nola, :icecast_poll_interval, 60_000)
:timer.send_after(interval, :poll)
state
end
defp update_stats(html) do
- raw = Floki.find(html, "div.roundbox")
- |> Enum.map(fn(html) ->
- html = Floki.raw_html(html)
- [{"h3", _, ["Mount Point /"<>mount]}] = Floki.find(html, "h3.mount")
- stats = Floki.find(html, "tr")
- |> Enum.map(fn({"tr", _, tds}) ->
- [{"td", _, keys}, {"td", _, values}] = tds
- key = List.first(keys)
- value = List.first(values)
- {key, value}
+ raw =
+ Floki.find(html, "div.roundbox")
+ |> Enum.map(fn html ->
+ html = Floki.raw_html(html)
+ [{"h3", _, ["Mount Point /" <> mount]}] = Floki.find(html, "h3.mount")
+
+ stats =
+ Floki.find(html, "tr")
+ |> Enum.map(fn {"tr", _, tds} ->
+ [{"td", _, keys}, {"td", _, values}] = tds
+ key = List.first(keys)
+ value = List.first(values)
+ {key, value}
+ end)
+ |> Enum.into(Map.new())
+
+ {mount, stats}
end)
- |> Enum.into(Map.new)
- {mount, stats}
- end)
- |> Enum.into(Map.new)
+ |> Enum.into(Map.new())
live? = if Map.get(raw["live"], "Content Type:", false), do: true, else: false
- np = if live? do
- raw["live"]["Currently playing:"]
- else
- raw["autodj"]["Currently playing:"]
- end
+
+ np =
+ if live? do
+ raw["live"]["Currently playing:"]
+ else
+ raw["autodj"]["Currently playing:"]
+ end
genre = raw["live"]["Genre:"] || nil
%{np: np || "", live: live? || false, genre: genre}
end
defp update_json_stats({:ok, body}) do
- Logger.debug "JSON STATS: #{inspect body}"
+ Logger.debug("JSON STATS: #{inspect(body)}")
end
defp update_json_stats(error) do
- Logger.error "Failed to decode JSON Stats: #{inspect error}"
+ Logger.error("Failed to decode JSON Stats: #{inspect(error)}")
end
defp request(uri, method, body \\ [], headers \\ []) do
headers = [{"user-agent", "Nola-API[115ans.net, sys.115ans.net] href@random.sh"}] ++ headers
options = @httpoison_opts
- case :ok do #:fuse.ask(@fuse, :sync) do
+ # :fuse.ask(@fuse, :sync) do
+ case :ok do
:ok -> run_request(method, uri, body, headers, options)
:blown -> :blown
end
@@ -93,14 +106,18 @@ defmodule Nola.Icecast do
# This is to work around hackney's behaviour of returning `{:error, :closed}` when a pool connection has been closed
# (keep-alive expired). We just retry the request immediatly up to five times.
- defp run_request(method, uri, body, headers, options), do: run_request(method, uri, body, headers, options, 0)
+ defp run_request(method, uri, body, headers, options),
+ do: run_request(method, uri, body, headers, options, 0)
+
defp run_request(method, uri, body, headers, options, retries) when retries < 4 do
case HTTPoison.request(method, uri, body, headers, options) do
{:error, :closed} -> run_request(method, uri, body, headers, options, retries + 1)
other -> other
end
end
- defp run_request(method, uri, body, headers, options, _exceeded_retries), do: {:error, :unavailable}
+
+ defp run_request(method, uri, body, headers, options, _exceeded_retries),
+ do: {:error, :unavailable}
#
# -- URIs
@@ -113,5 +130,4 @@ defmodule Nola.Icecast do
defp base_url do
"http://91.121.59.45:8089"
end
-
end
diff --git a/lib/nola/icecast_agent.ex b/lib/nola/icecast_agent.ex
index 8a3a72b..563b372 100644
--- a/lib/nola/icecast_agent.ex
+++ b/lib/nola/icecast_agent.ex
@@ -6,12 +6,10 @@ defmodule Nola.IcecastAgent do
end
def update(stats) do
- Agent.update(__MODULE__, fn(_old) -> stats end)
+ Agent.update(__MODULE__, fn _old -> stats end)
end
def get do
- Agent.get(__MODULE__, fn(stats) -> stats end)
+ Agent.get(__MODULE__, fn stats -> stats end)
end
-
end
-
diff --git a/lib/nola/membership.ex b/lib/nola/membership.ex
index 1c7303b..182f44d 100644
--- a/lib/nola/membership.ex
+++ b/lib/nola/membership.ex
@@ -7,11 +7,11 @@ defmodule Nola.Membership do
# Format: {key, last_seen}
defp dets() do
- to_charlist(Nola.data_path <> "/memberships.dets")
+ to_charlist(Nola.data_path() <> "/memberships.dets")
end
def start_link() do
- GenServer.start_link(__MODULE__, [], [name: __MODULE__])
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
@@ -25,9 +25,10 @@ defmodule Nola.Membership do
end
def merge_account(old_id, new_id) do
- #iex(37)> :ets.fun2ms(fn({{old_id, _, _}, _}=obj) when old_id == "42" -> obj end)
+ # iex(37)> :ets.fun2ms(fn({{old_id, _, _}, _}=obj) when old_id == "42" -> obj end)
spec = [{{{:"$1", :_, :_}, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
- Util.ets_mutate_select_each(:dets, dets(), spec, fn(table, obj = {{_old, net, chan}, ts}) ->
+
+ Util.ets_mutate_select_each(:dets, dets(), spec, fn table, obj = {{_old, net, chan}, ts} ->
:dets.delete_object(table, obj)
:dets.insert(table, {{new_id, net, chan}, ts})
end)
@@ -36,6 +37,7 @@ defmodule Nola.Membership do
def touch(%Nola.Account{id: id}, network, channel) do
:dets.insert(dets(), {{id, network, channel}, NaiveDateTime.utc_now()})
end
+
def touch(account_id, network, channel) do
if account = Nola.Account.get(account_id) do
touch(account, network, channel)
@@ -43,21 +45,28 @@ defmodule Nola.Membership do
end
def notify_channels(account, minutes \\ 30, last_active \\ true) do
- not_before = NaiveDateTime.add(NaiveDateTime.utc_now(), (minutes*-60), :second)
+ not_before = NaiveDateTime.add(NaiveDateTime.utc_now(), minutes * -60, :second)
spec = [{{{:"$1", :_, :_}, :_}, [{:==, :"$1", {:const, account.id}}], [:"$_"]}]
- memberships = :dets.select(dets(), spec)
- |> Enum.sort_by(fn({_, ts}) -> ts end, {:desc, NaiveDateTime})
- active_memberships = Enum.filter(memberships, fn({_, ts}) -> NaiveDateTime.compare(ts, not_before) == :gt end)
+
+ memberships =
+ :dets.select(dets(), spec)
+ |> Enum.sort_by(fn {_, ts} -> ts end, {:desc, NaiveDateTime})
+
+ active_memberships =
+ Enum.filter(memberships, fn {_, ts} -> NaiveDateTime.compare(ts, not_before) == :gt end)
+
cond do
active_memberships == [] && last_active ->
case memberships do
- [{{_, net, chan}, _}|_] -> [{net, chan}]
+ [{{_, net, chan}, _} | _] -> [{net, chan}]
_ -> []
end
+
active_memberships == [] ->
[]
+
true ->
- Enum.map(active_memberships, fn({{_, net, chan}, _}) -> {net,chan} end)
+ Enum.map(active_memberships, fn {{_, net, chan}, _} -> {net, chan} end)
end
end
@@ -78,16 +87,18 @@ defmodule Nola.Membership do
end
def members(network, channel) do
- #iex(19)> :ets.fun2ms(fn({{id, net, chan}, ts}) when net == network and chan == channel and ts > min_seen -> id end)
- limit = 0 # NaiveDateTime.add(NaiveDateTime.utc_now, 30*((24*-60)*60), :second)
+ # iex(19)> :ets.fun2ms(fn({{id, net, chan}, ts}) when net == network and chan == channel and ts > min_seen -> id end)
+ # NaiveDateTime.add(NaiveDateTime.utc_now, 30*((24*-60)*60), :second)
+ limit = 0
+
spec = [
{{{:"$1", :"$2", :"$3"}, :"$4"},
[
- {:andalso,
- {:andalso, {:==, :"$2", {:const, network}}, {:==, :"$3", {:const, channel}}},
+ {:andalso, {:andalso, {:==, :"$2", {:const, network}}, {:==, :"$3", {:const, channel}}},
{:>, :"$4", {:const, limit}}}
], [:"$1"]}
]
+
:dets.select(dets(), spec)
end
@@ -122,8 +133,6 @@ defmodule Nola.Membership do
{account, user, nick}
end
end
- |> Enum.filter(fn(x) -> x end)
+ |> Enum.filter(fn x -> x end)
end
-
-
end
diff --git a/lib/nola/message.ex b/lib/nola/message.ex
index b4e76da..1819e77 100644
--- a/lib/nola/message.ex
+++ b/lib/nola/message.ex
@@ -9,15 +9,14 @@ defmodule Nola.Message do
defstruct [
:id,
:text,
- {:transport, :irc},
- :network,
- :account,
- :sender,
- :channel,
- :trigger,
- :replyfun,
- :at,
- {:meta, %{}}
- ]
-
+ {:transport, :irc},
+ :network,
+ :account,
+ :sender,
+ :channel,
+ :trigger,
+ :replyfun,
+ :at,
+ {:meta, %{}}
+ ]
end
diff --git a/lib/nola/plugins.ex b/lib/nola/plugins.ex
index ac94736..6bfcfec 100644
--- a/lib/nola/plugins.ex
+++ b/lib/nola/plugins.ex
@@ -31,7 +31,7 @@ defmodule Nola.Plugins do
Nola.Plugins.Untappd,
Nola.Plugins.UserMention,
Nola.Plugins.WolframAlpha,
- Nola.Plugins.YouTube,
+ Nola.Plugins.YouTube
]
defmodule Supervisor do
@@ -44,13 +44,23 @@ defmodule Nola.Plugins do
def start_child(module, opts \\ []) do
Logger.info("Starting #{module}")
- spec = %{id: {Nola.Plugins,module}, start: {Nola.Plugins, :start_link, [module, opts]}, name: module, restart: :transient}
+
+ spec = %{
+ id: {Nola.Plugins, module},
+ start: {Nola.Plugins, :start_link, [module, opts]},
+ name: module,
+ restart: :transient
+ }
+
case DynamicSupervisor.start_child(__MODULE__, spec) do
- {:ok, _} = res -> res
+ {:ok, _} = res ->
+ res
+
:ignore ->
Logger.warn("Ignored #{module}")
:ignore
- {:error,_} = res ->
+
+ {:error, _} = res ->
Logger.error("Could not start #{module}: #{inspect(res, pretty: true)}")
res
end
@@ -73,10 +83,14 @@ defmodule Nola.Plugins do
end
def enabled() do
- :dets.foldl(fn
- {name, true, _}, acc -> [name | acc]
- _, acc -> acc
- end, [], dets())
+ :dets.foldl(
+ fn
+ {name, true, _}, acc -> [name | acc]
+ _, acc -> acc
+ end,
+ [],
+ dets()
+ )
end
def start_all() do
@@ -107,10 +121,12 @@ defmodule Nola.Plugins do
@doc "Enables or disables a plugin"
def switch(name, value) when is_boolean(value) do
- last = case get(name) do
- {:ok, last} -> last
- _ -> nil
- end
+ last =
+ case get(name) do
+ {:ok, last} -> last
+ _ -> nil
+ end
+
:dets.insert(dets(), {name, value, last})
end
@@ -124,8 +140,7 @@ defmodule Nola.Plugins do
def start_link(module, options \\ []) do
with {:disabled, {_, true, last}} <- {:disabled, get(module)},
- {:throttled, false} <- {:throttled, false}
- do
+ {:throttled, false} <- {:throttled, false} do
module.start_link()
else
{error, _} ->
@@ -133,5 +148,4 @@ defmodule Nola.Plugins do
:ignore
end
end
-
end
diff --git a/lib/nola/subnet.ex b/lib/nola/subnet.ex
index ac9d8e6..de469a6 100644
--- a/lib/nola/subnet.ex
+++ b/lib/nola/subnet.ex
@@ -17,22 +17,24 @@ defmodule Nola.Subnet do
end
def assign(binary) when is_binary(binary) do
- result = if subnet = find_subnet_for(binary) do
- {:ok, subnet}
- else
- Agent.get_and_update(__MODULE__, fn(dets) ->
- {subnet, _} = available_select(dets)
- :dets.insert(dets, {subnet, binary})
- :dets.sync(dets)
- {{:new, subnet}, dets}
- end)
- end
+ result =
+ if subnet = find_subnet_for(binary) do
+ {:ok, subnet}
+ else
+ Agent.get_and_update(__MODULE__, fn dets ->
+ {subnet, _} = available_select(dets)
+ :dets.insert(dets, {subnet, binary})
+ :dets.sync(dets)
+ {{:new, subnet}, dets}
+ end)
+ end
case result do
{:new, subnet} ->
ip = Pfx.host(subnet, 1)
set_reverse(binary, ip)
subnet
+
{:ok, subnet} ->
subnet
end
@@ -49,16 +51,40 @@ defmodule Nola.Subnet do
ip_fqdn = Pfx.dns_ptr(ip)
ip_local = String.replace(ip_fqdn, ".#{ptr_zone}", "")
rev? = String.ends_with?(value, ".users.goulag.org")
+
if rev? do
{:ok, rev_zone} = PowerDNSex.show_zone("users.goulag.org")
- rev_update? = Enum.any?(rev_zone.rrsets, fn(rr) -> rr.name == "#{ip_fqdn}." end)
- record = %{name: "#{value}.", type: "AAAA", ttl: 8600, records: [%{content: ip, disabled: false}]}
- if(rev_update?, do: PowerDNSex.update_record(rev_zone, record), else: PowerDNSex.create_record(rev_zone, record))
+ rev_update? = Enum.any?(rev_zone.rrsets, fn rr -> rr.name == "#{ip_fqdn}." end)
+
+ record = %{
+ name: "#{value}.",
+ type: "AAAA",
+ ttl: 8600,
+ records: [%{content: ip, disabled: false}]
+ }
+
+ if(rev_update?,
+ do: PowerDNSex.update_record(rev_zone, record),
+ else: PowerDNSex.create_record(rev_zone, record)
+ )
end
+
{:ok, zone} = PowerDNSex.show_zone(ptr_zone)
- update? = Enum.any?(zone.rrsets, fn(rr) -> rr.name == "#{ip_fqdn}." end)
- record = %{name: "#{ip_fqdn}.", type: "PTR", ttl: 3600, records: [%{content: "#{value}.", disabled: false}]}
- pdns = if(update?, do: PowerDNSex.update_record(zone, record), else: PowerDNSex.create_record(zone, record))
+ update? = Enum.any?(zone.rrsets, fn rr -> rr.name == "#{ip_fqdn}." end)
+
+ record = %{
+ name: "#{ip_fqdn}.",
+ type: "PTR",
+ ttl: 3600,
+ records: [%{content: "#{value}.", disabled: false}]
+ }
+
+ pdns =
+ if(update?,
+ do: PowerDNSex.update_record(zone, record),
+ else: PowerDNSex.create_record(zone, record)
+ )
+
:ok
end
@@ -76,9 +102,10 @@ defmodule Nola.Subnet do
defp available_select(dets) do
spec = [{{:"$1", :"$2"}, [is_integer: :"$2"], [{{:"$1", :"$2"}}]}]
{subnets, _} = :dets.select(dets, spec, 20)
- subnet = subnets
- |> Enum.sort_by(fn({_, last}) -> last end)
- |> List.first()
- end
+ subnet =
+ subnets
+ |> Enum.sort_by(fn {_, last} -> last end)
+ |> List.first()
+ end
end
diff --git a/lib/nola/token.ex b/lib/nola/token.ex
index 179bed2..f4fdd86 100644
--- a/lib/nola/token.ex
+++ b/lib/nola/token.ex
@@ -2,15 +2,15 @@ defmodule Nola.Token do
use GenServer
def start_link() do
- GenServer.start_link(__MODULE__, [], [name: __MODULE__])
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def lookup(id) do
- with \
- [{_, cred, date}] <- :ets.lookup(__MODULE__.ETS, id),
- IO.inspect("cred: #{inspect cred} valid for #{inspect date} now #{inspect DateTime.utc_now()}"),
- d when d > 0 <- DateTime.diff(date, DateTime.utc_now())
- do
+ with [{_, cred, date}] <- :ets.lookup(__MODULE__.ETS, id),
+ IO.inspect(
+ "cred: #{inspect(cred)} valid for #{inspect(date)} now #{inspect(DateTime.utc_now())}"
+ ),
+ d when d > 0 <- DateTime.diff(date, DateTime.utc_now()) do
{:ok, cred}
else
err -> {:error, err}
@@ -22,17 +22,21 @@ defmodule Nola.Token do
end
def init(_) do
- ets = :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
+ ets =
+ :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
+
{:ok, ets}
end
def handle_call({:new, cred}, _, ets) do
id = Nola.UserTrack.Id.large_id()
- expire = DateTime.utc_now()
- |> DateTime.add(15*60, :second)
+
+ expire =
+ DateTime.utc_now()
+ |> DateTime.add(15 * 60, :second)
+
obj = {id, cred, expire}
:ets.insert(ets, obj)
{:reply, {:ok, id}, ets}
end
-
end
diff --git a/lib/nola/trigger.ex b/lib/nola/trigger.ex
index 1dec9ac..d3e791b 100644
--- a/lib/nola/trigger.ex
+++ b/lib/nola/trigger.ex
@@ -8,5 +8,4 @@ defmodule Nola.Trigger do
:trigger,
:args
]
-
end
diff --git a/lib/nola/user_track.ex b/lib/nola/user_track.ex
index c1218b0..0b07a91 100644
--- a/lib/nola/user_track.ex
+++ b/lib/nola/user_track.ex
@@ -8,23 +8,23 @@ defmodule Nola.UserTrack do
# Privilege map:
# %{"#channel" => [:operator, :voice]
defmodule Storage do
-
def delete(id) do
- op(fn(ets) -> :ets.delete(ets, id) end)
+ op(fn ets -> :ets.delete(ets, id) end)
end
def insert(tuple) do
- op(fn(ets) -> :ets.insert(ets, tuple) end)
+ op(fn ets -> :ets.insert(ets, tuple) end)
end
def clear_network(network) do
- op(fn(ets) ->
+ op(fn ets ->
spec = [
{{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_, :_},
- [
- {:==, :"$1", {:const, network}}
- ], [:"$_"]}
+ [
+ {:==, :"$1", {:const, network}}
+ ], [:"$_"]}
]
+
:ets.match_delete(ets, spec)
end)
end
@@ -34,7 +34,7 @@ defmodule Nola.UserTrack do
end
def start_link do
- GenServer.start_link(__MODULE__, [], [name: __MODULE__])
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
@@ -43,13 +43,15 @@ defmodule Nola.UserTrack do
end
def handle_call({:op, fun}, _from, ets) do
- returned = try do
- {:ok, fun.(ets)}
- rescue
- rescued -> {:error, rescued}
- catch
- rescued -> {:error, rescued}
- end
+ returned =
+ try do
+ {:ok, fun.(ets)}
+ rescue
+ rescued -> {:error, rescued}
+ catch
+ rescued -> {:error, rescued}
+ end
+
{:reply, returned, ets}
end
@@ -58,31 +60,63 @@ defmodule Nola.UserTrack do
end
end
- defmodule Id, do: use EntropyString
+ defmodule Id, do: use(EntropyString)
defmodule User do
- defstruct [:id, :account, :network, :nick, {:nicks, []}, :username, :host, :realname, {:privileges, %{}}, {:last_active, %{}}, {:options, %{}}]
+ defstruct [
+ :id,
+ :account,
+ :network,
+ :nick,
+ {:nicks, []},
+ :username,
+ :host,
+ :realname,
+ {:privileges, %{}},
+ {:last_active, %{}},
+ {:options, %{}}
+ ]
def to_tuple(u = %__MODULE__{}) do
- {u.id || Nola.UserTrack.Id.large_id, u.network, u.account, String.downcase(u.nick), u.nick, u.nicks || [], u.username, u.host, u.realname, u.privileges, u.last_active, u.options}
+ {u.id || Nola.UserTrack.Id.large_id(), u.network, u.account, String.downcase(u.nick),
+ u.nick, u.nicks || [], u.username, u.host, u.realname, u.privileges, u.last_active,
+ u.options}
end
- #tuple size: 11
- def from_tuple({id, network, account, _downcased_nick, nick, nicks, username, host, realname, privs, last_active, opts}) do
- struct = %__MODULE__{id: id, account: account, network: network, nick: nick, nicks: nicks, username: username, host: host, realname: realname, privileges: privs, last_active: last_active, options: opts}
+ # tuple size: 11
+ def from_tuple(
+ {id, network, account, _downcased_nick, nick, nicks, username, host, realname, privs,
+ last_active, opts}
+ ) do
+ struct = %__MODULE__{
+ id: id,
+ account: account,
+ network: network,
+ nick: nick,
+ nicks: nicks,
+ username: username,
+ host: host,
+ realname: realname,
+ privileges: privs,
+ last_active: last_active,
+ options: opts
+ }
end
end
def find_by_account(%Nola.Account{id: id}) do
- #iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
+ # iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
spec = [
{{:_, :_, :"$2", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
- {:==, :"$2", {:const, id}}
+ {:==, :"$2", {:const, id}}
], [:"$_"]}
]
- results = :ets.select(@ets, spec)
- |> Enum.filter(& &1)
+
+ results =
+ :ets.select(@ets, spec)
+ |> Enum.filter(& &1)
+
for obj <- results, do: User.from_tuple(obj)
end
@@ -91,27 +125,39 @@ defmodule Nola.UserTrack do
end
def find_by_account(network, %Nola.Account{id: id}) do
- #iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
+ # iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
spec = [
{{:_, :"$1", :"$2", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
- {:andalso, {:==, :"$1", {:const, network}},
- {:==, :"$2", {:const, id}}}
+ {:andalso, {:==, :"$1", {:const, network}}, {:==, :"$2", {:const, id}}}
], [:"$_"]}
]
+
case :ets.select(@ets, spec) do
results = [_r | _] ->
- result = results
- |> Enum.reject(fn({_, net, _, _, _, _, _, _, _, _, actives, opts}) -> network != "matrix" && net == "matrix" end)
- |> Enum.reject(fn({_, net, _, _, _, _, _, _, _, _, actives, opts}) -> network != "telegram" && net == "telegram" end)
- |> Enum.reject(fn({_, _, _, _, _, _, _, _, _, _, actives, opts}) -> network not in ["matrix", "telegram"] && Map.get(opts, :puppet) end)
- |> Enum.sort_by(fn({_, _, _, _, _, _, _, _, _, _, actives, _}) ->
- Map.get(actives, nil)
- end, {:desc, NaiveDateTime})
- |> List.first
+ result =
+ results
+ |> Enum.reject(fn {_, net, _, _, _, _, _, _, _, _, actives, opts} ->
+ network != "matrix" && net == "matrix"
+ end)
+ |> Enum.reject(fn {_, net, _, _, _, _, _, _, _, _, actives, opts} ->
+ network != "telegram" && net == "telegram"
+ end)
+ |> Enum.reject(fn {_, _, _, _, _, _, _, _, _, _, actives, opts} ->
+ network not in ["matrix", "telegram"] && Map.get(opts, :puppet)
+ end)
+ |> Enum.sort_by(
+ fn {_, _, _, _, _, _, _, _, _, _, actives, _} ->
+ Map.get(actives, nil)
+ end,
+ {:desc, NaiveDateTime}
+ )
+ |> List.first()
if result, do: User.from_tuple(result)
- _ -> nil
+
+ _ ->
+ nil
end
end
@@ -119,18 +165,23 @@ defmodule Nola.UserTrack do
Storage.clear_network(network)
end
-
def merge_account(old_id, new_id) do
- #iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
+ # iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
spec = [
{{:_, :_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
- {:==, :"$1", {:const, old_id}}
+ {:==, :"$1", {:const, old_id}}
], [:"$_"]}
]
- Enum.each(:ets.select(@ets, spec), fn({id, net, _, downcased_nick, nick, nicks, username, host, realname, privs, active, opts}) ->
- Storage.op(fn(ets) ->
- :ets.insert(@ets, {id, net, new_id, downcased_nick, nick, nicks, username, host, realname, privs, active, opts})
+
+ Enum.each(:ets.select(@ets, spec), fn {id, net, _, downcased_nick, nick, nicks, username,
+ host, realname, privs, active, opts} ->
+ Storage.op(fn ets ->
+ :ets.insert(
+ @ets,
+ {id, net, new_id, downcased_nick, nick, nicks, username, host, realname, privs, active,
+ opts}
+ )
end)
end)
end
@@ -139,14 +190,18 @@ defmodule Nola.UserTrack do
find_by_nick(network, nick)
end
-
def find_by_nick(%ExIRC.SenderInfo{network: network, nick: nick}) do
find_by_nick(network, nick)
end
def find_by_nick(network, nick) do
- case :ets.match(@ets, {:"$1", network, :_, String.downcase(nick), :_, :_, :_, :_, :_, :_, :_, :_}) do
- [[id] | _] -> lookup(id)
+ case :ets.match(
+ @ets,
+ {:"$1", network, :_, String.downcase(nick), :_, :_, :_, :_, :_, :_, :_, :_}
+ ) do
+ [[id] | _] ->
+ lookup(id)
+
_ ->
nil
end
@@ -171,7 +226,7 @@ defmodule Nola.UserTrack do
end
def channel(network, channel) do
- Enum.filter(to_list(), fn({_, network, _, _, _, _, _, _, _, channels, _, _}) ->
+ Enum.filter(to_list(), fn {_, network, _, _, _, _, _, _, _, channels, _, _} ->
Map.get(channels, channel)
end)
end
@@ -179,53 +234,92 @@ defmodule Nola.UserTrack do
# TODO
def connected(network, nick, user, host, account_id, opts \\ %{}) do
if account = Nola.Account.get(account_id) do
- user = if user = find_by_nick(network, nick) do
- user
- else
- user = %User{id: Nola.UserTrack.Id.large_id, account: account_id, network: network, nick: nick, username: user, host: host, privileges: %{}, options: opts}
- Storage.op(fn(ets) ->
- :ets.insert(ets, User.to_tuple(user))
- end)
- user
- end
+ user =
+ if user = find_by_nick(network, nick) do
+ user
+ else
+ user = %User{
+ id: Nola.UserTrack.Id.large_id(),
+ account: account_id,
+ network: network,
+ nick: nick,
+ username: user,
+ host: host,
+ privileges: %{},
+ options: opts
+ }
+
+ Storage.op(fn ets ->
+ :ets.insert(ets, User.to_tuple(user))
+ end)
+
+ user
+ end
+
+ Nola.Irc.Connection.publish_event(network, %{
+ type: :connect,
+ user_id: user.id,
+ account_id: user.account
+ })
- Nola.Irc.Connection.publish_event(network, %{type: :connect, user_id: user.id, account_id: user.account})
:ok
else
:error
end
end
- def joined(c, s), do: joined(c,s,[])
+ def joined(c, s), do: joined(c, s, [])
- def joined(channel, sender=%{nick: nick, user: uname, host: host}, privileges, touch \\ true) do
- privileges = if Nola.Irc.admin?(sender) do
- privileges ++ [:admin]
- else privileges end
- user = if user = find_by_nick(sender.network, nick) do
- %User{user | username: uname, host: host, privileges: Map.put(user.privileges || %{}, channel, privileges)}
- else
- user = %User{id: Nola.UserTrack.Id.large_id, network: sender.network, nick: nick, username: uname, host: host, privileges: %{channel => privileges}}
+ def joined(channel, sender = %{nick: nick, user: uname, host: host}, privileges, touch \\ true) do
+ privileges =
+ if Nola.Irc.admin?(sender) do
+ privileges ++ [:admin]
+ else
+ privileges
+ end
+
+ user =
+ if user = find_by_nick(sender.network, nick) do
+ %User{
+ user
+ | username: uname,
+ host: host,
+ privileges: Map.put(user.privileges || %{}, channel, privileges)
+ }
+ else
+ user = %User{
+ id: Nola.UserTrack.Id.large_id(),
+ network: sender.network,
+ nick: nick,
+ username: uname,
+ host: host,
+ privileges: %{channel => privileges}
+ }
+
+ account = Nola.Account.lookup(user).id
+ user = %User{user | account: account}
+ end
- account = Nola.Account.lookup(user).id
- user = %User{user | account: account}
- end
user = touch_struct(user, channel)
if touch && user.account do
Nola.Membership.touch(user.account, sender.network, channel)
end
- Storage.op(fn(ets) ->
+ Storage.op(fn ets ->
:ets.insert(ets, User.to_tuple(user))
end)
- Nola.Irc.Connection.publish_event({sender.network, channel}, %{type: :join, user_id: user.id, account_id: user.account})
+ Nola.Irc.Connection.publish_event({sender.network, channel}, %{
+ type: :join,
+ user_id: user.id,
+ account_id: user.account
+ })
user
end
- #def joined(network, channel, nick, privileges) do
+ # def joined(network, channel, nick, privileges) do
# user = if user = find_by_nick(network, nick) do
# %User{user | privileges: Map.put(user.privileges, channel, privileges)}
# else
@@ -235,18 +329,24 @@ defmodule Nola.UserTrack do
# Storage.op(fn(ets) ->
# :ets.insert(ets, User.to_tuple(user))
# end)
- #end
+ # end
+
+ def messaged(
+ %Nola.Message{network: network, account: account, channel: chan, sender: %{nick: nick}} =
+ m
+ ) do
+ {user, account} =
+ if user = find_by_nick(network, nick) do
+ {touch_struct(user, chan), account || Nola.Account.lookup(user)}
+ else
+ user = %User{network: network, nick: nick, privileges: %{}}
+ account = Nola.Account.lookup(user)
+ {%User{user | account: account.id}, account}
+ end
- def messaged(%Nola.Message{network: network, account: account, channel: chan, sender: %{nick: nick}} = m) do
- {user, account} = if user = find_by_nick(network, nick) do
- {touch_struct(user, chan), account || Nola.Account.lookup(user)}
- else
- user = %User{network: network, nick: nick, privileges: %{}}
- account = Nola.Account.lookup(user)
- {%User{user | account: account.id}, account}
- end
Storage.insert(User.to_tuple(user))
if chan, do: Nola.Membership.touch(account, network, chan)
+
if !m.account do
{:ok, %Nola.Message{m | account: account}}
else
@@ -257,12 +357,19 @@ defmodule Nola.UserTrack do
def renamed(network, old_nick, new_nick) do
if user = find_by_nick(network, old_nick) do
old_account = Nola.Account.lookup(user)
- user = %User{user | nick: new_nick, nicks: [old_nick|user.nicks]}
+ user = %User{user | nick: new_nick, nicks: [old_nick | user.nicks]}
account = Nola.Account.lookup(user, false) || old_account
- user = %User{user | nick: new_nick, account: account.id, nicks: [old_nick|user.nicks]}
+ user = %User{user | nick: new_nick, account: account.id, nicks: [old_nick | user.nicks]}
Storage.insert(User.to_tuple(user))
channels = for {channel, _} <- user.privileges, do: channel
- Nola.Irc.Connection.publish_event(network, %{type: :nick, user_id: user.id, account_id: account.id, nick: new_nick, old_nick: old_nick})
+
+ Nola.Irc.Connection.publish_event(network, %{
+ type: :nick,
+ user_id: user.id,
+ account_id: account.id,
+ nick: new_nick,
+ old_nick: old_nick
+ })
end
end
@@ -270,12 +377,19 @@ defmodule Nola.UserTrack do
if user = find_by_nick(network, nick) do
privs = Map.get(user.privileges, channel)
- privs = Enum.reduce(add, privs, fn(priv, acc) -> [priv|acc] end)
- privs = Enum.reduce(remove, privs, fn(priv, acc) -> List.delete(acc, priv) end)
+ privs = Enum.reduce(add, privs, fn priv, acc -> [priv | acc] end)
+ privs = Enum.reduce(remove, privs, fn priv, acc -> List.delete(acc, priv) end)
user = %User{user | privileges: Map.put(user.privileges, channel, privs)}
Storage.insert(User.to_tuple(user))
- Nola.Irc.Connection.publish_event({network, channel}, %{type: :privileges, user_id: user.id, account_id: user.account, added: add, removed: remove})
+
+ Nola.Irc.Connection.publish_event({network, channel}, %{
+ type: :privileges,
+ user_id: user.id,
+ account_id: user.account,
+ added: add,
+ removed: remove
+ })
end
end
@@ -292,12 +406,25 @@ defmodule Nola.UserTrack do
privs = Map.delete(user.privileges, channel)
lasts = Map.delete(user.last_active, channel)
+
if Enum.count(privs) > 0 do
user = %User{user | privileges: privs}
Storage.insert(User.to_tuple(user))
- Nola.Irc.Connection.publish_event({network, channel}, %{type: :part, user_id: user.id, account_id: user.account, reason: nil})
+
+ Nola.Irc.Connection.publish_event({network, channel}, %{
+ type: :part,
+ user_id: user.id,
+ account_id: user.account,
+ reason: nil
+ })
else
- Nola.Irc.Connection.publish_event(network, %{type: :quit, user_id: user.id, account_id: user.account, reason: "Left all known channels"})
+ Nola.Irc.Connection.publish_event(network, %{
+ type: :quit,
+ user_id: user.id,
+ account_id: user.account,
+ reason: "Left all known channels"
+ })
+
Storage.delete(user.id)
end
end
@@ -309,17 +436,27 @@ defmodule Nola.UserTrack do
for {channel, _} <- user.privileges do
Nola.Membership.touch(user.account, sender.network, channel)
end
- Nola.Irc.Connection.publish_event(sender.network, %{type: :quit, user_id: user.id, account_id: user.account, reason: reason})
+
+ Nola.Irc.Connection.publish_event(sender.network, %{
+ type: :quit,
+ user_id: user.id,
+ account_id: user.account,
+ reason: reason
+ })
end
+
Storage.delete(user.id)
end
end
defp touch_struct(user = %User{last_active: last_active}, channel) do
now = NaiveDateTime.utc_now()
- last_active = last_active
- |> Map.put(channel, now)
- |> Map.put(nil, now)
+
+ last_active =
+ last_active
+ |> Map.put(channel, now)
+ |> Map.put(nil, now)
+
%User{user | last_active: last_active}
end
diff --git a/lib/open_ai.ex b/lib/open_ai.ex
index cc0de27..2b8783f 100644
--- a/lib/open_ai.ex
+++ b/lib/open_ai.ex
@@ -1,20 +1,40 @@
defmodule OpenAi do
+ require Logger
def post(path, data, options \\ []) do
config = Application.get_env(:nola, :openai, [])
- url = "https://api.openai.com#{path}"
- headers = [{"user-agent", "internal private experiment bot, href@random.sh"},
- {"content-type", "application/json"},
- {"authorization", "Bearer " <> Keyword.get(config, :key, "unset-api-key")}]
- options = options ++ [timeout: :timer.seconds(180), recv_timeout: :timer.seconds(180)]
+ base_url = Keyword.get(config, :base_url, "https://api.openai.com")
+ url = "#{base_url}#{path}"
+
+ headers = [
+ {"user-agent", "internal private experiment bot, href@random.sh"},
+ {"content-type", "application/json"},
+ {"authorization", "Bearer " <> Keyword.get(config, :key, "unset-api-key")}
+ ]
+
+ options = options ++ [timeout: :timer.seconds(30), recv_timeout: :timer.seconds(30)]
+ Logger.debug("openai: post: #{url} #{inspect(data)}")
+
with {:ok, json} <- Poison.encode(data),
- {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.post(url, json, headers, options),
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+ HTTPoison.post(url, json, headers, options),
{:ok, data} <- Poison.decode(body) do
{:ok, data}
else
- {:ok, %HTTPoison.Response{status_code: code}} -> {:error, Plug.Conn.Status.reason_atom(code)}
- {:error, %HTTPoison.Error{reason: reason}} -> {:error, reason}
+ {:ok, %HTTPoison.Response{status_code: code, body: body}} ->
+ Logger.error("OpenAI: HTTP #{code} #{inspect(body)}")
+ status = Plug.Conn.Status.reason_atom(code)
+
+ case Poison.decode(body) do
+ {:ok, %{"error" => %{"message" => message, "code" => code}}} ->
+ {:error, {status, message}}
+
+ kek ->
+ {:error, status}
+ end
+
+ {:error, %HTTPoison.Error{reason: reason}} ->
+ {:error, reason}
end
end
-
end
diff --git a/lib/plugins/account.ex b/lib/plugins/account.ex
index 0377e1c..2977f4b 100644
--- a/lib/plugins/account.ex
+++ b/lib/plugins/account.ex
@@ -1,187 +1,219 @@
defmodule Nola.Plugins.Account do
- @moduledoc """
- # Account
-
- * **account** Get current account id and token
- * **auth `<account-id>` `<token>`** Authenticate and link the current nickname to an account
- * **auth** list authentications methods
- * **whoami** list currently authenticated users
- * **web** get a one-time login link to web
- * **enable-telegram** Link a Telegram account
- * **enable-sms** Link a SMS number
- * **enable-untappd** Link a Untappd account
- * **set-name** set account name
- * **setusermeta puppet-nick `<nick>`** Set puppet IRC nickname
- """
-
- def irc_doc, do: @moduledoc
- def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
- def init(_) do
- {:ok, _} = Registry.register(Nola.PubSub, "messages:private", [])
- {:ok, nil}
- end
+ @moduledoc """
+ # Account
+
+ * **account** Get current account id and token
+ * **auth `<account-id>` `<token>`** Authenticate and link the current nickname to an account
+ * **auth** list authentications methods
+ * **whoami** list currently authenticated users
+ * **web** get a one-time login link to web
+ * **enable-telegram** Link a Telegram account
+ * **enable-sms** Link a SMS number
+ * **enable-untappd** Link a Untappd account
+ * **set-name** set account name
+ * **setusermeta puppet-nick `<nick>`** Set puppet IRC nickname
+ """
+
+ def irc_doc, do: @moduledoc
+ def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
+
+ def init(_) do
+ {:ok, _} = Registry.register(Nola.PubSub, "messages:private", [])
+ {:ok, nil}
+ end
- def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "help"}}, state) do
- text = [
- "account: show current account and auth token",
- "auth: show authentications methods",
- "whoami: list authenticated users",
- "set-name <name>: set account name",
- "web: login to web",
- "enable-sms | disable-sms: enable/change or disable sms",
- "enable-telegram: link/change telegram",
- "enable-untappd: link untappd account",
- "getmeta: show meta datas",
- "setusermeta: set user meta",
- ]
- m.replyfun.(text)
- {:noreply, state}
- end
+ def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "help"}}, state) do
+ text = [
+ "account: show current account and auth token",
+ "auth: show authentications methods",
+ "whoami: list authenticated users",
+ "set-name <name>: set account name",
+ "web: login to web",
+ "enable-sms | disable-sms: enable/change or disable sms",
+ "enable-telegram: link/change telegram",
+ "enable-untappd: link untappd account",
+ "getmeta: show meta datas",
+ "setusermeta: set user meta"
+ ]
+
+ m.replyfun.(text)
+ {:noreply, state}
+ end
- def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "auth"}}, state) do
- spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
- predicates = :dets.select(Nola.Account.file("predicates"), spec)
- text = for {net, {key, value}} <- predicates, do: "#{net}: #{to_string(key)}: #{value}"
- m.replyfun.(text)
- {:noreply, state}
- end
+ def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "auth"}}, state) do
+ spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
+ predicates = :dets.select(Nola.Account.file("predicates"), spec)
+ text = for {net, {key, value}} <- predicates, do: "#{net}: #{to_string(key)}: #{value}"
+ m.replyfun.(text)
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "whoami"}}, state) do
+ users =
+ for user <- Nola.UserTrack.find_by_account(m.account) do
+ chans =
+ Enum.map(user.privileges, fn {chan, _} -> chan end)
+ |> Enum.join(" ")
- def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "whoami"}}, state) do
- users = for user <- Nola.UserTrack.find_by_account(m.account) do
- chans = Enum.map(user.privileges, fn({chan, _}) -> chan end)
- |> Enum.join(" ")
"#{user.network} - #{user.nick}!#{user.username}@#{user.host} - #{chans}"
end
- m.replyfun.(users)
- {:noreply, state}
- end
- def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "account"}}, state) do
- account = Nola.Account.lookup(m.sender)
- text = ["Account Id: #{account.id}",
- "Authenticate to this account from another network: \"auth #{account.id} #{account.token}\" to the other bot!"]
- m.replyfun.(text)
- {:noreply, state}
- end
+ m.replyfun.(users)
+ {:noreply, state}
+ end
- def handle_info({:irc, :text, m = %Nola.Message{sender: sender, text: "auth"<>_}}, state) do
- #account = Nola.Account.lookup(m.sender)
- case String.split(m.text, " ") do
- ["auth", id, token] ->
- join_account(m, id, token)
- _ ->
- m.replyfun.("Invalid parameters")
- end
- {:noreply, state}
- end
+ def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "account"}}, state) do
+ account = Nola.Account.lookup(m.sender)
- def handle_info({:irc, :text, m = %Nola.Message{account: account, text: "set-name "<>name}}, state) do
- Nola.Account.update_account_name(account, name)
- m.replyfun.("Name changed: #{name}")
- {:noreply, state}
- end
+ text = [
+ "Account Id: #{account.id}",
+ "Authenticate to this account from another network: \"auth #{account.id} #{account.token}\" to the other bot!"
+ ]
- def handle_info({:irc, :text, m = %Nola.Message{text: "disable-sms"}}, state) do
- if Nola.Account.get_meta(m.account, "sms-number") do
- Nola.Account.delete_meta(m.account, "sms-number")
- m.replyfun.("SMS disabled.")
- else
- m.replyfun.("SMS already disabled.")
- end
- {:noreply, state}
- end
+ m.replyfun.(text)
+ {:noreply, state}
+ end
- def handle_info({:irc, :text, m = %Nola.Message{text: "web"}}, state) do
- login_url = Nola.AuthToken.new_url(m.account.id, nil)
- m.replyfun.("↪:" <> login_url)
- {:noreply, state}
- end
+ def handle_info({:irc, :text, m = %Nola.Message{sender: sender, text: "auth" <> _}}, state) do
+ # account = Nola.Account.lookup(m.sender)
+ case String.split(m.text, " ") do
+ ["auth", id, token] ->
+ join_account(m, id, token)
- def handle_info({:irc, :text, m = %Nola.Message{text: "enable-sms"}}, state) do
- code = String.downcase(EntropyString.small_id())
- Nola.Account.put_meta(m.account, "sms-validation-code", code)
- Nola.Account.put_meta(m.account, "sms-validation-target", m.network)
- number = Nola.Plugins.Sms.my_number()
- text = "To enable or change your number for SMS messaging, please send:"
- <> " \"enable #{code}\" to #{number}"
- m.replyfun.(text)
- {:noreply, state}
+ _ ->
+ m.replyfun.("Invalid parameters")
end
- def handle_info({:irc, :text, m = %Nola.Message{text: "enable-telegram"}}, state) do
- code = String.downcase(EntropyString.small_id())
- Nola.Account.delete_meta(m.account, "telegram-id")
- Nola.Account.put_meta(m.account, "telegram-validation-code", code)
- Nola.Account.put_meta(m.account, "telegram-validation-target", m.network)
- text = "To enable or change your number for telegram messaging, please open #{Nola.Telegram.my_path()} and send:"
- <> " \"/enable #{code}\""
- m.replyfun.(text)
- {:noreply, state}
- end
+ {:noreply, state}
+ end
+
+ def handle_info(
+ {:irc, :text, m = %Nola.Message{account: account, text: "set-name " <> name}},
+ state
+ ) do
+ Nola.Account.update_account_name(account, name)
+ m.replyfun.("Name changed: #{name}")
+ {:noreply, state}
+ end
- def handle_info({:irc, :text, m = %Nola.Message{text: "enable-untappd"}}, state) do
- auth_url = Untappd.auth_url()
- login_url = Nola.AuthToken.new_url(m.account.id, {:external_redirect, auth_url})
- m.replyfun.(["To link your Untappd account, open this URL:", login_url])
- {:noreply, state}
+ def handle_info({:irc, :text, m = %Nola.Message{text: "disable-sms"}}, state) do
+ if Nola.Account.get_meta(m.account, "sms-number") do
+ Nola.Account.delete_meta(m.account, "sms-number")
+ m.replyfun.("SMS disabled.")
+ else
+ m.replyfun.("SMS already disabled.")
end
- def handle_info({:irc, :text, m = %Nola.Message{text: "getmeta"<>_}}, state) do
- result = case String.split(m.text, " ") do
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :text, m = %Nola.Message{text: "web"}}, state) do
+ login_url = Nola.AuthToken.new_url(m.account.id, nil)
+ m.replyfun.("↪:" <> login_url)
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :text, m = %Nola.Message{text: "enable-sms"}}, state) do
+ code = String.downcase(EntropyString.small_id())
+ Nola.Account.put_meta(m.account, "sms-validation-code", code)
+ Nola.Account.put_meta(m.account, "sms-validation-target", m.network)
+ number = Nola.Plugins.Sms.my_number()
+
+ text =
+ "To enable or change your number for SMS messaging, please send:" <>
+ " \"enable #{code}\" to #{number}"
+
+ m.replyfun.(text)
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :text, m = %Nola.Message{text: "enable-telegram"}}, state) do
+ code = String.downcase(EntropyString.small_id())
+ Nola.Account.delete_meta(m.account, "telegram-id")
+ Nola.Account.put_meta(m.account, "telegram-validation-code", code)
+ Nola.Account.put_meta(m.account, "telegram-validation-target", m.network)
+
+ text =
+ "To enable or change your number for telegram messaging, please open #{Nola.Telegram.my_path()} and send:" <>
+ " \"/enable #{code}\""
+
+ m.replyfun.(text)
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :text, m = %Nola.Message{text: "enable-untappd"}}, state) do
+ auth_url = Untappd.auth_url()
+ login_url = Nola.AuthToken.new_url(m.account.id, {:external_redirect, auth_url})
+ m.replyfun.(["To link your Untappd account, open this URL:", login_url])
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :text, m = %Nola.Message{text: "getmeta" <> _}}, state) do
+ result =
+ case String.split(m.text, " ") do
["getmeta"] ->
for {k, v} <- Nola.Account.get_all_meta(m.account) do
case k do
- "u:"<>key -> "(user) #{key}: #{v}"
+ "u:" <> key -> "(user) #{key}: #{v}"
key -> "#{key}: #{v}"
end
end
+
["getmeta", key] ->
value = Nola.Account.get_meta(m.account, key)
- text = if value do
- "#{key}: #{value}"
- else
- "#{key} is not defined"
- end
+
+ text =
+ if value do
+ "#{key}: #{value}"
+ else
+ "#{key} is not defined"
+ end
+
_ ->
"usage: getmeta [key]"
end
- m.replyfun.(result)
- {:noreply, state}
- end
- def handle_info({:irc, :text, m = %Nola.Message{text: "setusermeta"<>_}}, state) do
- result = case String.split(m.text, " ") do
+ m.replyfun.(result)
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :text, m = %Nola.Message{text: "setusermeta" <> _}}, state) do
+ result =
+ case String.split(m.text, " ") do
["setusermeta", key, value] ->
Nola.Account.put_user_meta(m.account, key, value)
"ok"
+
_ ->
"usage: setusermeta <key> <value>"
end
- m.replyfun.(result)
- {:noreply, state}
- end
- def handle_info(_, state) do
- {:noreply, state}
- end
+ m.replyfun.(result)
+ {:noreply, state}
+ end
- defp join_account(m, id, token) do
- old_account = Nola.Account.lookup(m.sender)
- new_account = Nola.Account.get(id)
- if new_account && token == new_account.token do
- case Nola.Account.merge_account(old_account.id, new_account.id) do
- :ok ->
- if old_account.id == new_account.id do
- m.replyfun.("Already authenticated, but hello")
- else
- m.replyfun.("Accounts merged!")
- end
- _ -> m.replyfun.("Something failed :(")
- end
- else
- m.replyfun.("Invalid token")
+ def handle_info(_, state) do
+ {:noreply, state}
+ end
+
+ defp join_account(m, id, token) do
+ old_account = Nola.Account.lookup(m.sender)
+ new_account = Nola.Account.get(id)
+
+ if new_account && token == new_account.token do
+ case Nola.Account.merge_account(old_account.id, new_account.id) do
+ :ok ->
+ if old_account.id == new_account.id do
+ m.replyfun.("Already authenticated, but hello")
+ else
+ m.replyfun.("Accounts merged!")
+ end
+
+ _ ->
+ m.replyfun.("Something failed :(")
end
+ else
+ m.replyfun.("Invalid token")
end
-
end
-
+end
diff --git a/lib/plugins/alcoolog.ex b/lib/plugins/alcoolog.ex
index 69bd60c..de69b13 100644
--- a/lib/plugins/alcoolog.ex
+++ b/lib/plugins/alcoolog.ex
@@ -49,27 +49,40 @@ defmodule Nola.Plugins.Alcoolog do
@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_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)
+ triggers = for(t <- @pubsub_triggers, do: "trigger:" <> t)
+
for sub <- @pubsub ++ triggers do
{:ok, _} = Registry.register(Nola.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) ->
+
+ 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()
+ 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)
@@ -77,9 +90,11 @@ defmodule Nola.Plugins.Alcoolog do
dets
object = {nick, naive = %NaiveDateTime{}, volumes, active, cl, deg, name, comment, meta} ->
- date = naive
- |> DateTime.from_naive!("Etc/UTC")
- |> DateTime.to_unix()
+ 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)
@@ -94,6 +109,7 @@ defmodule Nola.Plugins.Alcoolog do
dets
end
end
+
:dets.foldl(traverse_fun, dets, dets)
:dets.sync(dets)
state = %{dets: dets, meta: meta, ets: ets}
@@ -101,59 +117,82 @@ defmodule Nola.Plugins.Alcoolog do
end
@eau ["santo", "santeau"]
- def handle_info({:irc, :trigger, santeau, m = %Nola.Message{trigger: %Nola.Trigger{args: _, type: :bang}}}, state) when santeau in @eau do
+ def handle_info(
+ {:irc, :trigger, santeau,
+ m = %Nola.Message{trigger: %Nola.Trigger{args: _, type: :bang}}},
+ state
+ )
+ when santeau in @eau do
Nola.Plugins.Txt.reply_random(m, "alcoolog.santo")
{:noreply, state}
end
- def handle_info({:irc, :trigger, "soif", m = %Nola.Message{trigger: %Nola.Trigger{args: _, type: :bang}}}, state) do
- now = DateTime.utc_now()
- |> Timex.Timezone.convert("Europe/Paris")
+ def handle_info(
+ {:irc, :trigger, "soif", m = %Nola.Message{trigger: %Nola.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()
+ {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
@@ -161,99 +200,151 @@ defmodule Nola.Plugins.Alcoolog do
{:noreply, state}
end
- def handle_info({:irc, :trigger, "sobrepour", m = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :bang}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "sobrepour",
+ m = %Nola.Message{trigger: %Nola.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
+
+ 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)
+ duration = round(DateTime.diff(time, now) / 60.0)
- IO.puts "diff #{inspect duration} sober in #{inspect stats.sober_in}"
+ 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")
+ 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 = %Nola.Message{trigger: %Nola.Trigger{args: [], type: :plus}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "alcoolog",
+ m = %Nola.Message{trigger: %Nola.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)
+
+ 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 = %Nola.Message{trigger: %Nola.Trigger{args: [], type: :bang}}}, state) do
- url = NolaWeb.Router.Helpers.alcoolog_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel))
+ def handle_info(
+ {:irc, :trigger, "alcoolog",
+ m = %Nola.Message{trigger: %Nola.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 = %Nola.Message{trigger: %Nola.Trigger{args: args = [cl, deg], type: :bang}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "alcool",
+ m = %Nola.Message{trigger: %Nola.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
+ 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
+ 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})"
+ )
- 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 = %Nola.Message{trigger: %Nola.Trigger{args: [cl, deg | comment], type: :bang}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "santai",
+ m = %Nola.Message{trigger: %Nola.Trigger{args: [cl, deg | comment], type: :bang}}},
+ state
+ ) do
santai(m, state, cl, deg, comment)
{:noreply, state}
end
@@ -263,80 +354,138 @@ defmodule Nola.Plugins.Alcoolog do
"{{message.sender.nick}}: et voilà la petite sœur !"
]
- def handle_info({:irc, :trigger, "bis", m = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :bang}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "bis",
+ m = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :bang}}},
+ state
+ ) do
handle_info({:irc, :trigger, "moar", m}, state)
end
- def handle_info({:irc, :trigger, "again", m = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :bang}}}, state) do
+
+ def handle_info(
+ {:irc, :trigger, "again",
+ m = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :bang}}},
+ state
+ ) do
handle_info({:irc, :trigger, "moar", m}, state)
end
- def handle_info({:irc, :trigger, "moar", m = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :bang}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "moar",
+ m = %Nola.Message{trigger: %Nola.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
+ 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")
+ 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
+
+ _ ->
+ 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
+ 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
+ {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
- {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")}
- _ ->
+ {: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
+ end
+ end
cond do
- cl == nil -> m.replyfun.(cl_extra)
- deg == nil -> m.replyfun.(comment)
- cl >= 500 || deg >= 100 -> Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_toohuge")
- cl == 0 || deg == 0 -> Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_zero")
- cl < 0 || deg < 0 -> Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_negative")
+ cl == nil ->
+ m.replyfun.(cl_extra)
+
+ deg == nil ->
+ m.replyfun.(comment)
+
+ cl >= 500 || deg >= 100 ->
+ Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_toohuge")
+
+ cl == 0 || deg == 0 ->
+ Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_zero")
+
+ cl < 0 || deg < 0 ->
+ Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_negative")
+
true ->
points = Alcool.units(cl, deg)
- now = m.at || DateTime.utc_now()
- |> DateTime.to_unix(:millisecond)
+
+ 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)
@@ -344,87 +493,132 @@ defmodule Nola.Plugins.Alcoolog do
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()
+
+ :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.Plugins.Txt.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)
+ 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
+
+ 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
+ sober =
+ nonow
+ |> DateTime.add(round(stats.sober_in * 60), :second)
+ |> Timex.Timezone.convert("Europe/Paris")
- since_str = if stats.since && stats.since_min > 180 do
- "(depuis: #{stats.since_s}) "
- else
- ""
- end
+ at =
+ if nonow.day == sober.day do
+ {:ok, detail} =
+ Timex.Format.DateTime.Formatters.Default.lformat(
+ sober,
+ "aujourd'hui {h24}:{m}",
+ "fr"
+ )
- 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
+ detail
+ else
+ {:ok, detail} =
+ Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
- meta = if beer_id do
- Map.put(meta, "untappd:beer_id", beer_id)
- else
- meta
+ 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() ->
+ 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}")
+ 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
+
+ {: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}°)"
+ local_extra =
+ if auto_set do
+ if comment do
+ " #{comment} (#{cl}cl @ #{deg}°)"
+ else
+ "#{cl}cl @ #{deg}°"
+ end
else
- "#{cl}cl @ #{deg}°"
+ ""
end
- else
- ""
- end
+
m.replyfun.(msg.(m.sender.nick, local_extra))
- notify = Nola.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
+ notify = Nola.Membership.notify_channels(m.account) -- [{m.network, m.channel}]
+
for {net, chan} <- notify do
user = Nola.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
@@ -432,24 +626,26 @@ defmodule Nola.Plugins.Alcoolog do
Nola.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
+ 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.Plugins.Txt.random("alcoolog.#{to_string(miss)}")
+
if miss do
for {net, chan} <- Nola.Membership.notify_channels(m.account) do
user = Nola.UserTrack.find_by_account(net, m.account)
@@ -461,45 +657,59 @@ defmodule Nola.Plugins.Alcoolog do
end
end
- def handle_info({:irc, :trigger, "santai", m = %Nola.Message{trigger: %Nola.Trigger{args: _, type: :bang}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "santai",
+ m = %Nola.Message{trigger: %Nola.Trigger{args: _, type: :bang}}},
+ state
+ ) do
m.replyfun.("!santai <cl> <degrés> [commentaire]")
{:noreply, state}
end
def get_all_stats() do
Nola.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)
+ |> 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
Nola.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)
+ |> 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
Nola.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)
+ |> 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() :: %{Nola.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)
+ :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
@@ -508,129 +718,204 @@ defmodule Nola.Plugins.Alcoolog do
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}} ->
+ {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
+ trend =
+ if rising do
+ "▲"
+ else
+ "▼"
+ end
- rising_file_key = if rising, do: "_rising", else: ""
- txt_file = "alcoolog." <> "user_" <> to_string(user_state) <> rising_file_key
- user_status = Nola.Plugins.Txt.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
+ 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.Plugins.Txt.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
+ 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)
+ 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,
+ %{
+ 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,
+ 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,
+ 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
+
+ _ ->
+ nil
end
end
- def handle_info({:irc, :trigger, "sobre", m = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :dot}}}, state) do
- nicks = Nola.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.()
+ def handle_info(
+ {:irc, :trigger, "sobre",
+ m = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :dot}}},
+ state
+ ) do
+ nicks =
+ Nola.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 = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :bang}}}, state) do
- account = case args do
- [nick] -> Nola.Account.find_always_by_nick(m.network, m.channel, nick)
- [] -> m.account
- end
+ def handle_info(
+ {:irc, :trigger, "sobre",
+ m = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :bang}}},
+ state
+ ) do
+ account =
+ case args do
+ [nick] -> Nola.Account.find_always_by_nick(m.network, m.channel, nick)
+ [] -> m.account
+ end
if account do
user = Nola.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")
+
+ 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")
+ {: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 !")
@@ -638,61 +923,85 @@ defmodule Nola.Plugins.Alcoolog do
else
m.replyfun.("inconnu")
end
+
{:noreply, state}
end
- def handle_info({:irc, :trigger, "alcoolisme", m = %Nola.Message{trigger: %Nola.Trigger{args: [], type: :dot}}}, state) do
- nicks = Nola.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} | "
+ def handle_info(
+ {:irc, :trigger, "alcoolisme",
+ m = %Nola.Message{trigger: %Nola.Trigger{args: [], type: :dot}}},
+ state
+ ) do
+ nicks =
+ Nola.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
- "#{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 = %Nola.Message{trigger: %Nola.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
+ def handle_info(
+ {:irc, :trigger, "alcoolisme",
+ m = %Nola.Message{trigger: %Nola.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)
+ aday = time * (24 * 60 * 60)
now = DateTime.utc_now()
- before = now
- |> DateTime.add(-aday, :second)
- |> DateTime.to_unix(:millisecond)
+
+ 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
@@ -701,30 +1010,37 @@ defmodule Nola.Plugins.Alcoolog do
end
def user_over_time(state, account, count) do
- delay = count*((24 * 60)*60)
+ 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"}, :_, :_, :_, :_, :_, :_, :_},
+
+ 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()
+ :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)
@@ -733,164 +1049,219 @@ defmodule Nola.Plugins.Alcoolog do
def user_over_time_gl(account, count) do
state = data_state()
meta = get_user_meta(state, account.id)
- delay = count*((24 * 60)*60)
+ 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"}, :_, :_, :_, :_, :_, :_, :_},
+
+ 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)
+ |> 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()
- 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)
+ 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}}], [:"$_"]}
+ # 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}
+
+ # tuple ets: {{nick, date}, volumes, current, nom, commentaire}
members = Nola.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 = Nola.Account.get(nick)
- user = Nola.UserTrack.find_by_account(m.network, account)
- nick = if(user, do: user.nick, else: account.name)
- "#{nick}: #{Float.round(count, 4)}"
- end)
- |> Enum.intersperse(", ")
+ 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 = Nola.Account.get(nick)
+ user = Nola.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 = %Nola.Message{trigger: %Nola.Trigger{args: [], type: :plus}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "alcoolisme",
+ m = %Nola.Message{trigger: %Nola.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}")
+
+ m.replyfun.(
+ "+alcoolisme sexe: #{hf} poids: #{meta.weight} facteur de perte: #{meta.loss_factor}"
+ )
+
{:noreply, state}
end
- def handle_info({:irc, :trigger, "alcoolisme", m = %Nola.Message{trigger: %Nola.Trigger{args: [h, weight | rest], type: :plus}}}, state) do
- h = case h do
- "h" -> true
- "f" -> false
- _ -> nil
- end
+ def handle_info(
+ {:irc, :trigger, "alcoolisme",
+ m = %Nola.Message{trigger: %Nola.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 =
+ case Util.float_paparse(weight) do
{weight, _} -> weight
_ -> nil
end
- {factor} = case rest do
+ {factor} =
+ case rest do
[factor] ->
case Util.float_paparse(factor) do
{float, _} -> {float}
_ -> {@default_user_meta.loss_factor}
end
- _ -> {@default_user_meta.loss_factor}
+
+ _ ->
+ {@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.Plugins.Txt.reply_random(m, "alcoolog.fatter")
- old_meta.weight == meta.weight ->
- m.replyfun.("aucun changement!")
- true ->
- Nola.Plugins.Txt.reply_random(m, "alcoolog.thinner")
- 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.Plugins.Txt.reply_random(m, "alcoolog.fatter")
+
+ old_meta.weight == meta.weight ->
+ m.replyfun.("aucun changement!")
+
+ true ->
+ Nola.Plugins.Txt.reply_random(m, "alcoolog.thinner")
end
+ end
{:noreply, state}
end
- def handle_info({:irc, :trigger, "santai", m = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :minus}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "santai",
+ m = %Nola.Message{trigger: %Nola.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.Plugins.Txt.reply_random(m, "alcoolog.delete")
- notify = Nola.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
+ notify = Nola.Membership.notify_channels(m.account) -- [{m.network, m.channel}]
+
for {net, chan} <- notify do
user = Nola.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
- Nola.Irc.Connection.broadcast_message(net, chan, "#{nick} -santai #{points} #{type} #{descr}")
+
+ Nola.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 = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :bang}}},
+ state
+ ) do
+ {account, duration} =
+ case args do
+ [nick | rest] -> {Nola.Account.find_always_by_nick(m.network, m.channel, nick), rest}
+ [] -> {m.account, []}
+ end
- def handle_info({:irc, :trigger, "alcoolisme", m = %Nola.Message{trigger: %Nola.Trigger{args: args, type: :bang}}}, state) do
- {account, duration} = case args do
- [nick | rest] -> {Nola.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
+ duration =
+ case duration do
+ ["semaine"] ->
+ 7
+
+ [j] ->
+ case Integer.parse(j) do
+ {j, "j"} -> j
+ _ -> nil
+ end
+
+ _ ->
+ nil
+ end
+
user = Nola.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("")
+ 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 :/")
@@ -900,47 +1271,67 @@ defmodule Nola.Plugins.Alcoolog do
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: "")
+ 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
+ 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}")
+
+ 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
@@ -951,7 +1342,13 @@ defmodule Nola.Plugins.Alcoolog do
end
end
- defp rename_object_owner(table, ets, object = {old_id, date, volume, current, cl, deg, name, comment, meta}, old_id, new_id) do
+ 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})
@@ -960,58 +1357,75 @@ defmodule Nola.Plugins.Alcoolog do
# 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)
+ # 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)}")
+ 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) ->
+
+ 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("#{__MODULE__}: unhandled info #{inspect t}")
+ Logger.debug("#{__MODULE__}: unhandled info #{inspect(t)}")
{:noreply, state}
end
def nick_history(account) do
spec = [
- {{{:"$1", :_}, :_, :_, :_, :_, :_, :_, :_},
- [{:==, :"$1", {:const, account.id}}],
- [:"$_"]}
+ {{{:"$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)
+ 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
@@ -1022,26 +1436,34 @@ defmodule Nola.Plugins.Alcoolog do
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))}"
+ "+#{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))
+ 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")
+ 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
@@ -1056,10 +1478,12 @@ defmodule Nola.Plugins.Alcoolog 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
@@ -1077,12 +1501,15 @@ defmodule Nola.Plugins.Alcoolog do
defp user_stats(state = %{ets: ets}, account_id) do
meta = get_user_meta(state, account_id)
- aday = (10 * 60)*60
+ 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)
+
+ 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"}, :_, :_, :_, :_, :_, :_, :_},
[
@@ -1090,43 +1517,57 @@ defmodule Nola.Plugins.Alcoolog do
{:"=:=", {:const, account_id}, :"$1"}
], [:"$_"]}
]
- # tuple ets: {{nick, date}, volumes, current, nom, commentaire}
+
+ # 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)
+ 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)
+ 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_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)}
+
+ 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
+ aday = 24 * 7 * 60 * 60
+
+ now =
+ if now do
+ now
+ else
+ DateTime.utc_now()
+ end
+
+ before =
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)
+ |> DateTime.add(-aday, :second)
+ |> DateTime.to_unix(:millisecond)
+
+ # match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
match = [
{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
[
@@ -1134,59 +1575,77 @@ defmodule Nola.Plugins.Alcoolog do
{:"=:=", {: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)
+
+ # 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
+ {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
- all
+ 0
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}
+
+ # 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 + peak, date, [{date, peak} | acc], active_drinks}
+ all
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"
+
+ # IO.puts "\n LEVEL #{inspect level}\n\n\n\n"
cond do
level < 0 -> {0.0, 0}
true -> {level, active_drinks}
@@ -1194,23 +1653,31 @@ defmodule Nola.Plugins.Alcoolog do
end
defp format_duration_from_now(date, with_detail \\ true) do
- date = if is_integer(date) do
- date = DateTime.from_unix!(date, :millisecond)
+ 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")
- 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)
+ 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 =
+ if mins_since > 0 do
+ "il y a "
+ else
+ "dans "
+ end
+
word <> ago <> if(with_detail, do: " #{detail}", else: "")
else
"maintenant #{detail}"
@@ -1218,12 +1685,12 @@ defmodule Nola.Plugins.Alcoolog do
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
+ 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/plugins/alcoolog_announcer.ex b/lib/plugins/alcoolog_announcer.ex
index f172d85..452f56c 100644
--- a/lib/plugins/alcoolog_announcer.ex
+++ b/lib/plugins/alcoolog_announcer.ex
@@ -27,19 +27,21 @@ defmodule Nola.Plugins.AlcoologAnnouncer do
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}])
+ 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)}}}
- ], [:"$_"]}
+ {{:"$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
@@ -49,25 +51,30 @@ defmodule Nola.Plugins.AlcoologAnnouncer do
{:ok, _} = Registry.register(Nola.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, {stats, now(), dets, ets}}#, {:continue, :traverse}}
+ dets_filename = (Nola.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist()
+ {:ok, dets} = :dets.open_file(dets_filename, [{:type, :bag}])
+
+ # :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
+ ets = nil
+ # , {:continue, :traverse}}
+ {:ok, {stats, now(), dets, ets}}
end
def handle_continue(:traverse, state = {_, _, dets, ets}) do
- traverse_fun = fn(obj, dets) ->
+ 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}")
+ IO.puts("ok #{inspect(obj)}")
dets
+
{nick, ts, value} ->
- :ets.insert(ets, { {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")
@@ -75,166 +82,210 @@ defmodule Nola.Plugins.AlcoologAnnouncer do
end
def alcohol_reached(old, new, level) do
- (old.active < level && new.active >= level) && (new.active5m >= level)
- end
+ 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
-
+ 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()
+ apero =
+ Enum.shuffle(@apero)
+ |> Enum.random()
case apero do
{:timed, list} ->
- spawn(fn() ->
+ spawn(fn ->
for line <- list do
Nola.Irc.Connection.broadcast_message("evolu.net", "#dmz", line)
:timer.sleep(:timer.seconds(5))
end
end)
+
string ->
Nola.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
+ # 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
- 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
+ {acct, event}
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
+ 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}")
+ # IO.puts("#{acct}: #{message}")
account = Nola.Account.get(acct)
+
for {net, chan} <- Nola.Membership.notify_channels(account) do
user = Nola.UserTrack.find_by_account(net, account)
nick = if(user, do: user.nick, else: account.name)
@@ -245,8 +296,8 @@ defmodule Nola.Plugins.AlcoologAnnouncer do
timer()
- #IO.puts "tick stats ok"
- {:noreply, {stats,now,dets,ets}}
+ # IO.puts "tick stats ok"
+ {:noreply, {stats, now, dets, ets}}
end
def handle_info(_, state) do
@@ -265,5 +316,4 @@ defmodule Nola.Plugins.AlcoologAnnouncer do
defp timer() do
Process.send_after(self(), :stats, :timer.seconds(@seconds))
end
-
end
diff --git a/lib/plugins/base.ex b/lib/plugins/base.ex
index 97aaa05..7fb285b 100644
--- a/lib/plugins/base.ex
+++ b/lib/plugins/base.ex
@@ -1,5 +1,4 @@
defmodule Nola.Plugins.Base do
-
def irc_doc, do: nil
def start_link() do
@@ -17,85 +16,96 @@ defmodule Nola.Plugins.Base do
end
def handle_info({:irc, :trigger, "plugins", msg = %{trigger: %{type: :bang, args: []}}}, _) do
- enabled_string = Nola.Plugins.enabled()
- |> Enum.map(fn(string_or_module) ->
- case string_or_module do
- string when is_binary(string) -> string
- module when is_atom(module) ->
- module
- |> Macro.underscore()
- |> String.split("/", parts: :infinity)
- |> List.last()
+ enabled_string =
+ Nola.Plugins.enabled()
+ |> Enum.map(fn string_or_module ->
+ case string_or_module do
+ string when is_binary(string) ->
+ string
+
+ module when is_atom(module) ->
+ module
+ |> Macro.underscore()
+ |> String.split("/", parts: :infinity)
+ |> List.last()
end
- end)
- |> Enum.sort()
- |> Enum.join(", ")
+ end)
+ |> Enum.sort()
+ |> 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.Plugins, Macro.camelize(plugin)])
+
with true <- Code.ensure_loaded?(module),
- pid when is_pid(pid) <- GenServer.whereis(module)
- do
+ pid when is_pid(pid) <- GenServer.whereis(module) do
m.replyfun.("loaded, active: #{inspect(pid)}")
else
- false -> m.replyfun.("not loaded")
+ false ->
+ m.replyfun.("not loaded")
+
nil ->
- msg = case Nola.Plugins.get(module) do
- :disabled -> "disabled"
- {_, false, _} -> "disabled"
- _ -> "not active"
- end
+ msg =
+ case Nola.Plugins.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.Plugins, Macro.camelize(plugin)])
+
with true <- Code.ensure_loaded?(module),
Nola.Plugins.switch(module, true),
- {:ok, pid} <- Nola.Plugins.start(module)
- do
+ {:ok, pid} <- Nola.Plugins.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.Plugins, Macro.camelize(plugin)])
+
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module),
:ok <- GenServer.stop(pid),
- {:ok, pid} <- Nola.Plugins.start(module)
- do
+ {:ok, pid} <- Nola.Plugins.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.Plugins, Macro.camelize(plugin)])
+
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module),
- :ok <- GenServer.stop(pid)
- do
+ :ok <- GenServer.stop(pid) do
Nola.Plugins.switch(module, false)
m.replyfun.("stopped: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
nil -> m.replyfun.("not active")
end
+
{:noreply, nil}
end
@@ -106,7 +116,14 @@ defmodule Nola.Plugins.Base do
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))
+ url =
+ NolaWeb.Router.Helpers.irc_url(
+ NolaWeb.Endpoint,
+ :index,
+ m.network,
+ NolaWeb.format_chan(m.channel)
+ )
+
m.replyfun.("-> #{url}")
{:noreply, nil}
end
@@ -115,7 +132,10 @@ defmodule Nola.Plugins.Base 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()
+
+ 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()
brand = Nola.brand(:name)
@@ -123,22 +143,24 @@ defmodule Nola.Plugins.Base do
if message.channel do
message.replyfun.([
- <<"🤖 ", 2, "#{brand}", 2, " v", 2, "#{ver}", 2, "! My owner is #{Nola.brand(:owner)} and help is at #{url}">>,
+ <<"🤖 ", 2, "#{brand}", 2, " v", 2, "#{ver}", 2,
+ "! My owner is #{Nola.brand(:owner)} and help is at #{url}">>
])
else
message.replyfun.([
- <<"🤖 I am a robot running ", 2, "#{brand}", 2, " version ", 2, "#{ver}", 2, " — source: #{Nola.source_url()}">>,
+ <<"🤖 I am a robot running ", 2, "#{brand}", 2, " version ", 2, "#{ver}", 2,
+ " — source: #{Nola.source_url()}">>,
"Source code: #{Nola.source_url()}",
"🦾 Elixir #{elixir_ver} #{otp_ver} on #{system}",
"🙋🏻 Owner: #{owner}",
- "🌍 Web interface: #{url}"
+ "🌍 Web interface: #{url}"
])
end
+
{:noreply, nil}
end
def handle_info(_msg, _) do
{:noreply, nil}
end
-
end
diff --git a/lib/plugins/boursorama.ex b/lib/plugins/boursorama.ex
index 025a250..5b1ea9a 100644
--- a/lib/plugins/boursorama.ex
+++ b/lib/plugins/boursorama.ex
@@ -1,5 +1,4 @@
defmodule Nola.Plugins.Boursorama do
-
def irc_doc() do
"""
# bourses
@@ -25,25 +24,34 @@ defmodule Nola.Plugins.Boursorama do
{:ok, nil}
end
- def handle_info({:irc, :trigger, cac, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang}}}, state) when cac in ["cac40", "caca40"] do
+ def handle_info(
+ {:irc, :trigger, cac, m = %Nola.Message{trigger: %Nola.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)
+ {:ok, html} = Floki.parse_document(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
+
+ 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}")
@@ -54,5 +62,4 @@ defmodule Nola.Plugins.Boursorama do
m.replyfun.("caca40: erreur http")
end
end
-
end
diff --git a/lib/plugins/buffer.ex b/lib/plugins/buffer.ex
index 42a435e..5f848ef 100644
--- a/lib/plugins/buffer.ex
+++ b/lib/plugins/buffer.ex
@@ -6,7 +6,12 @@ defmodule Nola.Plugins.Buffer do
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
+
+ spec =
+ fun do
+ {{n, c, _}, m} when n == ^network and (c == ^channel or is_nil(c)) -> m
+ end
+
:ets.select(@table, spec, limit)
end
@@ -18,6 +23,7 @@ defmodule Nola.Plugins.Buffer do
for e <- ~w(messages triggers events outputs) do
{:ok, _} = Registry.register(Nola.PubSub, e, plugin: __MODULE__)
end
+
{:ok, :ets.new(@table, [:named_table, :ordered_set, :protected])}
end
@@ -34,11 +40,11 @@ defmodule Nola.Plugins.Buffer do
defp ts(nil), do: ts(NaiveDateTime.utc_now())
defp ts(naive = %NaiveDateTime{}) do
- ts = naive
- |> DateTime.from_naive!("Etc/UTC")
- |> DateTime.to_unix()
+ ts =
+ naive
+ |> DateTime.from_naive!("Etc/UTC")
+ |> DateTime.to_unix()
-ts
end
-
end
diff --git a/lib/plugins/calc.ex b/lib/plugins/calc.ex
index 2ff6cb4..20442ce 100644
--- a/lib/plugins/calc.ex
+++ b/lib/plugins/calc.ex
@@ -12,20 +12,27 @@ defmodule Nola.Plugins.Calc do
end
def init(_) do
- {:ok, _} = Registry.register(Nola.PubSub, "trigger:calc", [plugin: __MODULE__])
+ {:ok, _} = Registry.register(Nola.PubSub, "trigger:calc", plugin: __MODULE__)
{:ok, nil}
end
- def handle_info({:irc, :trigger, "calc", message = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: expr_list}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "calc",
+ message = %Nola.Message{trigger: %Nola.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)
+
+ 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
- rescue
- error -> if(error[:message], do: "#{error.message}", else: "erreur")
- end
+
message.replyfun.("#{message.sender.nick}: #{expr} = #{result}")
{:noreply, state}
end
@@ -33,5 +40,4 @@ defmodule Nola.Plugins.Calc do
def handle_info(msg, state) do
{:noreply, state}
end
-
end
diff --git a/lib/plugins/coronavirus.ex b/lib/plugins/coronavirus.ex
index afd8a33..db9f646 100644
--- a/lib/plugins/coronavirus.ex
+++ b/lib/plugins/coronavirus.ex
@@ -1,6 +1,7 @@
defmodule Nola.Plugins.Coronavirus do
require Logger
NimbleCSV.define(CovidCsv, separator: ",", escape: "\"")
+
@moduledoc """
# Corona Virus
@@ -19,7 +20,7 @@ defmodule Nola.Plugins.Coronavirus do
end
def init(_) do
- {:ok, _} = Registry.register(Nola.PubSub, "trigger:coronavirus", [plugin: __MODULE__])
+ {:ok, _} = Registry.register(Nola.PubSub, "trigger:coronavirus", plugin: __MODULE__)
{:ok, nil, {:continue, :init}}
:ignore
end
@@ -38,53 +39,87 @@ defmodule Nola.Plugins.Coronavirus do
{:noreply, %{data: data}}
end
- def handle_info({:irc, :trigger, "coronavirus", m = %Nola.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}")
+ def handle_info(
+ {:irc, :trigger, "coronavirus", m = %Nola.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()
+
+ 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 = %Nola.Message{trigger: %{type: :bang, args: location}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "coronavirus",
+ m = %Nola.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})")
+ 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 = %Nola.Message{trigger: %{type: :query, args: location}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "coronavirus",
+ m = %Nola.Message{trigger: %{type: :query, args: location}}},
+ state
+ ) do
m.replyfun.("https://github.com/CSSEGISandData/COVID-19")
{:noreply, state}
end
@@ -93,80 +128,125 @@ defmodule Nola.Plugins.Coronavirus do
# 2. Fetch yesterday if no results
defp fetch_data(current_data, date \\ nil) do
now = Date.utc_today()
- url = fn(date) ->
+
+ 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)
+ 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}"
+ 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,
+ 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
}
- 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)
+ 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
+ }
- acc = if state && state != "" do
- Map.put(acc, state, entry)
- else
+ 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
+ end)
- other ->
- Logger.info("Coronavirus line failed: #{inspect line}")
- acc
- end
- end)
- Logger.info "Updated coronavirus database"
+ Logger.info("Updated coronavirus database")
{data, :timer.minutes(60)}
+
{:ok, %HTTPoison.Response{status_code: 404}} ->
- Logger.debug "Corona 404 #{cur_url}"
+ 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}"
+ Logger.error("Coronavirus: Update failed #{inspect(other)}")
{current_data, :timer.minutes(5)}
end
end
-
end
diff --git a/lib/plugins/correction.ex b/lib/plugins/correction.ex
index b50733b..0d3f5bd 100644
--- a/lib/plugins/correction.ex
+++ b/lib/plugins/correction.ex
@@ -6,13 +6,14 @@ defmodule Nola.Plugins.Correction do
"""
def irc_doc, do: @moduledoc
+
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
- {:ok, _} = Registry.register(Nola.PubSub, "messages", [plugin: __MODULE__])
- {:ok, _} = Registry.register(Nola.PubSub, "triggers", [plugin: __MODULE__])
+ {:ok, _} = Registry.register(Nola.PubSub, "messages", plugin: __MODULE__)
+ {:ok, _} = Registry.register(Nola.PubSub, "triggers", plugin: __MODULE__)
{:ok, %{}}
end
@@ -27,33 +28,40 @@ defmodule Nola.Plugins.Correction do
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)
+ 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")
+
+ _ ->
+ 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
+ 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/plugins/dice.ex b/lib/plugins/dice.ex
index dcf7d0b..be0d6f4 100644
--- a/lib/plugins/dice.ex
+++ b/lib/plugins/dice.ex
@@ -21,23 +21,24 @@ defmodule Nola.Plugins.Dice do
end
def init([]) do
- {:ok, _} = Registry.register(Nola.PubSub, "trigger:dice", [plugin: __MODULE__])
+ {:ok, _} = Registry.register(Nola.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) ->
+ 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
+ {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)
@@ -49,18 +50,20 @@ defmodule Nola.Plugins.Dice do
end
defp roll(state, message, faces, 1) when faces > 0 do
- random = :crypto.rand_uniform(1, faces+1)
+ 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, 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/plugins/finance.ex b/lib/plugins/finance.ex
index 3ecc2fb..68afc48 100644
--- a/lib/plugins/finance.ex
+++ b/lib/plugins/finance.ex
@@ -27,25 +27,31 @@ defmodule Nola.Plugins.Finance do
@crypto_list "http://www.alphavantage.co/digital_currency_list/"
HTTPoison.start()
- load_currency = fn(url) ->
+
+ 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)
+ |> Enum.map(fn line ->
+ [symbol, name] =
+ line
+ |> String.strip()
+ |> String.split(",", parts: 2)
+
{symbol, name}
end)
- |> Enum.into(Map.new)
+ |> 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
@@ -58,92 +64,135 @@ defmodule Nola.Plugins.Finance do
{:ok, nil}
end
+ def handle_info(
+ {:irc, :trigger, "stocks", message = %{trigger: %{type: :query, args: args = search}}},
+ state
+ ) do
+ search(search, message)
+ {:noreply, state}
+ end
- def handle_info({:irc, :trigger, "stocks", message = %{trigger: %{type: :query, args: args = search}}}, state) do
+ defp search(search, message) do
search = Enum.join(search, "%20")
- url = "https://www.alphavantage.co/query?function=SYMBOL_SEARCH&keywords=#{search}&apikey=#{api_key()}"
+
+ 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)
+ IO.inspect(data)
+
if error = Map.get(data, "Error Message") do
- Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
+ 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
+ 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}"
+ 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})")
+ 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()}"
+ 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)
+ IO.inspect(data)
+
+ case data do
+ %{"Error Message" => error} ->
+ Logger.error("AlphaVantage API invalid request #{url} - #{inspect(error)}")
+ message.replyfun.("stocks: error: #{error}")
+
+ %{"Global Quote" => data = %{"01. symbol" => _}} ->
+ 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)
+
+ _ ->
+ message.replyfun.("stocks: unknown symbol: #{symbol}")
+ search([symbol], message)
end
+
{:ok, resp = %HTTPoison.Response{status_code: code}} ->
- Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
+ 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})")
+ 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()}"
- 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}")
+ Logger.error("AlphaVantage API invalid request #{url} - #{inspect(error)}")
message.replyfun.("forex: requête invalide")
else
data = Map.get(data, "Realtime Currency Exchange Rate")
@@ -151,34 +200,47 @@ defmodule Nola.Plugins.Finance do
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})")
+ 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}"
+ 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})")
+ 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
+ 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(", ")
+
+ 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
@@ -186,5 +248,4 @@ defmodule Nola.Plugins.Finance do
Application.get_env(:nola, :alphavantage, [])
|> Keyword.get(:api_key, "demo")
end
-
end
diff --git a/lib/plugins/gpt.ex b/lib/plugins/gpt.ex
deleted file mode 100644
index 8ee8c10..0000000
--- a/lib/plugins/gpt.ex
+++ /dev/null
@@ -1,355 +0,0 @@
-defmodule Nola.Plugins.Gpt do
- require Logger
- import Nola.Plugins.TempRefHelper
-
- 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(Nola.PubSub, "trigger:#{@trigger}", regopts)
- {:ok, %__MODULE__{temprefs: new_temp_refs()}}
- end
-
- def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.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 = %Nola.Message{trigger: %Nola.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 = %Nola.Message{trigger: %Nola.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 = %Nola.Message{trigger: %Nola.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 = %Nola.Message{trigger: %Nola.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 = %Nola.Message{trigger: %Nola.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 = case original_prompt do
- %{"continue_prompt" => prompt_string} when is_binary(prompt_string) ->
- 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})
- %{"messages" => _} ->
- continue_prompt
- |> Map.put("prompt", "{{content}}")
- |> Map.put("prompt_format", "liquid")
- |> Map.put("messages", Map.get(run, "messages"))
- _ ->
- 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
-
- # Chat prompt
- # "prompt" is the template for the initial user message
- # "messages" is original messages to be put before the initial user one
- defp prompt(msg, prompt = %{"type" => "chat", "prompt" => prompt_template, "messages" => messages}, content, state) do
- Logger.debug("gpt_plugin:prompt/4 (chat) #{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
-
- messages = Enum.map(messages, fn(%{"role" => role, "content" => text}) ->
- text = case Map.get(prompt, "prompt_format", "liquid") do
- "liquid" -> Tmpl.render(text, msg, Map.get(prompt, "prompt_liquid_variables", %{}))
- "norender" -> text
- end
- %{"role" => role, "content" => text}
- end) ++ [%{"role" => "user", "content" => prompt_text}]
-
- args = Map.get(prompt, "openai_params")
- |> Map.put_new("model", "gpt-3.5-turbo")
- |> Map.put("messages", messages)
- |> 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/chat/completions", args) do
- {:ok, %{"choices" => [%{"message" => %{"content" => 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,
- "messages" => messages ++ [%{"role" => "assistant", "content" => text}],
- "message_at" => msg.at,
- "reply_at" => DateTime.utc_now(),
- "gpt_id" => gpt_id,
- "gpt_at" => created,
- "gpt_usage" => usage,
- "type" => "chat",
- "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 prompt(msg, prompt = %{"type" => "completions", "prompt" => prompt_template}, content, state) do
- Logger.debug("gpt: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/plugins/helpers/temp_ref.ex b/lib/plugins/helpers/temp_ref.ex
index 160169d..f4407d8 100644
--- a/lib/plugins/helpers/temp_ref.ex
+++ b/lib/plugins/helpers/temp_ref.ex
@@ -30,7 +30,7 @@ defmodule Nola.Plugins.TempRefHelper do
length = Keyword.get(options, :length, 3)
for _ <- 1..length, into: "", do: <<Enum.random('bcdfghjkmpqtrvwxy2346789')>>
end
-
+
def increase(options) do
Keyword.put(options, :length, Keyword.get(options, :length, 3) + 1)
end
@@ -42,8 +42,13 @@ defmodule Nola.Plugins.TempRefHelper do
max: Keyword.get(options, :max, []),
expire: Keyword.get(options, :expire, :infinity),
build_fun: Keyword.get(options, :build_fun, &__MODULE__.SimpleAlphaNumericBuilder.build/1),
- build_increase_fun: Keyword.get(options, :build_increase_fun, &__MODULE__.SimpleAlphaNumericBuilder.increase/1),
- build_options: Keyword.get(options, :build_options, [length: 3])
+ build_increase_fun:
+ Keyword.get(
+ options,
+ :build_increase_fun,
+ &__MODULE__.SimpleAlphaNumericBuilder.increase/1
+ ),
+ build_options: Keyword.get(options, :build_options, length: 3)
}
end
@@ -58,6 +63,7 @@ defmodule Nola.Plugins.TempRefHelper do
def put_temp_ref(data, state = %__MODULE__{}) do
state = janitor_refs(state)
key = new_nonexisting_key(state)
+
if key do
ref = {key, DateTime.utc_now(), data}
{key, %__MODULE__{state | refs: [ref | state.refs]}}
@@ -78,18 +84,19 @@ defmodule Nola.Plugins.TempRefHelper do
end
defp new_nonexisting_key(state = %__MODULE__{refs: refs}, i \\ 1) do
- build_options = if rem(i, 5) == 0 do
- state.build_increase_fun.(state.build_options)
- else
- state.build_options
- end
-
+ build_options =
+ if rem(i, 5) == 0 do
+ state.build_increase_fun.(state.build_options)
+ else
+ state.build_options
+ end
+
key = state.build_fun.(state.build_options)
+
if !List.keymember?(refs, key, 0) do
key
else
new_nonexisting_key(state, i + 1)
end
end
-
end
diff --git a/lib/plugins/image.ex b/lib/plugins/image.ex
deleted file mode 100644
index 446cb49..0000000
--- a/lib/plugins/image.ex
+++ /dev/null
@@ -1,246 +0,0 @@
-defmodule Nola.Plugins.Image do
- require Logger
- import Nola.Plugins.TempRefHelper
-
- def irc_doc() do
- """
- # Image Generation
-
- * **`!d2 [-n 1..10] [-g 256, 512, 1024] <prompt>`** generate image(s) using OpenAI Dall-E 2
- * **`!sd [options] <prompt>`** generate image(s) using Stable Diffusion models (see below)
-
- ## !sd
-
- * `-m X` (sd2) Model (sd2: Stable Diffusion v2, sd1: Stable Diffusion v1.5, any3: Anything v3, any4: Anything v4, oj: OpenJourney)
- * `-w X, -h X` (512) width and height. (128, 256, 384, 448, 512, 576, 640, 704, 768)
- * `-n 1..10` (1) number of images to generate
- * `-s X` (null) Seed
- * `-S 0..500` (50) denoising steps
- * `-X X` (KLMS) scheduler (DDIM, K_EULER, DPMSolverMultistep, K_EULER_ANCESTRAL, PNDM, KLMS)
- * `-g 1..20` (7.5) guidance scale
- * `-P 0.0..1.0` (0.8) prompt strength
- """
- end
-
- def start_link() do
- GenServer.start_link(__MODULE__, [], name: __MODULE__)
- end
-
- defstruct [:temprefs]
-
- def init(_) do
- regopts = [plugin: __MODULE__]
- {:ok, _} = Registry.register(Nola.PubSub, "trigger:d2", regopts)
- {:ok, _} = Registry.register(Nola.PubSub, "trigger:sd", regopts)
- {:ok, %__MODULE__{temprefs: new_temp_refs()}}
- end
-
- def handle_info({:irc, :trigger, "sd", msg = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}}, state) do
- {:noreply, case OptionParser.parse(args, aliases: [m: :model], strict: [model: :string]) do
- {_, [], _} ->
- msg.replyfun.("#{msg.sender.nick}: sd: missing prompt")
- state
- {opts, prompt, _} ->
- process_sd(Keyword.get(opts, :model, "sd2"), Enum.join(prompt, " "), msg, state)
- end}
- end
-
- def handle_info({:irc, :trigger, "d2", msg = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}}, state) do
- opts = OptionParser.parse(args,
- aliases: [n: :n, g: :geometry],
- strict: [n: :integer, geometry: :integer]
- )
- case opts do
- {_opts, [], _} ->
- msg.replyfun.("#{msg.sender.nick}: d2: missing prompt")
- {:noreply, state}
- {opts, prompts, _} ->
- prompt = Enum.join(prompts, " ")
- geom = Keyword.get(opts, :geometry, 256)
- request = %{
- "prompt" => prompt,
- "n" => Keyword.get(opts, :n, 1),
- "size" => "#{geom}x#{geom}",
- "response_format" => "b64_json",
- "user" => msg.account.id,
- }
-
- id = FlakeId.get()
-
- state = case OpenAi.post("/v1/images/generations", request) do
- {:ok, %{"data" => data}} ->
- urls = for {%{"b64_json" => b64}, idx} <- Enum.with_index(data) do
- with {:ok, body} <- Base.decode64(b64),
- <<smol_body::binary-size(20), _::binary>> = body,
- {:ok, magic} <- GenMagic.Pool.perform(Nola.GenMagic, {:bytes, smol_body}),
- bucket = Application.get_env(:nola, :s3, []) |> Keyword.get(:bucket),
- s3path = "#{msg.account.id}/iD2#{id}#{idx}.png",
- s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type),
- {:ok, _} <- ExAws.request(s3req),
- path = NolaWeb.Router.Helpers.url(NolaWeb.Endpoint) <> "/files/#{s3path}"
- do
- {:ok, path}
- end
- end
-
- urls = for {:ok, path} <- urls, do: path
- msg.replyfun.("#{msg.sender.nick}: #{Enum.join(urls, " ")}")
- state
- {:error, atom} when is_atom(atom) ->
- Logger.error("dalle2: #{inspect atom}")
- msg.replyfun.("#{msg.sender.nick}: dalle2: ☠️ #{to_string(atom)}")
- state
- error ->
- Logger.error("dalle2: #{inspect error}")
- msg.replyfun.("#{msg.sender.nick}: dalle2: ☠️ ")
- state
- end
- {:noreply, state}
- end
- end
-
- defp process_sd(model, prompt, msg, state) do
- {general_opts, _, _} = OptionParser.parse(msg.trigger.args,
- aliases: [n: :number, w: :width, h: :height],
- strict: [number: :integer, width: :integer, height: :integer]
- )
-
- general_opts = general_opts
- |> Keyword.put_new(:number, 1)
-
- case sd_model(model, prompt, general_opts, msg.trigger.args) do
- {:ok, env} ->
- base_url = "https://api.runpod.ai/v1/#{env.name}"
- {headers, options} = runpod_headers(env, state)
- result = with {:ok, json} <- Poison.encode(%{"input" => env.request}),
- {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.post("#{base_url}/run", json, headers, options),
- {:ok, %{"id" => id} = data} <- Poison.decode(body) do
- Logger.debug("runpod: started job #{id}: #{inspect data}")
- spawn(fn() -> runpod_result_loop("#{base_url}/status/#{id}", env, msg, state) end)
- :ok
- else
- {:ok, %HTTPoison.Response{status_code: code}} -> {:error, Plug.Conn.Status.reason_atom(code)}
- {:error, %HTTPoison.Error{reason: reason}} -> {:error, reason}
- end
-
- case result do
- {:error, reason} ->
- Logger.error("runpod: http error for #{base_url}/run: #{inspect reason}")
- msg.replyfun.("#{msg.sender.nick}: sd: runpod failed: #{inspect reason}")
- _ -> :ok
- end
- {:error, error} ->
- msg.replyfun.("#{msg.sender.nick}: sd: #{error}")
- end
-
- state
- end
-
- defp runpod_result_loop(url, env, msg, state) do
- Logger.debug("runpod_result_loop: new")
- {headers, options} = runpod_headers(env, state)
- with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(url, headers ++ [{"content-type", "application/json"}], options),
- {:ok, %{"status" => "COMPLETED"} = data} <- Poison.decode(body) do
- id = FlakeId.get()
- tasks = for {%{"image" => url, "seed" => seed}, idx} <- Enum.with_index(Map.get(data, "output", [])) do
- Task.async(fn() ->
-with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(url, [], options),
-bucket = Application.get_env(:nola, :s3, []) |> Keyword.get(:bucket),
-s3path = "#{msg.account.id}/iR#{env.nick}#{id}#{idx}-#{seed}.png",
-s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: "image/png"),
-{:ok, _} <- ExAws.request(s3req),
-path = NolaWeb.Router.Helpers.url(NolaWeb.Endpoint) <> "/files/#{s3path}"
-do
- {:ok, path}
-else
- error ->
- Logger.error("runpod_result: error while uploading #{url}: #{inspect error}")
- {:error, error}
-end
- end)
- end
- |> Task.yield_many(5000)
- |> Enum.map(fn {task, res} ->
- res || Task.shutdown(task, :brutal_kill)
- end)
-
- results = for({:ok, {:ok, url}} <- tasks, do: url)
-
- msg.replyfun.("#{msg.sender.nick}: #{Enum.join(results, " ")}")
- else
- {:ok, %{"status" => "FAILED"} = data} ->
- Logger.error("runpod_result_loop: job FAILED: #{inspect data}")
- msg.replyfun.("#{msg.sender.nick}: sd: job failed: #{Map.get(data, "error", "error")}")
- {:ok, %{"status" => _} = data} ->
- Logger.debug("runpod_result_loop: not completed: #{inspect data}")
- :timer.sleep(:timer.seconds(1))
- runpod_result_loop(url, env, msg, state)
- {:ok, %HTTPoison.Response{status_code: 403}} ->
- msg.replyfun.("#{msg.sender.nick}: sd: runpod failure: unauthorized")
- error ->
- Logger.warning("image: sd: runpod http error: #{inspect error}")
- :timer.sleep(:timer.seconds(2))
- runpod_result_loop(url, env, msg, state)
- end
- end
-
- defp runpod_headers(_env, _state) do
- config = Application.get_env(:nola, :runpod, [])
- headers = [{"user-agent", "nola.lol bot, href@random.sh"},
- {"authorization", "Bearer " <> Keyword.get(config, :key, "unset-api-key")}]
- options = [timeout: :timer.seconds(180), recv_timeout: :timer.seconds(180)]
- {headers, options}
- end
-
- defp sd_model(name, _, general_opts, opts) when name in ~w(sd2 sd1 oj any any4) do
- {opts, prompt, _} = OptionParser.parse(opts, [
- aliases: [P: :strength, s: :seed, S: :steps, g: :guidance, X: :scheduler, q: :negative],
- strict: [strength: :float, steps: :integer, guidance: :float, scheduler: :string, seed: :integer, negative: :keep]
- ])
- opts = general_opts ++ opts
- prompt = Enum.join(prompt, " ")
-
- negative = case Keyword.get_values(opts, :negative) do
- [] -> nil
- list -> Enum.join(list, " ")
- end
-
- full_name = case name do
- "sd2" -> "stable-diffusion-v2"
- "sd1" -> "stable-diffusion-v1"
- "oj" -> "sd-openjourney"
- "any" -> "sd-anything-v3"
- "any4" -> "sd-anything-v4"
- end
-
- default_scheduler = case name do
- "sd2" -> "KLMS"
- _ -> "K-LMS"
- end
-
- request = %{
- "prompt" => prompt,
- "num_outputs" => general_opts[:number],
- "width" => opts[:width] || 512,
- "height" => opts[:height] || 512,
- "prompt_strength" => opts[:strength] || 0.8,
- "num_inference_steps" => opts[:steps] || 30,
- "guidance_scale" => opts[:guidance] || 7.5,
- "scheduler" => opts[:scheduler] || default_scheduler,
- "seed" => opts[:seed] || :rand.uniform(100_000_00)
- }
-
- request = if negative do
- Map.put(request, "negative_prompt", negative)
- else
- request
- end
-
- {:ok, %{name: full_name, nick: name, request: request}}
- end
-
- defp sd_model(name, _, _, _) do
- {:error, "unsupported model: \"#{name}\""}
- end
-
-end
diff --git a/lib/plugins/kick_roulette.ex b/lib/plugins/kick_roulette.ex
index b25b46d..2194775 100644
--- a/lib/plugins/kick_roulette.ex
+++ b/lib/plugins/kick_roulette.ex
@@ -6,27 +6,28 @@ defmodule Nola.Plugins.KickRoulette do
"""
def irc_doc, do: @moduledoc
+
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
- {:ok, _} = Registry.register(Nola.PubSub, "trigger:kick", [plugin: __MODULE__])
+ {:ok, _} = Registry.register(Nola.PubSub, "trigger:kick", plugin: __MODULE__)
{:ok, nil}
end
- def handle_info({:irc, :trigger, "kick", message = %{trigger: %{type: :bang, args: []}}}, _) do
+ def handle_info({:irc, :trigger, "kick", message = %{trigger: %{type: :bang, args: _}}}, _) do
if 5 == :crypto.rand_uniform(1, 6) do
- spawn(fn() ->
+ 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/plugins/last_fm.ex b/lib/plugins/last_fm.ex
index b7d0a92..0d513c4 100644
--- a/lib/plugins/last_fm.ex
+++ b/lib/plugins/last_fm.ex
@@ -23,34 +23,55 @@ defmodule Nola.Plugins.LastFm do
def init([]) do
regopts = [plugin: __MODULE__]
for t <- @pubsub_topics, do: {:ok, _} = Registry.register(Nola.PubSub, t, regopts)
- dets_filename = (Nola.data_path() <> "/" <> "lastfm.dets") |> String.to_charlist
+ 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
+ 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}\".")
+
+ 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
+ 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
+ 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
@@ -59,18 +80,24 @@ defmodule Nola.Plugins.LastFm do
{:noreply, state}
end
- def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: [nick_or_user]}}}, state) do
+ 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 = Nola.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)
+ 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
@@ -84,40 +111,50 @@ defmodule Nola.Plugins.LastFm 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 = Nola.Account.get(nick_or_user) || Nola.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
+ id_or_user =
+ if account =
+ Nola.Account.get(nick_or_user) ||
+ Nola.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 = Nola.Account.get(id_or_user) do
- user = Nola.UserTrack.find_by_account(message.network, account)
- if(user, do: user.nick, else: account.name)
- else
- username
- end
+
+ user =
+ if account = Nola.Account.get(id_or_user) do
+ user = Nola.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
@@ -125,32 +162,57 @@ defmodule Nola.Plugins.LastFm do
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
+
+ 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)}"}
+ {: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}"
+ Logger.error("Lastfm http error: #{inspect(error)}")
:error
end
end
- defp fetch_track(user, %{"recenttracks" => %{"track" => [ t = %{"name" => name, "artist" => %{"name" => artist}} | _]}}) do
+
+ 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)
+
+ 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}"
+ Logger.error("Lastfm http error: #{inspect(error)}")
:error
end
end
- defp format_now_playing(%{"recenttracks" => %{"track" => [track = %{"@attr" => %{"nowplaying" => "true"}} | _]}}, et) do
+ defp format_now_playing(
+ %{"recenttracks" => %{"track" => [track = %{"@attr" => %{"nowplaying" => "true"}} | _]}},
+ et
+ ) do
format_track(true, track, et)
end
@@ -173,15 +235,16 @@ defmodule Nola.Plugins.LastFm do
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(", ")
+
+ 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/plugins/link.ex b/lib/plugins/link.ex
index 4c4261f..0dca6ae 100644
--- a/lib/plugins/link.ex
+++ b/lib/plugins/link.ex
@@ -37,57 +37,53 @@ defmodule Nola.Plugins.Link do
def short_irc_doc, do: false
def irc_doc, do: @ircdoc
require Logger
+ alias __MODULE__.Quirks
+ alias __MODULE__.Store
+ alias __MODULE__.Scraper
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
+ @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(Nola.PubSub, "messages", [plugin: __MODULE__])
- #{:ok, _} = Registry.register(Nola.PubSub, "messages:telegram", [plugin: __MODULE__])
+ {:ok, _} = Registry.register(Nola.PubSub, "messages", plugin: __MODULE__)
+ # {:ok, _} = Registry.register(Nola.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) ->
+ |> 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)
+ if Store.inhibit_link?(word, {message.network, message.channel}) do
+ Logger.debug("link inhibited #{word}")
+ else
+ handle_link(word, uri, message)
+ end
end
end
end)
+
{:noreply, state}
end
@@ -99,6 +95,48 @@ defmodule Nola.Plugins.Link do
:ok
end
+ def handle_link(url, uri, message) do
+ spawn(fn ->
+ :timer.kill_after(:timer.seconds(30))
+
+ store = Store.get_link(url)
+
+ case store || expand_link([uri]) do
+ {:ok, uris, text} = save ->
+ text =
+ case uris do
+ [uri] ->
+ text
+
+ [luri | _] ->
+ if luri.host == uri.host && luri.path == uri.path do
+ text
+ else
+ ["-> #{URI.to_string(luri)}", text]
+ end
+ end
+
+ case text do
+ lines when is_list(lines) ->
+ for text <- lines, do: message.replyfun.(text)
+ if !store, do: Store.insert_link(url, save)
+ Store.witness_link(url, {message.network, message.channel})
+
+ text when is_binary(text) ->
+ message.replyfun.(text)
+ if !store, do: Store.insert_link(url, save)
+ Store.witness_link(url, {message.network, message.channel})
+
+ nil ->
+ nil
+ end
+
+ _ ->
+ nil
+ end
+ end)
+ end
+
# 1. Match the first valid handler
# 2. Try to run the handler
# 3. If :error or crash, default link.
@@ -110,17 +148,24 @@ defmodule Nola.Plugins.Link 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)
+ 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 ->
+ module = Module.concat([module])
+
+ case module.match(uri, opts) do
+ {true, params} ->
+ Logger.debug("link: will expand with #{inspect(module)} for #{inspect(uri)}")
+ {:halt, {module, params, opts}}
+
+ false ->
+ {:cont, acc}
+ end
+ end)
+
run_expand(acc, handler)
end
@@ -128,21 +173,27 @@ defmodule Nola.Plugins.Link do
expand_default(acc)
end
- def run_expand(acc=[uri|_], {module, params, opts}) do
- Logger.debug("link: expanding #{inspect uri} with #{inspect module}")
+ def run_expand(acc = [uri | _], {module, params, opts}) do
case module.expand(uri, params, opts) do
- {:ok, data} -> {:ok, acc, data}
- :error -> expand_default(acc)
- :skip -> nil
+ {:ok, data} ->
+ Logger.debug("link: expanded #{inspect(uri)} with #{inspect(module)}")
+ {:ok, acc, data}
+
+ :error ->
+ Logger.error("Error expanding URL #{uri} with #{inspect(module)}")
+ expand_default(acc)
+
+ :skip ->
+ nil
end
rescue
e ->
- Logger.error("link: rescued #{inspect uri} with #{inspect module}: #{inspect 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}}")
+ Logger.error("link: catched #{inspect(uri)} with #{inspect(module)}: #{inspect({e, b})}")
expand_default(acc)
end
@@ -155,40 +206,48 @@ defmodule Nola.Plugins.Link do
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)
+ 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}
+ 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
- rescue
- e ->
- Logger.error(inspect(e))
- {:cont, false}
- catch
- e, b ->
- Logger.error(inspect({b}))
- {:cont, false}
- end
- end)
+ end)
cond do
handler != false and length <= 30_000_000 ->
case get_body(url, 30_000_000, client, handler, <<>>) do
- {:ok, _} = ok -> ok
+ {:ok, _} = ok ->
+ ok
+
:error ->
{:ok, "file: #{content_type}, size: #{human_size(length)}"}
end
- #String.starts_with?(content_type, "text/html") && length <= 30_000_000 ->
+
+ # String.starts_with?(content_type, "text/html") && length <= 30_000_000 ->
# get_body(url, 30_000_000, client, <<>>)
true ->
:hackney.close(client)
@@ -197,62 +256,94 @@ defmodule Nola.Plugins.Link do
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)
+ 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
+ defp get_req(url, {:ok, status, headers, client}) do
+ Logger.error("Error fetching URL #{url} = #{status}")
: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
+ 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 >>)
+ 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
+ body =
+ case mode do
+ :body ->
+ acc
+
+ :file ->
+ {:ok, tmpfile} = Plug.Upload.random_file("linkplugin")
+ File.write!(tmpfile, acc)
+ tmpfile
+ end
+
+ Logger.debug("expanding body with #{inspect(handler)}: #{inspect(body)}")
handler.post_expand(url, body, params, opts)
+
{:error, reason} ->
- {:ok, "failed to fetch body: #{inspect 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]
+
+ uri = Quirks.uri(uri)
+
+ headers = [
+ {"user-agent", Quirks.user_agent(uri.host)}
+ ]
+
+ proxy = Keyword.get(Application.get_env(:nola, __MODULE__, []), :proxy, nil)
+ options = [follow_redirect: false, max_body_length: 30_000_000, proxy: proxy]
+ url = URI.to_string(uri)
+
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} when status in [400, 403] ->
+ Logger.warning("Was denied to fetch URL, using scraper #{url} = #{status}")
+ retry_expand_with_scraper(acc, url)
+
{:error, status, _headers} ->
- text = Plug.Conn.Status.reason_phrase(status)
- {:ok, acc, "Error: HTTP #{text} (#{status})"}
+ Logger.error("Error fetching URL #{url} = #{status}")
+ {:ok, acc, nil}
+
{:error, {:tls_alert, {:handshake_failure, err}}} ->
- {:ok, acc, "TLS Error: #{to_string(err)}"}
+ Logger.error("Error fetching URL #{url} = TLS Error: #{to_string(err)}")
+ {:ok, acc, nil}
+
+ {:error, :timeout} ->
+ Logger.error("Error fetching URL #{url} = timeout")
+ retry_expand_with_scraper(acc, url)
+
{:error, reason} ->
- {:ok, acc, "Error: #{to_string(reason)}"}
+ Logger.error("Error fetching URL #{url} = #{to_string(reason)}")
+ {:ok, acc, nil}
end
end
@@ -261,6 +352,27 @@ defmodule Nola.Plugins.Link do
{:ok, [uri], "-> #{URI.to_string(uri)}"}
end
+ # Last resort: scrape the page
+ # We'll be mostly calling this when 403 or 500 or timeout because site blocks us.
+ # An external service will scrape the page for us and return the body.
+ # We'll call directly the HTML handler on the result.
+ defp retry_expand_with_scraper(acc, url) do
+ Logger.info("Attempting scraper")
+ handlers = Keyword.get(Application.get_env(:nola, __MODULE__), :handlers)
+ Logger.info("Attempting scraper #{inspect(handlers)}")
+
+ with true <- Keyword.has_key?(handlers, :"Nola.Plugins.Link.HTML"),
+ {:ok, body, _meta} <- Scraper.get(url),
+ {:ok, text} <- __MODULE__.HTML.post_expand(url, body, nil, nil) do
+ {:ok, acc, text}
+ else
+ error ->
+ Logger.debug("Attempt with scraper failed: #{inspect(error)}")
+ # We give up here. We don't return anything (the acc from caller `expand default`
+ # does not matter anymore) and I see returning error messages as useless.
+ {:ok, acc, nil}
+ end
+ end
defp human_size(bytes) do
bytes
diff --git a/lib/plugins/link/data/quirks_rewrite_host.txt b/lib/plugins/link/data/quirks_rewrite_host.txt
new file mode 100644
index 0000000..0533ce7
--- /dev/null
+++ b/lib/plugins/link/data/quirks_rewrite_host.txt
@@ -0,0 +1,27 @@
+# list of host rewrites
+# fixups: https://gist.github.com/Lexedia/bbbde4dbbf628b0bfe8476a96a977a8f
+
+x.com:fxtwitter.com
+twitter.com:fxtwitter.com
+vxtwitter.com:fxtwitter.com
+girlcockx.com:fxtwitter.com
+stupidpenisx.com:fxtwitter.com
+
+bsky.app:xsky.app
+
+instagram.com:ddinstagram.com
+
+tumblr.com:tpmblr.com
+
+# reddit: new reddit don't have titles for pages we don't handle in the reddit module
+# fallback to old. which has nice titles
+reddit.com:old.reddit.com
+
+pixiv.net:phixiv.net
+
+threads.net:fixthreads.net
+
+tiktok.com:tnktok.com
+vm.tiktok.com:vm.tnktok.com
+
+twitch.tv:fxtwitch.tv
diff --git a/lib/plugins/link/data/quirks_telegram_bot_user_agent.txt b/lib/plugins/link/data/quirks_telegram_bot_user_agent.txt
new file mode 100644
index 0000000..3008272
--- /dev/null
+++ b/lib/plugins/link/data/quirks_telegram_bot_user_agent.txt
@@ -0,0 +1,30 @@
+# list of domains which needs a telegram bot user agent
+# to return proper og:* meta tags
+# www. subdomain is added automatically
+
+x.com
+vxtwitter.com
+fxtwitter.com
+fixupx.com
+
+facebook.com
+
+instagram.com
+xnstagram.com
+ddinstagram.com
+
+tpmblr.com
+
+reddit.com
+old.reddit.com
+
+xsky.app
+
+phixiv.net
+
+fixthreads.net
+
+tnktok.com
+vm.tnktok.com
+
+fxtwitch.tv
diff --git a/lib/plugins/link/github.ex b/lib/plugins/link/github.ex
index 0069a40..fcd76a0 100644
--- a/lib/plugins/link/github.ex
+++ b/lib/plugins/link/github.ex
@@ -3,11 +3,10 @@ defmodule Nola.Plugins.Link.Github do
@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
+ with ["", user, repo] <- String.split(path, "/") do
+ {true, %{user: user, repo: repo, path: "#{user}/#{repo}"}}
+ else
+ _ -> false
end
end
@@ -18,32 +17,66 @@ defmodule Nola.Plugins.Link.Github do
@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
+ with {:ok, response} <- HTTPoison.get("https://api.github.com/repos/#{user}/#{repo}"),
+ {:ok, json} <- Jason.decode(response.body) do
+ info = %{
+ full_name: json["full_name"],
+ disabled: json["disabled"],
+ archived: json["archived"],
+ source: json["source"],
+ description: json["description"],
+ topics: json["topics"],
+ language: json["language"],
+ open_issues_count: json["open_issues_count"],
+ pushed_at: json["pushed_at"],
+ stargazers_count: json["stargazers_count"],
+ subscribers_count: json["subscribers_count"],
+ forks_count: json["forks_count"]
+ }
+
+ start = build_start(info)
+ tags = build_tags(info)
+ network = build_network(info)
+
+ {:ok, [start, tags, network]}
+ else
+ _ -> :error
end
end
+ defp build_start(info) do
+ parts =
+ []
+ |> maybe_add(info.disabled, " (disabled)")
+ |> maybe_add(info.archived, " (archived)")
+ |> maybe_add(
+ info.source && info.source["full_name"] != info.full_name,
+ " (⑂ #{info.source["full_name"]})"
+ )
+
+ "#{info.full_name}#{parts} - #{info.description}"
+ end
+
+ defp build_tags(info) do
+ for(t <- info.topics || [], do: "##{t}") |> Enum.intersperse(", ") |> Enum.join("")
+ end
+
+ defp build_network(info) do
+ lang = (info.language && "#{info.language} - ") || ""
+ issues = (info.open_issues_count && "#{info.open_issues_count} issues - ") || ""
+
+ last_push =
+ if at = info.pushed_at do
+ {:ok, date, _} = DateTime.from_iso8601(at)
+ " - last pushed #{DateTime.to_string(date)}"
+ else
+ ""
+ end
+
+ "#{lang}#{issues}#{info.stargazers_count} stars - #{info.subscribers_count} watchers - #{info.forks_count} forks#{last_push}"
+ end
+
+ defp maybe_add(acc, condition, value) do
+ if condition, do: acc ++ [value], else: acc
+ end
end
diff --git a/lib/plugins/link/html.ex b/lib/plugins/link/html.ex
index a941aac..7df1bfc 100644
--- a/lib/plugins/link/html.ex
+++ b/lib/plugins/link/html.ex
@@ -5,102 +5,154 @@ defmodule Nola.Plugins.Link.HTML do
def match(_, _), do: false
@impl true
- def post_match(_url, "text/html"<>_, _header, _opts) do
- {:body, nil}
- end
+ def post_match(_url, "text/html" <> _, _header, _opts), do: {:body, nil}
def post_match(_, _, _, _), do: false
@impl true
def post_expand(url, body, _params, _opts) do
- html = Floki.parse(body)
- title = collect_title(html)
+ {:ok, html} = Floki.parse_document(body)
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} — "
+ text =
+ if has_sufficient_opengraph_data?(opengraph) do
+ generate_text_from_opengraph(url, html, opengraph)
else
- ""
+ clean_text(collect_title(html))
end
- [clean_text("#{prefix}#{Map.get(opengraph, "title")}")] ++ Nola.Irc.Message.splitlong(clean_text("#{date}#{Map.get(opengraph, "description")}"))
- else
- clean_text(title)
- end
+
{:ok, text}
end
+ defp has_sufficient_opengraph_data?(opengraph) do
+ Map.has_key?(opengraph, "title") && Map.has_key?(opengraph, "description")
+ end
+
+ defp generate_text_from_opengraph(url, html, opengraph) do
+ itemprops = collect_itemprops(html)
+ prefix = collect_prefix_and_site_name(url, opengraph, itemprops)
+ title = Map.get(opengraph, "title")
+ description = collect_description(opengraph, itemprops, title, 400)
+
+ [clean_text("#{prefix}#{title}")] ++ description
+ end
+
defp collect_title(html) do
case Floki.find(html, "title") do
- [{"title", [], [title]} | _] ->
- String.trim(title)
- _ ->
- nil
+ [{"title", [], [title]} | _] -> String.trim(title)
+ _ -> ""
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)
+ Floki.find(html, "head meta")
+ |> Enum.reverse()
+ |> Enum.reduce(%{}, &extract_meta_tag/2)
+ end
+
+ defp extract_meta_tag({"meta", values, []}, acc) do
+ with {_, name} <- List.keyfind(values, "property", 0, {nil, nil}),
+ {_, content} <- List.keyfind(values, "content", 0, {nil, nil}),
+ true <- is_valid_meta_tag?(name) do
+ Map.put(acc, strip_prefix(name), content)
+ else
+ _ -> acc
+ end
+ end
+
+ defp extract_meta_tag(_, acc), do: acc
+
+ defp is_valid_meta_tag?(nil) do
+ false
+ end
+
+ defp is_valid_meta_tag?(name) do
+ String.starts_with?(name, "og:") || String.starts_with?(name, "article:")
end
+ defp strip_prefix("og:" <> key), do: key
+ defp strip_prefix(other), do: other
+
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)
+ Floki.find(html, "[itemprop]")
+ |> Enum.reduce(%{}, &extract_itemprop/2)
+ end
+
+ defp extract_itemprop({"meta", values, []}, acc) do
+ with {_, name} <- List.keyfind(values, "itemprop", 0, {nil, nil}),
+ {_, content} <- List.keyfind(values, "content", 0, {nil, nil}),
+ true <- String.starts_with?(name, "article:") do
+ Map.put(acc, name, content)
+ else
+ _ -> acc
+ end
+ end
+
+ defp extract_itemprop(_, acc), do: acc
+
+ defp collect_prefix_and_site_name(url, opengraph, itemprops) do
+ uri = URI.parse(url)
+ site_name = Map.get(opengraph, "site_name", uri.host)
+ paywall_status = get_paywall_status(opengraph, itemprops)
+ section = get_section(opengraph, itemprops)
+
+ prefix = "#{paywall_status}#{site_name}#{section}"
+ if prefix == "", do: "", else: "#{prefix} — "
+ end
+
+ defp get_paywall_status(opengraph, itemprops) do
+ content_tier =
+ Map.get(
+ opengraph,
+ "article:content_tier",
+ Map.get(itemprops, "article:content_tier", "free")
+ )
+
+ if content_tier == "free", do: "", else: "[paywall] "
+ end
+
+ defp get_section(opengraph, itemprops) do
+ section = Map.get(opengraph, "article:section", Map.get(itemprops, "article:section"))
+ if section, do: ": #{section}", else: ""
+ end
+
+ defp collect_description(opengraph, itemprops, title, max_length) do
+ date = get_formatted_date(opengraph, itemprops)
+ description = transform_description(Map.get(opengraph, "description"), max_length)
+
+ cond do
+ title == description -> date
+ String.jaro_distance(title, description) > 0.8 -> date
+ true -> Nola.Irc.Message.splitlong(clean_text("#{date}#{description}"))
+ end
+ end
+
+ defp get_formatted_date(opengraph, itemprops) do
+ published_time =
+ Map.get(
+ opengraph,
+ "article:published_time",
+ Map.get(itemprops, "article:published_time", "")
+ )
+
+ case DateTime.from_iso8601(published_time) do
+ {:ok, date, _} -> "#{Timex.format!(date, "%d/%m/%y", :strftime)}. "
+ _ -> ""
+ end
+ end
+
+ # TODO: Swap with AI description instead of truncating.
+ defp transform_description(nil, _), do: nil
+
+ defp transform_description(string, length) when is_binary(string) do
+ if String.length(string) > length, do: "#{String.slice(string, 0..length)}…", else: string
end
defp clean_text(text) do
text
|> String.replace("\n", " ")
+ |> String.replace("<br>", " ")
+ |> String.replace("<br/>", " ")
+ |> String.replace("<br />", " ")
|> HtmlEntities.decode()
end
-
-
end
diff --git a/lib/plugins/link/image.ex b/lib/plugins/link/image.ex
index cf3d9b0..2fb6862 100644
--- a/lib/plugins/link/image.ex
+++ b/lib/plugins/link/image.ex
@@ -6,7 +6,7 @@ defmodule Nola.Plugins.Link.Image do
def match(_, _), do: false
@impl true
- def post_match(_url, "image/"<>_, _header, _opts) do
+ def post_match(_url, "image/" <> _, _header, _opts) do
{:body, nil}
end
@@ -16,65 +16,77 @@ defmodule Nola.Plugins.Link.Image do
def post_expand(_url, bytes, _, opts) do
pil_process = Keyword.get(opts, :pil_process, {:pil, :"py@127.0.0.1"})
clip_ask_process = Keyword.get(opts, :clip_ask_process, {:clip_ask, :"py@127.0.0.1"})
- img2txt_process = Keyword.get(opts, :img2txt_process, {:image_to_text_vit_gpt2, :"py@127.0.0.1"})
-
- tasks = [
- Task.async(fn ->
- {:ok, pil} = GenServer.call(pil_process, {:run, bytes})
- pil = pil
- |> Enum.map(fn({k, v}) -> {String.to_atom(to_string(k)), v} end)
- pil
- end),
- Task.async(fn ->
- {:ok, descr} = GenServer.call(img2txt_process, {:run, bytes})
- {:img2txt, to_string(descr)}
- end),
- Task.async(fn ->
- {:ok, prompts} = GenServer.call(clip_ask_process, {:run, bytes})
-
- prompts = prompts
- |> Enum.sort_by(& elem(&1, 1), &>=/2)
- |> Enum.take(3)
- |> Enum.map(& to_string(elem(&1, 0)))
- |> Enum.join(", ")
- {:prompts, prompts}
+
+ img2txt_process =
+ Keyword.get(opts, :img2txt_process, {:image_to_text_vit_gpt2, :"py@127.0.0.1"})
+
+ tasks =
+ [
+ Task.async(fn ->
+ {:ok, pil} = GenServer.call(pil_process, {:run, bytes})
+
+ pil =
+ pil
+ |> Enum.map(fn {k, v} -> {String.to_atom(to_string(k)), v} end)
+
+ pil
+ end),
+ Task.async(fn ->
+ {:ok, descr} = GenServer.call(img2txt_process, {:run, bytes})
+ {:img2txt, to_string(descr)}
+ end),
+ Task.async(fn ->
+ {:ok, prompts} = GenServer.call(clip_ask_process, {:run, bytes})
+
+ prompts =
+ prompts
+ |> Enum.sort_by(&elem(&1, 1), &>=/2)
+ |> Enum.take(3)
+ |> Enum.map(&to_string(elem(&1, 0)))
+ |> Enum.join(", ")
+
+ {:prompts, prompts}
+ end)
+ ]
+ |> Task.yield_many(5000)
+ |> Enum.map(fn {task, res} ->
+ res || Task.shutdown(task, :brutal_kill)
end)
- ]
- |> Task.yield_many(5000)
- |> Enum.map(fn {task, res} ->
- res || Task.shutdown(task, :brutal_kill)
- end)
- results = Enum.into(List.flatten(for({:ok, value} <- tasks, do: value)), Map.new)
+ results = Enum.into(List.flatten(for({:ok, value} <- tasks, do: value)), Map.new())
img2txt = Map.get(results, :img2txt)
prompts = Map.get(results, :prompts)
- pil = if Map.get(results, :width) do
- animated = if Map.get(results, :animated), do: " animated", else: ""
- "#{Map.get(results, :width, 0)}x#{Map.get(results, :height, 0)}#{animated} — "
- else
- ""
- end
+ pil =
+ if Map.get(results, :width) do
+ animated = if Map.get(results, :animated), do: " animated", else: ""
+ "#{Map.get(results, :width, 0)}x#{Map.get(results, :height, 0)}#{animated} — "
+ else
+ ""
+ end
- descr = cond do
- img2txt && prompts ->
- "#{pil}#{prompts} — #{img2txt}"
- img2txt ->
- "#{pil}#{img2txt}"
- prompts ->
- "#{pil}#{prompts}"
- pil != "" ->
- "#{pil}"
- true ->
- nil
- end
+ descr =
+ cond do
+ img2txt && prompts ->
+ "#{pil}#{prompts} — #{img2txt}"
+
+ img2txt ->
+ "#{pil}#{img2txt}"
+
+ prompts ->
+ "#{pil}#{prompts}"
+
+ pil != "" ->
+ "#{pil}"
+
+ true ->
+ nil
+ end
if descr do
{:ok, "image: #{descr}"}
else
:error
end
-
end
-
end
diff --git a/lib/plugins/link/img_debrid_link.ex b/lib/plugins/link/img_debrid_link.ex
new file mode 100644
index 0000000..39a60a9
--- /dev/null
+++ b/lib/plugins/link/img_debrid_link.ex
@@ -0,0 +1,35 @@
+defmodule Nola.Plugins.Link.ImgDebridLink do
+ @behaviour Nola.Plugins.Link
+
+ @impl true
+ def match(uri = %URI{host: host, path: path}, _opts)
+ when host in ["img.debrid-link.fr", "img.debrid-link.com"] do
+ case String.split(path, "/") do
+ ["", ids] -> {true, %{id: ids}}
+ _ -> false
+ end
+ end
+
+ def match(_, _), do: false
+
+ @impl true
+ def post_match(_, _, _, _), do: false
+
+ @impl true
+ def expand(_uri, %{id: ids}, _opts) do
+ with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+ HTTPoison.get("https://img.debrid-link.com/api/#{ids}/infos", [], []),
+ {:ok, %{"success" => true, "value" => values}} <- Jason.decode(body) do
+ items =
+ for %{"name" => name, "url" => %{"direct" => direct_url}} <- values do
+ "#{name}: #{direct_url}"
+ end
+
+ {:ok, items}
+ else
+ error ->
+ Logger.info("error: #{inspect(error)}")
+ :error
+ end
+ end
+end
diff --git a/lib/plugins/link/imgur.ex b/lib/plugins/link/imgur.ex
index 9fe9354..49bbb7d 100644
--- a/lib/plugins/link/imgur.ex
+++ b/lib/plugins/link/imgur.ex
@@ -19,19 +19,24 @@ defmodule Nola.Plugins.Link.Imgur do
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
+
+ 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
+
+ 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
+
+ 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
@@ -49,6 +54,7 @@ defmodule Nola.Plugins.Link.Imgur 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)
@@ -59,6 +65,7 @@ defmodule Nola.Plugins.Link.Imgur do
width = Map.get(data, "width")
size = Map.get(data, "size")
{:ok, "image, #{width}x#{height}, #{size} bytes #{nsfw}#{title}"}
+
other ->
:error
end
@@ -68,6 +75,7 @@ defmodule Nola.Plugins.Link.Imgur 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)
@@ -75,22 +83,31 @@ defmodule Nola.Plugins.Link.Imgur do
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
+
+ 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/plugins/link/pdf.ex b/lib/plugins/link/pdf.ex
index e91dcc2..bb14594 100644
--- a/lib/plugins/link/pdf.ex
+++ b/lib/plugins/link/pdf.ex
@@ -6,7 +6,7 @@ defmodule Nola.Plugins.Link.PDF do
def match(_, _), do: false
@impl true
- def post_match(_url, "application/pdf"<>_, _header, _opts) do
+ def post_match(_url, "application/pdf" <> _, _header, _opts) do
{:file, nil}
end
@@ -16,24 +16,32 @@ defmodule Nola.Plugins.Link.PDF do
def post_expand(url, file, _, _) do
case System.cmd("pdftitle", ["-p", file]) do
{text, 0} ->
- text = text
- |> String.trim()
+ text =
+ text
+ |> String.trim()
if text == "" do
:error
else
basename = Path.basename(url, ".pdf")
- text = "[#{basename}] " <> text
- |> String.split("\n")
+
+ text =
+ ("[#{basename}] " <> text)
+ |> String.split("\n")
+
{:ok, text}
end
+
{_, 127} ->
- Logger.error("dependency `pdftitle` is missing, please install it: `pip3 install pdftitle`.")
+ 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}")
+ Logger.warn("command `pdftitle` exited with status code #{code}:\n#{inspect(error)}")
:error
end
end
-
end
diff --git a/lib/plugins/link/quirks.ex b/lib/plugins/link/quirks.ex
new file mode 100644
index 0000000..6f46f6b
--- /dev/null
+++ b/lib/plugins/link/quirks.ex
@@ -0,0 +1,45 @@
+defmodule Nola.Plugins.Link.Quirks do
+ @rewrite_hosts "./lib/plugins/link/data/quirks_rewrite_host.txt"
+ |> Util.read_file_list!()
+ |> Enum.map(fn line ->
+ [old, new] = String.split(line, ":")
+ {old, new}
+ end)
+ |> Enum.map(fn {old, new} -> [{old, new}, {"www.#{old}", new}] end)
+ |> List.flatten()
+ |> then(fn list ->
+ IO.puts("Link Quirks: rewrite_hosts: #{inspect(list)}")
+ list
+ end)
+
+ for {old, new} <- @rewrite_hosts do
+ def uri(%URI{host: unquote(old)} = uri) do
+ %URI{uri | host: unquote(new)}
+ end
+ end
+
+ def uri(url) do
+ url
+ end
+
+ @telegram_bot_hosts "./lib/plugins/link/data/quirks_telegram_bot_user_agent.txt"
+ |> Util.read_file_list!()
+ |> Enum.map(fn h -> [h, "www.#{h}"] end)
+ |> List.flatten()
+ |> then(fn list ->
+ IO.puts("Link Quirks: telegram_bot_hosts: #{inspect(list)}")
+ list
+ end)
+
+ def user_agent(host) when host in @telegram_bot_hosts do
+ "TelegramBot (like TwitterBot)"
+ end
+
+ def user_agent(_host) do
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
+ end
+
+ def list() do
+ [rewrite_hosts: @rewrite_hosts, telegram_bot_hosts: @telegram_bot_hosts]
+ end
+end
diff --git a/lib/plugins/link/redacted.ex b/lib/plugins/link/redacted.ex
index a7cfe74..0c14520 100644
--- a/lib/plugins/link/redacted.ex
+++ b/lib/plugins/link/redacted.ex
@@ -2,7 +2,10 @@ defmodule Nola.Plugins.Link.Redacted do
@behaviour Nola.Plugins.Link
@impl true
- def match(uri = %URI{host: "redacted.ch", path: "/torrent.php", query: query = "id="<>id}, _opts) do
+ 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
@@ -14,5 +17,4 @@ defmodule Nola.Plugins.Link.Redacted do
def expand(_uri, %{torrent: id}, _opts) do
end
-
end
diff --git a/lib/plugins/link/reddit.ex b/lib/plugins/link/reddit.ex
index 016e025..2a6527d 100644
--- a/lib/plugins/link/reddit.ex
+++ b/lib/plugins/link/reddit.ex
@@ -6,14 +6,18 @@ defmodule Nola.Plugins.Link.Reddit 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}}
+
+ # ["", "u", user] ->
+ # {true, %{mode: :user, path: path, user: user}}
_ ->
false
end
@@ -33,87 +37,120 @@ defmodule Nola.Plugins.Link.Reddit do
@impl true
def expand(_, %{mode: :sub, sub: sub}, _opts) do
url = "https://api.reddit.com/r/#{sub}/about"
- case HTTPoison.get(url) do
+
+ case HTTPoison.get(url, [],
+ proxy: Keyword.get(Application.get_env(:nola, __MODULE__, []), :proxy, nil)
+ ) 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"
+ 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
+ case HTTPoison.get("https://api.reddit.com#{path}?sr_detail=true", [],
+ proxy: Keyword.get(Application.get_env(:nola, __MODULE__, []), :proxy, nil)
+ ) 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)
+
+ 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?, 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
+
+ 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}"
+
+ content =
+ "by u/#{Map.get(op, "author")} - #{state_str}#{up} up, #{comments} comments - #{self_str}"
{:ok, [title, content]}
+
err ->
:error
end
end
-
end
diff --git a/lib/plugins/link/scraper.ex b/lib/plugins/link/scraper.ex
new file mode 100644
index 0000000..c30ae5f
--- /dev/null
+++ b/lib/plugins/link/scraper.ex
@@ -0,0 +1,66 @@
+defmodule Nola.Plugins.Link.Scraper do
+ defmodule UseScraper do
+ require Logger
+
+ def get(url, config) do
+ base_url = Keyword.get(config, :base_url, "https://api.usescraper.com")
+ api_key = Keyword.get(config, :api_key, "unset api key")
+ options = Keyword.get(config, :http_options, [])
+
+ headers = [
+ {"user-agent", "nola, href@random.sh"},
+ {"content-type", "application/json"},
+ {"authorization", "Bearer " <> api_key}
+ ]
+
+ Logger.debug("scraper: use_scraper: get: #{url}")
+
+ with {:ok, json} <- Poison.encode(%{"url" => url, "format" => "html"}),
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
+ HTTPoison.post("#{base_url}/scraper/scrape", json, headers, options),
+ {:ok,
+ %{
+ "status" => "scraped",
+ "html" => body,
+ "meta" => meta = %{"fetchedUrlStatusCode" => 200}
+ }} <- Poison.decode(body) do
+ {:ok, body, meta}
+ else
+ {:ok,
+ %{
+ "status" => "scraped",
+ "text" => body,
+ "meta" => meta = %{"fetchedUrlStatusCode" => code}
+ }} ->
+ Logger.error("scraper: use_scraper: scraper got http #{code} for #{url}")
+ status = Plug.Conn.Status.reason_atom(code)
+ {:error, status}
+
+ {:ok, %{"status" => "failed"}} ->
+ Logger.error("scraper: use_scraper: scraper service failed for #{url}")
+ {:error, :scrape_failed}
+
+ {:ok, %HTTPoison.Response{status_code: code, body: body}} ->
+ Logger.error("scraper: use_scraper: scraper service failed (http #{code}) for #{url}")
+ status = Plug.Conn.Status.reason_atom(code)
+ {:error, status}
+
+ {:error, %HTTPoison.Error{reason: reason}} ->
+ Logger.error(
+ "scraper: use_scraper: scraper service failed (http #{inspect(reason)}) for #{url}"
+ )
+
+ {:error, reason}
+ end
+ end
+ end
+
+ def get(url) do
+ config = Keyword.get(Application.get_env(:nola, Nola.Plugins.Link, []), :scraper) || []
+
+ case config[:service] do
+ "usescraper" -> UseScraper.get(url, config[:config] || [])
+ _ -> {:error, :scraping_disabled}
+ end
+ end
+end
diff --git a/lib/plugins/link/store.ex b/lib/plugins/link/store.ex
new file mode 100644
index 0000000..4e2aa58
--- /dev/null
+++ b/lib/plugins/link/store.ex
@@ -0,0 +1,95 @@
+defmodule Nola.Plugins.Link.Store do
+ alias DialyxirVendored.Warnings.Apply
+ use GenServer
+ require Logger
+ require Record
+ import Ex2ms
+
+ @type url() :: String.t()
+
+ Record.defrecord(:link, link: nil, result: nil, at: nil)
+ @type link :: record(:link, link: url(), result: any(), at: nil)
+
+ Record.defrecord(:link_seen, key: nil, at: nil)
+
+ @doc "A `link_seen` record represents a link that has been seen at a specific time in a given context."
+ @type link_seen :: record(:link_seen, key: {url(), String.t()}, at: nil)
+
+ def setup do
+ :ets.new(:links, [:set, :public, :named_table, keypos: 2])
+ :ets.new(:links_witness, [:set, :public, :named_table, keypos: 2])
+ end
+
+ @spec insert_link(url(), any()) :: true
+ def insert_link(url, result) do
+ :ets.insert(
+ :links,
+ link(link: url, result: result, at: DateTime.utc_now() |> DateTime.to_unix())
+ )
+ end
+
+ @spec get_link(url()) :: any() | nil
+ def get_link(url) do
+ case :ets.lookup(:links, url) do
+ [link(result: result)] -> result
+ [] -> nil
+ end
+ end
+
+ @spec witness_link(url(), String.t()) :: boolean()
+ def inhibit_link?(url, key) do
+ case :ets.lookup(:links_witness, {url, key}) do
+ [_] -> true
+ [] -> false
+ end
+ end
+
+ @spec witness_link(url(), String.t()) :: :ok | :inhibit
+ def witness_link(url, key) do
+ if inhibit_link?(url, key) do
+ :inhibit
+ else
+ :ets.insert(
+ :links_witness,
+ link_seen(key: {url, key}, at: DateTime.utc_now() |> DateTime.to_unix())
+ )
+
+ :ok
+ end
+ end
+
+ def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
+
+ @doc false
+ @impl true
+ def init(_) do
+ setup()
+ env = Keyword.fetch!(Application.get_env(:nola, Nola.Plugins.Link, []), :store)
+ :erlang.send_after(env[:interval], self(), :expire)
+ {:ok, nil}
+ end
+
+ @doc false
+ @impl true
+ def handle_info(:expire, state) do
+ env = Keyword.fetch!(Application.get_env(:nola, Nola.Plugins.Link, []), :store)
+ :erlang.send_after(env[:interval], self(), :expire)
+ ttl = env[:ttl] / 1000
+ inhibit = env[:inhibit] / 1000
+ now = DateTime.utc_now() |> DateTime.to_unix()
+
+ links_evicted =
+ :ets.select_delete(:links, [
+ {{:_, :_, :_, :"$1"}, [{:<, :"$1", now - ttl}], [true]}
+ ])
+
+ witness_evicted =
+ :ets.select_delete(:links_witness, [
+ {{:_, :_, :"$1"}, [{:<, :"$1", now - inhibit}], [true]}
+ ])
+
+ Logger.debug("evicted #{links_evicted} links and #{witness_evicted} witnesses")
+
+ {:noreply, state}
+ end
+end
diff --git a/lib/plugins/link/twitter.ex b/lib/plugins/link/twitter.ex
index 48e6bae..ac2efe7 100644
--- a/lib/plugins/link/twitter.ex
+++ b/lib/plugins/link/twitter.ex
@@ -22,12 +22,15 @@ defmodule Nola.Plugins.Link.Twitter do
* `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
+ 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
+
+ _ ->
+ false
end
end
@@ -62,56 +65,75 @@ defmodule Nola.Plugins.Link.Twitter do
# 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 =
+ if tweet.quoted_status do
+ quote_url = link_tweet(tweet.quoted_status, opts, true)
+ String.replace(text, quote_url, "")
+ else
+ text
+ end
+
text = Nola.Irc.Message.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
+ 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)
- quoted = if tweet.quoted_status do
- full_text = tweet.quoted_status
- |> expand_twitter_text(opts)
- |> Nola.Irc.Message.splitlong_with_prefix(">")
+ text =
+ if tweet.in_reply_to_screen_name == tweet.user.screen_name,
+ do: "continued from",
+ else: "replying to"
- head = format_tweet_header(tweet.quoted_status, opts, details: false, prefix: "↓ quoting")
+ <<3, 15, " ↪ ", text::binary, " ", reply_url::binary, 3>>
+ end
- [head | full_text]
- else
- []
- end
+ quoted =
+ if tweet.quoted_status do
+ full_text =
+ tweet.quoted_status
+ |> expand_twitter_text(opts)
+ |> Nola.Irc.Message.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
+ # <<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)
- 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)
+ 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
+
+ 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)
- |> Enum.join(" ")
- String.replace(text, entity.url, url)
- end)
- |> HtmlEntities.decode()
+ |> HtmlEntities.decode()
end
defp format_tweet_header(tweet, opts, format_opts \\ []) do
@@ -134,25 +156,28 @@ defmodule Nola.Plugins.Link.Twitter do
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
+
+ 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 =
+ 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 =
+ meta
+ |> Enum.filter(& &1)
+ |> Enum.join(" - ")
meta = <<3, 15, meta::binary, " → #{link}", 3>>
<<author::binary, " — ", meta::binary>>
end
-
end
diff --git a/lib/plugins/link/youtube.ex b/lib/plugins/link/youtube.ex
index 0114940..adf9337 100644
--- a/lib/plugins/link/youtube.ex
+++ b/lib/plugins/link/youtube.ex
@@ -17,11 +17,12 @@ defmodule Nola.Plugins.Link.YouTube do
"""
@impl true
- def match(uri = %URI{host: yt, path: "/watch", query: "v="<>video_id}, _opts) when yt in ["youtube.com", "www.youtube.com"] do
+ 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
+ def match(%URI{host: "youtu.be", path: "/" <> video_id}, _opts) do
{true, %{video_id: video_id}}
end
@@ -33,40 +34,58 @@ defmodule Nola.Plugins.Link.YouTube do
@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
+
+ 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"]}
+ end
+
+ {:ok,
+ line ++
+ [
+ "#{snippet["title"]}",
+ "— #{duration} — uploaded by #{snippet["channelTitle"]} — #{date}" <>
+ " — #{item["statistics"]["viewCount"]} views, #{item["statistics"]["likeCount"]} likes"
+ ]}
else
:error
end
- _ -> :error
+
+ _ ->
+ :error
end
end
end
-
end
diff --git a/lib/plugins/logger.ex b/lib/plugins/logger.ex
index 46c2a5b..1418ddc 100644
--- a/lib/plugins/logger.ex
+++ b/lib/plugins/logger.ex
@@ -32,52 +32,53 @@ defmodule Nola.Plugins.Logger do
end
def handle_info(info, state) do
- Logger.debug("logger_plugin: unhandled info: #{inspect info}")
+ Logger.debug("logger_plugin: unhandled info: #{inspect(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}")
+ Logger.debug("logger_plugin: saved: #{inspect(id)}")
state
+
error ->
- Logger.error("logger_plugin: save failed: #{inspect 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("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("logger_plugin: catched processing for #{inspect(entry)}: #{inspect(e)}")
Logger.error(Exception.format(e, b, __STACKTRACE__))
state
end
def format_to_db(msg = %Nola.Message{id: id}) do
- channel = cond do
- msg.channel -> msg.channel
- msg.account -> msg.account.id
- msg.sender -> msg.sender.nick
- true -> nil
- end
+ channel =
+ cond do
+ msg.channel -> msg.channel
+ msg.account -> msg.account.id
+ msg.sender -> msg.sender.nick
+ true -> nil
+ end
- id = [msg.network, channel, id]
- |> Enum.filter(& &1)
- |> Enum.join(":")
+ id =
+ [msg.network, channel, id]
+ |> Enum.filter(& &1)
+ |> Enum.join(":")
- %{"_id" => id,
+ %{
+ "_id" => id,
"type" => "nola.message:v1",
"object" => %Nola.Message{msg | meta: Map.delete(msg.meta, :from)}
}
end
def format_to_db(anything) do
- %{"_id" => FlakeId.get(),
- "type" => "object",
- "object" => anything}
+ %{"_id" => FlakeId.get(), "type" => "object", "object" => anything}
end
-
end
diff --git a/lib/plugins/preums.ex b/lib/plugins/preums.ex
index 83d99cd..5891ab3 100644
--- a/lib/plugins/preums.ex
+++ b/lib/plugins/preums.ex
@@ -37,45 +37,47 @@ defmodule Nola.Plugins.Preums do
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)
+ :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) ->
+ 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
+ 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
+ fun = fn x = {{chan, date}, account_id, time, perfect, text}, acc ->
+ if (channel == nil and chan) or channel == chan do
+ incr_points = if NaiveDateTime.utc_now().year == time.year, do: 1, else: 0
{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})
+ Map.put(acc, account_id, {count + 1, points + incr_points})
else
acc
end
end
+
:dets.foldl(fun, %{}, dets)
- |> Enum.sort_by(fn({_account_id, value}) -> elem(value, sort_elem) end, &>=/2)
+ |> 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
@@ -90,39 +92,51 @@ defmodule Nola.Plugins.Preums do
{:ok, _} = Registry.register(Nola.PubSub, "messages", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "triggers", regopts)
{:ok, dets} = :dets.open_file(dets(), [{:repair, :force}])
- Util.ets_mutate_select_each(:dets, dets, [{:"$1", [], [:"$1"]}], fn(table, obj) ->
+
+ 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} ->
+ {{net, {bork, chan}}, date} ->
:dets.delete(table, key)
- nick = if Nola.Account.get(nick) do
- nick
- else
- if acct = Nola.Account.find_always_by_nick(net, nil, nick) do
- acct.id
- else
+
+ nick =
+ if Nola.Account.get(nick) do
nick
+ else
+ if acct = Nola.Account.find_always_by_nick(net, nil, nick) do
+ acct.id
+ else
+ nick
+ end
end
- end
- :dets.insert(table, { { {net,chan}, date }, nick, now, perfect, text})
+
+ :dets.insert(table, {{{net, chan}, date}, nick, now, perfect, text})
+
{{_net, nil}, _} ->
:dets.delete(table, key)
+
{{net, chan}, date} ->
if !Nola.Account.get(nick) do
if acct = Nola.Account.find_always_by_nick(net, chan, nick) do
:dets.delete(table, key)
- :dets.insert(table, { { {net,chan}, date }, acct.id, now, perfect, text})
+ :dets.insert(table, {{{net, chan}, date}, acct.id, now, perfect, text})
end
end
+
_ ->
- Logger.debug("DID NOT FIX: #{inspect key}")
+ Logger.debug("DID NOT FIX: #{inspect(key)}")
end
end)
+
{:ok, %{dets: dets}}
end
# Latest
- def handle_info({:irc, :trigger, "preums", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "preums", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang}}},
+ state
+ ) do
channelkey = {m.network, m.channel}
state = handle_preums(m, state)
tz = timezone(channelkey)
@@ -130,14 +144,16 @@ defmodule Nola.Plugins.Preums do
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
+
+ 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
- end
if item do
{_, account_id, date, _perfect, text} = item
@@ -147,33 +163,45 @@ defmodule Nola.Plugins.Preums do
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 = %Nola.Message{trigger: %Nola.Trigger{type: :dot}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "preums", m = %Nola.Message{trigger: %Nola.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 = Nola.Account.get(account_id)
- user = Nola.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
+
+ top =
+ topnicks(state.dets, channel, sort_by: :score)
+ |> Enum.map(fn {account_id, {count, score}} ->
+ account = Nola.Account.get(account_id)
+ user = Nola.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 = %Nola.Message{trigger: %Nola.Trigger{type: :query}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "preums", m = %Nola.Message{trigger: %Nola.Trigger{type: :query}}},
+ state
+ ) do
state = handle_preums(m, state)
msg = "!preums - preums du jour, .preums top preumseurs"
m.replymsg.(msg)
@@ -194,27 +222,33 @@ defmodule Nola.Plugins.Preums do
# 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) ->
+
+ 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
+ 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) ->
+
+ 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
@@ -222,7 +256,6 @@ defmodule Nola.Plugins.Preums 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})
@@ -247,20 +280,23 @@ defmodule Nola.Plugins.Preums do
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)
+ perfect? = Enum.any?([~r/preum(s|)/i], 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}")
+ Logger.error("#{__MODULE__} dets lookup failed: #{inspect(error)}")
state
end
else
@@ -271,6 +307,4 @@ defmodule Nola.Plugins.Preums do
def score(_chan, _account, _time, _perfect, _text) do
1
end
-
-
end
diff --git a/lib/plugins/quatre_cent_vingt.ex b/lib/plugins/quatre_cent_vingt.ex
index 6b3cc46..f530446 100644
--- a/lib/plugins/quatre_cent_vingt.ex
+++ b/lib/plugins/quatre_cent_vingt.ex
@@ -10,11 +10,15 @@ defmodule Nola.Plugins.QuatreCentVingt do
"""
@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!!"]
+ 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 [
@@ -23,7 +27,7 @@ defmodule Nola.Plugins.QuatreCentVingt do
"~~o∞~~",
"*\\o/*",
"**\\o/**",
- "*ô*",
+ "*ô*"
]
@coeffs Range.new(1, 100)
@@ -34,75 +38,102 @@ defmodule Nola.Plugins.QuatreCentVingt do
def init(_) do
for coeff <- @coeffs do
- {:ok, _} = Registry.register(Nola.PubSub, "trigger:#{420*coeff}", [plugin: __MODULE__])
+ {:ok, _} = Registry.register(Nola.PubSub, "trigger:#{420 * coeff}", plugin: __MODULE__)
end
- {:ok, _} = Registry.register(Nola.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, _} = Registry.register(Nola.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 = %Nola.Message{trigger: %Nola.Trigger{args: [], type: :bang}}}, dets) do
+
+ def handle_info(
+ {:irc, :trigger, unquote(qvc),
+ m = %Nola.Message{trigger: %Nola.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
+ # this is ugly
+ now = DateTime.to_unix(DateTime.utc_now()) - 1
+
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
- ""
+ :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 = %Nola.Message{trigger: %Nola.Trigger{args: [nick], type: :bang}}}, dets) do
+ def handle_info(
+ {:irc, :trigger, "420",
+ m = %Nola.Message{trigger: %Nola.Trigger{args: [nick], type: :bang}}},
+ dets
+ ) do
account = Nola.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
+ 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) ->
+
+ 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
+ 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) ->
+
+ 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
@@ -115,23 +146,26 @@ defmodule Nola.Plugins.QuatreCentVingt do
: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")
+ 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)
+ 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
@@ -145,5 +179,4 @@ defmodule Nola.Plugins.QuatreCentVingt do
emoji = Enum.random(@emojis)
"#{emoji} [#{count}]"
end
-
end
diff --git a/lib/plugins/radio_france.ex b/lib/plugins/radio_france.ex
index d95c54a..e9adc4e 100644
--- a/lib/plugins/radio_france.ex
+++ b/lib/plugins/radio_france.ex
@@ -23,27 +23,45 @@ defmodule Nola.Plugins.RadioFrance do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(Nola.PubSub, "trigger:radiofrance", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:rf", regopts)
+
for s <- @shortcuts do
{:ok, _} = Registry.register(Nola.PubSub, "trigger:#{s}", regopts)
end
+
{:ok, nil}
end
- def handle_info({:irc, :trigger, "rf", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "rf", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang}}},
+ state
+ ) do
handle_info({:irc, :trigger, "radiofrance", m}, state)
end
- def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: []}}}, state) do
+ def handle_info(
+ {:irc, :trigger, @trigger,
+ m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: []}}},
+ state
+ ) do
m.replyfun.("radiofrance: précisez la station!")
{:noreply, state}
end
- def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}}, state) do
+ def handle_info(
+ {:irc, :trigger, @trigger,
+ m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}},
+ state
+ ) do
now(args_to_station(args), m)
{:noreply, state}
end
- def handle_info({:irc, :trigger, trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}}, state) when trigger in @shortcuts do
+ def handle_info(
+ {:irc, :trigger, trigger,
+ m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}},
+ state
+ )
+ when trigger in @shortcuts do
now(args_to_station([trigger | args]), m)
{:noreply, state}
end
@@ -56,7 +74,7 @@ defmodule Nola.Plugins.RadioFrance do
end
def handle_info(info, state) do
- Logger.debug("unhandled info: #{inspect info}")
+ Logger.debug("unhandled info: #{inspect(info)}")
{:noreply, state}
end
@@ -67,7 +85,7 @@ defmodule Nola.Plugins.RadioFrance do
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"])
+ 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"])
@@ -76,21 +94,25 @@ defmodule Nola.Plugins.RadioFrance do
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:"
+ 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: "suivi de:"
+ if next_song?, do: "🔜", else: "à suivre:"
end
- else
- if next_song?, do: "🔜", else: "à suivre:"
- end
+
m.replyfun.("#{next_prefix} #{next}")
end
@@ -117,9 +139,11 @@ defmodule Nola.Plugins.RadioFrance do
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
@@ -129,5 +153,4 @@ defmodule Nola.Plugins.RadioFrance do
|> String.replace("france", "france ")
|> String.replace("_", " ")
end
-
end
diff --git a/lib/plugins/say.ex b/lib/plugins/say.ex
index 114ca64..3ccd0a4 100644
--- a/lib/plugins/say.ex
+++ b/lib/plugins/say.ex
@@ -1,5 +1,4 @@
defmodule Nola.Plugins.Say do
-
def irc_doc do
"""
# say
@@ -25,31 +24,39 @@ defmodule Nola.Plugins.Say do
{:ok, nil}
end
- def handle_info({:irc, :trigger, "say", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do
+ 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
+ end
- def handle_info({:irc, :trigger, "asay", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do
+ 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
+ end
- def handle_info({:irc, :text, m = %{text: "say "<>rest}}, state) do
+ 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
+ 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
@@ -60,7 +67,9 @@ defmodule Nola.Plugins.Say do
defp say_for(account, target, text, with_nick?) do
for {net, chan} <- Nola.Membership.of_account(account) do
chan2 = String.replace(chan, "#", "")
- if (target == "#{net}/#{chan}" || target == "#{net}/#{chan2}" || target == chan || target == chan2) do
+
+ if target == "#{net}/#{chan}" || target == "#{net}/#{chan2}" || target == chan ||
+ target == chan2 do
if with_nick? do
Nola.Irc.send_message_as(account, net, chan, text)
else
@@ -69,5 +78,4 @@ defmodule Nola.Plugins.Say do
end
end
end
-
end
diff --git a/lib/plugins/script.ex b/lib/plugins/script.ex
index c8d00a9..0a65627 100644
--- a/lib/plugins/script.ex
+++ b/lib/plugins/script.ex
@@ -23,20 +23,24 @@ defmodule Nola.Plugins.Script do
end
def init([]) do
- {:ok, _} = Registry.register(Nola.PubSub, "trigger:script", [plugin: __MODULE__])
- dets_filename = (Nola.data_path() <> "/" <> "scripts.dets") |> String.to_charlist
+ {:ok, _} = Registry.register(Nola.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
+ 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
+ # prout
+ ["del", name] -> :ok
+ # stop
+ [name] -> :ok
end
end
-
end
diff --git a/lib/plugins/seen.ex b/lib/plugins/seen.ex
index 045702c..cff0928 100644
--- a/lib/plugins/seen.ex
+++ b/lib/plugins/seen.ex
@@ -6,6 +6,7 @@ defmodule Nola.Plugins.Seen do
"""
def irc_doc, do: @moduledoc
+
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@@ -19,7 +20,11 @@ defmodule Nola.Plugins.Seen do
{:ok, %{dets: dets}}
end
- def handle_info({:irc, :trigger, "seen", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: [nick]}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "seen",
+ m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: [nick]}}},
+ state
+ ) do
witness(m, state)
m.replyfun.(last_seen(m.channel, nick, state))
{:noreply, state}
@@ -43,17 +48,20 @@ defmodule Nola.Plugins.Seen do
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)
+ 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à..."
+
+ true ->
+ "#{nick} est là..."
end
+
[] ->
"je ne connais pas de #{nick}"
end
end
-
end
diff --git a/lib/plugins/sms.ex b/lib/plugins/sms.ex
index 8dd15ad..713ac3f 100644
--- a/lib/plugins/sms.ex
+++ b/lib/plugins/sms.ex
@@ -8,9 +8,10 @@ defmodule Nola.Plugins.Sms do
def irc_doc, do: @moduledoc
require Logger
- def incoming(from, "enable "<>key) do
+ def incoming(from, "enable " <> key) do
key = String.trim(key)
account = Nola.Account.find_meta_account("sms-validation-code", String.downcase(key))
+
if account do
net = Nola.Account.get_meta(account, "sms-validation-target")
Nola.Account.put_meta(account, "sms-number", from)
@@ -24,15 +25,21 @@ defmodule Nola.Plugins.Sms do
def incoming(from, message) do
account = Nola.Account.find_meta_account("sms-number", from)
+
if account do
- reply_fun = fn(text) ->
+ reply_fun = fn text ->
send_sms(from, text)
end
- trigger_text = if Enum.any?(Nola.Irc.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(message, trigger) end) do
- message
- else
- "!"<>message
- end
+
+ trigger_text =
+ if Enum.any?(Nola.Irc.Connection.triggers(), fn {trigger, _} ->
+ String.starts_with?(message, trigger)
+ end) do
+ message
+ else
+ "!" <> message
+ end
+
message = %Nola.Message{
id: FlakeId.get(),
transport: :sms,
@@ -44,7 +51,8 @@ defmodule Nola.Plugins.Sms do
replyfun: reply_fun,
trigger: Nola.Irc.Connection.extract_trigger(trigger_text)
}
- Logger.debug("converted sms to message: #{inspect message}")
+
+ Logger.debug("converted sms to message: #{inspect(message)}")
Nola.Irc.Connection.publish(message, ["messages:sms"])
message
end
@@ -69,53 +77,70 @@ defmodule Nola.Plugins.Sms do
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!()
+
+ 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: 200}} ->
+ :ok
+
{:ok, %HTTPoison.Response{status_code: code} = resp} ->
- Logger.error("SMS Error: #{inspect resp}")
+ Logger.error("SMS Error: #{inspect(resp)}")
{:error, code}
- {:error, error} -> {:error, error}
+
+ {:error, error} ->
+ {:error, error}
end
end
def init([]) do
- {:ok, _} = Registry.register(Nola.PubSub, "trigger:sms", [plugin: __MODULE__])
+ {:ok, _} = Registry.register(Nola.PubSub, "trigger:sms", plugin: __MODULE__)
:ok = register_ovh_callback()
{:ok, %{}}
:ignore
end
- def handle_info({:irc, :trigger, "sms", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: [nick | text]}}}, state) do
- with \
- {:tree, false} <- {:tree, m.sender.nick == "Tree"},
- {_, %Nola.Account{} = account} <- {:account, Nola.Account.find_always_by_nick(m.network, m.channel, nick)},
- {_, number} when not is_nil(number) <- {:number, Nola.Account.get_meta(account, "sms-number")}
- do
+ def handle_info(
+ {:irc, :trigger, "sms",
+ m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: [nick | text]}}},
+ state
+ ) do
+ with {:tree, false} <- {:tree, m.sender.nick == "Tree"},
+ {_, %Nola.Account{} = account} <-
+ {:account, Nola.Account.find_always_by_nick(m.network, m.channel, nick)},
+ {_, number} when not is_nil(number) <-
+ {:number, Nola.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
+
+ 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}")
+ {: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
@@ -125,19 +150,26 @@ defmodule Nola.Plugins.Sms do
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"
+
+ 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!()
+ |> 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
+
+ error ->
+ error
end
end
@@ -147,8 +179,13 @@ defmodule Nola.Plugins.Sms do
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}]
+
+ 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
diff --git a/lib/plugins/tell.ex b/lib/plugins/tell.ex
index b4d05dc..923c2ef 100644
--- a/lib/plugins/tell.ex
+++ b/lib/plugins/tell.ex
@@ -8,6 +8,7 @@ defmodule Nola.Plugins.Tell do
"""
def irc_doc, do: @moduledoc
+
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@@ -24,7 +25,7 @@ defmodule Nola.Plugins.Tell do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(Nola.PubSub, "account", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:tell", regopts)
- {:ok, dets} = :dets.open_file(dets(), [type: :bag])
+ {:ok, dets} = :dets.open_file(dets(), type: :bag)
{:ok, %{dets: dets}}
end
@@ -33,44 +34,59 @@ defmodule Nola.Plugins.Tell do
{:noreply, state}
end
- def handle_info({:irc, :trigger, "tell", m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: [target | message]}}}, state) do
+ def handle_info(
+ {:irc, :trigger, "tell",
+ m = %Nola.Message{trigger: %Nola.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 = Nola.Account.get(from)
- user = Nola.UserTrack.find_by_account(network, account)
- fromnick = if user, do: user.nick, else: account.name
- "#{nick}: <#{fromnick}> #{message}"
- end)
- Enum.each(strs, fn(s) -> Nola.Irc.Connection.broadcast_message(network, channel, s) end)
+ strs =
+ Enum.map(messages, fn {_, from, message, at} ->
+ account = Nola.Account.get(from)
+ user = Nola.UserTrack.find_by_account(network, account)
+ fromnick = if user, do: user.nick, else: account.name
+ "#{nick}: <#{fromnick}> #{message}"
+ end)
+
+ Enum.each(strs, fn s -> Nola.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) ->
+ # :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 ->
+ {{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
+
+ _ ->
+ :ok
end
end)
+
{:noreply, state}
end
-
def handle_info(info, state) do
{:noreply, state}
end
@@ -83,16 +99,15 @@ defmodule Nola.Plugins.Tell do
defp do_tell(state, m, nick_target, message) do
target = Nola.Account.find_always_by_nick(m.network, m.channel, nick_target)
message = Enum.join(message, " ")
- with \
- {:target, %Nola.Account{} = target} <- {:target, target},
+
+ with {:target, %Nola.Account{} = target} <- {:target, target},
{:same, false} <- {:same, target.id == m.account.id},
- target_user = Nola.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)),
+ target_user = Nola.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()}
+ {: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
@@ -102,5 +117,4 @@ defmodule Nola.Plugins.Tell do
{:message, _} -> m.replyfun.("can't tell without a message")
end
end
-
end
diff --git a/lib/plugins/txt.ex b/lib/plugins/txt.ex
index 3b431e9..c582e67 100644
--- a/lib/plugins/txt.ex
+++ b/lib/plugins/txt.ex
@@ -53,12 +53,21 @@ defmodule Nola.Plugins.Txt do
end
def init([]) do
- dets_locks_filename = (Nola.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist
+ 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.Plugins.Txt.Markov.Native)
+
+ markov_handler =
+ Keyword.get(
+ Application.get_env(:nola, __MODULE__, []),
+ :markov_handler,
+ Nola.Plugins.Txt.MarkovPyMarkovify
+ )
+
{:ok, markov} = markov_handler.start_link()
- {:ok, _} = Registry.register(Nola.PubSub, "triggers", [plugin: __MODULE__])
- {:ok, %__MODULE__{locks: locks, markov_handler: markov_handler, markov: markov, triggers: load()}}
+ {:ok, _} = Registry.register(Nola.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
@@ -69,7 +78,10 @@ defmodule Nola.Plugins.Txt do
# ADMIN: RW/RO
#
- def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :plus}}}, state = %{rw: false}) do
+ 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}}
@@ -78,7 +90,10 @@ defmodule Nola.Plugins.Txt do
end
end
- def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :minus}}}, state = %{rw: true}) do
+ 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}}
@@ -91,26 +106,30 @@ defmodule Nola.Plugins.Txt do
# 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
+ 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
+ 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
@@ -119,26 +138,26 @@ defmodule Nola.Plugins.Txt do
#
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
+ total =
+ Enum.reduce(state.triggers, 0, fn {_, data}, acc ->
+ acc + Enum.count(data)
+ 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"
+ link =
+ NolaWeb.Router.Helpers.irc_url(
+ NolaWeb.Endpoint,
+ :txt,
+ msg.network,
+ NolaWeb.format_chan(msg.channel)
+ )
+
+ total = "#{Enum.count(state.triggers)} fichiers, #{to_string(total)} lignes: #{link}"
ro = if !state.rw, do: " (lecture seule activée)", else: ""
- (detail<>total<>ro)
+ (total <> ro)
|> msg.replyfun.()
+
{:noreply, state}
end
@@ -147,48 +166,53 @@ defmodule Nola.Plugins.Txt do
#
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]
+ result =
+ Enum.reduce(state.triggers, [], fn {trigger, data}, acc ->
+ Enum.reduce(data, acc, fn {l, _}, acc ->
+ [{trigger, l} | acc]
+ end)
end)
- end)
- |> Enum.shuffle()
+ |> 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)
+ 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)
- |> Enum.shuffle()
- end)
if result do
{source, line} = result
msg.replyfun.(["#{source}: " | line])
end
+
{:noreply, state}
end
@@ -200,11 +224,15 @@ defmodule Nola.Plugins.Txt do
end
def with_stateful_results(key, initfun) do
- pid = case :global.whereis_name(key) do
- :undefined ->
- start_stateful_results(key, initfun.())
- pid -> pid
- end
+ 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
@@ -214,19 +242,24 @@ defmodule Nola.Plugins.Txt do
def start_stateful_results(key, list) do
me = self()
- {pid, _} = spawn_monitor(fn() ->
- Process.monitor(me)
- stateful_results(me, list)
- end)
+
+ {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
@@ -246,10 +279,11 @@ defmodule Nola.Plugins.Txt do
:get ->
send(owner, {:stateful_results, line})
stateful_results(owner, rest)
+
{:DOWN, _ref, :process, ^owner, _} ->
:ok
- after
- @stateful_results_expire -> :ok
+ after
+ @stateful_results_expire -> :ok
end
end
@@ -261,20 +295,28 @@ defmodule Nola.Plugins.Txt do
case state.markov_handler.sentence(state.markov) do
{:ok, line} ->
msg.replyfun.(line)
+
error ->
- Logger.error "Txt Markov error: "<>inspect 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
+ 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
+ Logger.error("Txt Markov error: " <> inspect(error))
end
+
{:noreply, state}
end
@@ -282,12 +324,13 @@ defmodule Nola.Plugins.Txt do
# TXT CREATE
#
- def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do
- with \
- {trigger, _} <- clean_trigger(trigger),
- true <- can_write?(state, msg, trigger),
- :ok <- create_file(trigger)
- do
+ 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
@@ -301,23 +344,35 @@ defmodule Nola.Plugins.Txt do
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
+ 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
@@ -325,18 +380,20 @@ defmodule Nola.Plugins.Txt do
# 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
+ 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}")
+ Logger.debug("txt add failed: #{inspect(error)}")
{:noreply, state}
end
end
@@ -346,13 +403,11 @@ defmodule Nola.Plugins.Txt do
#
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)
+ 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)
@@ -363,7 +418,7 @@ defmodule Nola.Plugins.Txt do
end
end
- def handle_info(:reload_markov, state=%__MODULE__{triggers: triggers, markov: markov}) do
+ def handle_info(:reload_markov, state = %__MODULE__{triggers: triggers, markov: markov}) do
state.markov_handler.reload(state.triggers, state.markov)
{:noreply, state}
end
@@ -382,41 +437,48 @@ defmodule Nola.Plugins.Txt 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
+ 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.with_index
- Map.put(m, key, data)
- end)
- |> Enum.sort
- |> Enum.into(Map.new)
+ |> 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", [])
+ 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
@@ -429,30 +491,36 @@ defmodule Nola.Plugins.Txt do
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
+ 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
+
+ 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)
+ 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
@@ -466,50 +534,57 @@ defmodule Nola.Plugins.Txt 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)
+ 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}}
+ {:error, {:jaro, string, idx + 1}}
else
- File.write!(directory() <> "/" <> trigger <> ".txt", content<>"\n", [:append])
- idx = Enum.count(triggers[trigger])+1
+ File.write!(directory() <> "/" <> trigger <> ".txt", content <> "\n", [:append])
+ idx = Enum.count(triggers[trigger]) + 1
{:ok, idx}
end
else
{:error, :notxt}
end
- _ -> {:error, :badarg}
+
+ _ ->
+ {: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] =
+ 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
+
+ (prefix <> line)
|> String.split("\\\\")
- |> Enum.map(fn(line) ->
+ |> Enum.map(fn line ->
String.split(line, "\\\\\\\\")
end)
|> List.flatten()
- |> Enum.map(fn(line) ->
+ |> Enum.map(fn line ->
String.trim(line)
|> Tmpl.render(msg)
end)
@@ -521,10 +596,13 @@ defmodule Nola.Plugins.Txt do
defp can_write?(%{rw: rw?, locks: locks}, msg = %{channel: nil, sender: sender}, trigger) do
admin? = Nola.Irc.admin?(sender)
- locked? = case :dets.lookup(locks, trigger) do
- [{trigger}] -> true
- _ -> false
- end
+
+ locked? =
+ case :dets.lookup(locks, trigger) do
+ [{trigger}] -> true
+ _ -> false
+ end
+
unlocked? = if rw? == false, do: false, else: !locked?
can? = unlocked? || admin?
@@ -533,16 +611,24 @@ defmodule Nola.Plugins.Txt 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
+ defp can_write?(
+ state = %__MODULE__{rw: rw?, locks: locks},
+ msg = %{channel: channel, sender: sender},
+ trigger
+ ) do
admin? = Nola.Irc.admin?(sender)
operator? = Nola.UserTrack.operator?(msg.network, channel, sender.nick)
- locked? = case :dets.lookup(locks, trigger) do
- [{trigger}] -> true
- _ -> false
- end
+
+ locked? =
+ case :dets.lookup(locks, trigger) do
+ [{trigger}] -> true
+ _ -> false
+ end
+
unlocked? = if rw? == false, do: false, else: !locked?
can? = admin? || operator? || unlocked?
@@ -550,7 +636,7 @@ defmodule Nola.Plugins.Txt 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/plugins/txt/markov.ex b/lib/plugins/txt/markov.ex
index b47666c..2b3d210 100644
--- a/lib/plugins/txt/markov.ex
+++ b/lib/plugins/txt/markov.ex
@@ -1,9 +1,7 @@
defmodule Nola.Plugins.Txt.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}
-
+ @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/plugins/txt/markov_native.ex b/lib/plugins/txt/markov_native.ex
deleted file mode 100644
index aa6b454..0000000
--- a/lib/plugins/txt/markov_native.ex
+++ /dev/null
@@ -1,33 +0,0 @@
-defmodule Nola.Plugins.Txt.MarkovNative do
- @behaviour Nola.Plugins.Txt.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/plugins/txt/markov_py_markovify.ex b/lib/plugins/txt/markov_py_markovify.ex
index f79ed47..47ff0a7 100644
--- a/lib/plugins/txt/markov_py_markovify.ex
+++ b/lib/plugins/txt/markov_py_markovify.ex
@@ -1,5 +1,4 @@
defmodule Nola.Plugins.Txt.MarkovPyMarkovify do
-
def start_link() do
{:ok, nil}
end
@@ -19,7 +18,8 @@ defmodule Nola.Plugins.Txt.MarkovPyMarkovify do
defp run(args \\ []) do
{binary, script} = script()
args = [script, Path.expand(Nola.Plugins.Txt.directory()) | args]
- IO.puts "Args #{inspect args}"
+ IO.puts("Args #{inspect(args)}")
+
case MuonTrap.cmd(binary, args) do
{response, 0} -> response
{response, code} -> "error #{code}: #{response}"
@@ -28,12 +28,11 @@ defmodule Nola.Plugins.Txt.MarkovPyMarkovify do
defp script() do
default_script = to_string(:code.priv_dir(:nola)) <> "/irc/txt/markovify.py"
- env = Application.get_env(:nola, Nola.Plugins.Txt, [])
- |> Keyword.get(:py_markovify, [])
+
+ env =
+ Application.get_env(:nola, Nola.Plugins.Txt, [])
+ |> Keyword.get(:py_markovify, [])
{Keyword.get(env, :python, "python3"), Keyword.get(env, :script, default_script)}
end
-
-
-
end
diff --git a/lib/plugins/untappd.ex b/lib/plugins/untappd.ex
index e409172..5a4c070 100644
--- a/lib/plugins/untappd.ex
+++ b/lib/plugins/untappd.ex
@@ -1,5 +1,4 @@
defmodule Nola.Plugins.Untappd do
-
def irc_doc() do
"""
# [Untappd](https://untappd.com)
@@ -18,49 +17,67 @@ defmodule Nola.Plugins.Untappd do
end
def init(_) do
- {:ok, _} = Registry.register(Nola.PubSub, "trigger:beer", [plugin: __MODULE__])
+ {:ok, _} = Registry.register(Nola.PubSub, "trigger:beer", plugin: __MODULE__)
{:ok, %{}}
end
- def handle_info({:irc, :trigger, _, m = %Nola.Message{trigger: %{type: :bang, args: args}}}, state) do
+ def handle_info(
+ {:irc, :trigger, _, m = %Nola.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")}°"
+
+ 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(", ")
+
+ 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 = %Nola.Message{trigger: %{type: :query, args: args}}}, state) do
+ def handle_info(
+ {:irc, :trigger, _, m = %Nola.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("")
+ 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/plugins/user_mention.ex b/lib/plugins/user_mention.ex
index e7c7420..1a9881c 100644
--- a/lib/plugins/user_mention.ex
+++ b/lib/plugins/user_mention.ex
@@ -19,11 +19,24 @@ defmodule Nola.Plugins.UserMention do
{:ok, nil}
end
- def handle_info({:irc, :trigger, nick, message = %Nola.Message{sender: sender, account: account, network: network, channel: channel, trigger: %Nola.Trigger{type: :at, args: content}}}, state) do
- nick = nick
- |> String.trim(":")
- |> String.trim(",")
+ def handle_info(
+ {:irc, :trigger, nick,
+ message = %Nola.Message{
+ sender: sender,
+ account: account,
+ network: network,
+ channel: channel,
+ trigger: %Nola.Trigger{type: :at, args: content}
+ }},
+ state
+ ) do
+ nick =
+ nick
+ |> String.trim(":")
+ |> String.trim(",")
+
target = Nola.Account.find_always_by_nick(network, channel, nick)
+
if target do
telegram = Nola.Account.get_meta(target, "telegram-id")
sms = Nola.Account.get_meta(target, "sms-number")
@@ -31,22 +44,27 @@ defmodule Nola.Plugins.UserMention do
cond do
telegram ->
- Nola.Telegram.send_message(telegram, "`#{channel}` <**#{sender.nick}**> #{Enum.join(content, " ")}")
+ Nola.Telegram.send_message(
+ telegram,
+ "`#{channel}` <**#{sender.nick}**> #{Enum.join(content, " ")}"
+ )
+
sms ->
case Nola.Plugins.Sms.send_sms(sms, text) do
{:error, code} -> message.replyfun("#{sender.nick}: erreur #{code} (sms)")
end
+
true ->
Nola.Plugins.Tell.tell(message, nick, content)
end
else
- message.replyfun.("#{nick} m'est inconnu")
+ false
end
+
{:noreply, state}
end
def handle_info(_, state) do
{:noreply, state}
end
-
end
diff --git a/lib/plugins/wikipedia.ex b/lib/plugins/wikipedia.ex
index 47b14da..0bcbee7 100644
--- a/lib/plugins/wikipedia.ex
+++ b/lib/plugins/wikipedia.ex
@@ -15,15 +15,24 @@ defmodule Nola.Plugins.Wikipedia do
end
def init(_) do
- {:ok, _} = Registry.register(Nola.PubSub, "trigger:wp", [plugin: __MODULE__])
+ {:ok, _} = Registry.register(Nola.PubSub, "trigger:wp", plugin: __MODULE__)
{:ok, nil}
end
- def handle_info({:irc, :trigger, _, message = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: []}}}, state) do
+ def handle_info(
+ {:irc, :trigger, _,
+ message = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: []}}},
+ state
+ ) do
irc_random(message)
{:noreply, state}
end
- def handle_info({:irc, :trigger, _, message = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}}, state) do
+
+ def handle_info(
+ {:irc, :trigger, _,
+ message = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}},
+ state
+ ) do
irc_search(Enum.join(args, " "), message)
{:noreply, state}
end
@@ -33,19 +42,22 @@ defmodule Nola.Plugins.Wikipedia do
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,
+ "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
@@ -58,6 +70,7 @@ defmodule Nola.Plugins.Wikipedia do
"grnnamespace" => 0,
"prop" => "info"
}
+
case query_wikipedia(params) do
{:ok, %{"query" => %{"pages" => map = %{}}}} ->
[{_, item}] = Map.to_list(map)
@@ -65,6 +78,7 @@ defmodule Nola.Plugins.Wikipedia do
url = "https://fr.wikipedia.org/wiki/" <> String.replace(title, " ", "_")
msg = "Wikipédia: #{title} — #{url}"
message.replyfun.(msg)
+
_ ->
nil
end
@@ -72,19 +86,23 @@ defmodule Nola.Plugins.Wikipedia do
defp query_wikipedia(params) do
url = "https://fr.wikipedia.org/w/api.php"
- params = params
- |> Map.put("format", "json")
- |> Map.put("utf8", "")
+
+ 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: 200, body: body}} ->
+ Jason.decode(body)
+
{:ok, %HTTPoison.Response{status_code: 400, body: body}} ->
- Logger.error "Wikipedia HTTP 400: #{inspect body}"
+ Logger.error("Wikipedia HTTP 400: #{inspect(body)}")
{:error, "http 400"}
+
error ->
- Logger.error "Wikipedia http error: #{inspect error}"
+ Logger.error("Wikipedia http error: #{inspect(error)}")
{:error, "http client error"}
end
end
-
end
diff --git a/lib/plugins/wolfram_alpha.ex b/lib/plugins/wolfram_alpha.ex
index 120af16..f9d5a5e 100644
--- a/lib/plugins/wolfram_alpha.ex
+++ b/lib/plugins/wolfram_alpha.ex
@@ -15,33 +15,44 @@ defmodule Nola.Plugins.WolframAlpha do
end
def init(_) do
- {:ok, _} = Registry.register(Nola.PubSub, "trigger:wa", [plugin: __MODULE__])
+ {:ok, _} = Registry.register(Nola.PubSub, "trigger:wa", plugin: __MODULE__)
{:ok, nil}
end
- def handle_info({:irc, :trigger, _, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: query}}}, state) do
+ def handle_info(
+ {:irc, :trigger, _, m = %Nola.Message{trigger: %Nola.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
+
+ 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
+ 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/plugins/youtube.ex b/lib/plugins/youtube.ex
index 39bf03d..5e36301 100644
--- a/lib/plugins/youtube.ex
+++ b/lib/plugins/youtube.ex
@@ -16,11 +16,17 @@ defmodule Nola.Plugins.YouTube do
end
def init([]) do
- for t <- ["trigger:yt", "trigger:youtube"], do: {:ok, _} = Registry.register(Nola.PubSub, t, [plugin: __MODULE__])
+ for t <- ["trigger:yt", "trigger:youtube"],
+ do: {:ok, _} = Registry.register(Nola.PubSub, t, plugin: __MODULE__)
+
{:ok, %__MODULE__{}}
end
- def handle_info({:irc, :trigger, _, message = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}}, state) do
+ def handle_info(
+ {:irc, :trigger, _,
+ message = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: args}}},
+ state
+ ) do
irc_search(Enum.join(args, " "), message)
{:noreply, state}
end
@@ -34,71 +40,91 @@ defmodule Nola.Plugins.YouTube 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"
+
+ 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)
+ message.replyfun.("Erreur YouTube: " <> error)
+
_ ->
nil
end
end
defp search(query) do
- query = query
- |> String.strip
+ 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,
+ "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}"
+ Logger.error("YouTube HTTP #{code}: #{inspect(body)}")
{:error, "http #{code}"}
+
error ->
- Logger.error "YouTube http error: #{inspect 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}"
+ Logger.error("YouTube HTTP #{code}: #{inspect(body)}")
{:error, "http #{code}"}
+
error ->
- Logger.error "YouTube http error: #{inspect error}"
+ Logger.error("YouTube http error: #{inspect(error)}")
:error
end
end
-
end
diff --git a/lib/telegram.ex b/lib/telegram.ex
index b161b63..289b913 100644
--- a/lib/telegram.ex
+++ b/lib/telegram.ex
@@ -34,42 +34,62 @@ defmodule Nola.Telegram do
{:ok, %{room_state: room_state}}
end
- def handle_update(%{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/start"<>_}}, _token, state) do
- text = "*Welcome to beautte!*\n\nQuery the bot on IRC and say \"enable-telegram\" to continue."
+ def handle_update(
+ %{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/start" <> _}},
+ _token,
+ state
+ ) do
+ text =
+ "*Welcome to beautte!*\n\nQuery the bot on IRC and say \"enable-telegram\" to continue."
+
send_message(m["chat"]["id"], text)
{:ok, %{account: nil}}
end
- def handle_update(%{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/enable"<>_}}, _token, state) do
- key = case String.split(text, " ") do
- ["/enable", key | _] -> key
- _ -> "nil"
- end
+ def handle_update(
+ %{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/enable" <> _}},
+ _token,
+ state
+ ) do
+ key =
+ case String.split(text, " ") do
+ ["/enable", key | _] -> key
+ _ -> "nil"
+ end
- #Handled message "1247435154:AAGnSSCnySn0RuVxy_SUcDEoOX_rbF6vdq0" %{"message" =>
+ # Handled message "1247435154:AAGnSSCnySn0RuVxy_SUcDEoOX_rbF6vdq0" %{"message" =>
# %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
# "date" => 1591027272, "entities" =>
# [%{"length" => 7, "offset" => 0, "type" => "bot_command"}],
# "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
# "message_id" => 11, "text" => "/enable salope"}, "update_id" => 764148578}
account = Nola.Account.find_meta_account("telegram-validation-code", String.downcase(key))
- text = if account do
- net = Nola.Account.get_meta(account, "telegram-validation-target")
- Nola.Account.put_meta(account, "telegram-id", m["chat"]["id"])
- Nola.Account.put_meta(account, "telegram-username", m["chat"]["username"])
- Nola.Account.put_meta(account, "telegram-username", m["chat"]["username"])
- Nola.Account.delete_meta(account, "telegram-validation-code")
- Nola.Account.delete_meta(account, "telegram-validation-target")
- Nola.Irc.Connection.broadcast_message(net, account, "Telegram #{m["chat"]["username"]} account added!")
- "Yay! Linked to account **#{account.name}**."
- else
- "Token invalid"
- end
+
+ text =
+ if account do
+ net = Nola.Account.get_meta(account, "telegram-validation-target")
+ Nola.Account.put_meta(account, "telegram-id", m["chat"]["id"])
+ Nola.Account.put_meta(account, "telegram-username", m["chat"]["username"])
+ Nola.Account.put_meta(account, "telegram-username", m["chat"]["username"])
+ Nola.Account.delete_meta(account, "telegram-validation-code")
+ Nola.Account.delete_meta(account, "telegram-validation-target")
+
+ Nola.Irc.Connection.broadcast_message(
+ net,
+ account,
+ "Telegram #{m["chat"]["username"]} account added!"
+ )
+
+ "Yay! Linked to account **#{account.name}**."
+ else
+ "Token invalid"
+ end
+
send_message(m["chat"]["id"], text)
{:ok, %{account: account.id}}
end
- #[debug] Unhandled update: %{"message" =>
+ # [debug] Unhandled update: %{"message" =>
# %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
# "date" => 1591096015,
# "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
@@ -87,7 +107,7 @@ defmodule Nola.Telegram do
end
end
- #[debug] Unhandled update: %{"callback_query" =>
+ # [debug] Unhandled update: %{"callback_query" =>
# %{
# "chat_instance" => "-7948978714441865930", "data" => "evolu.net/#dmz",
# "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
@@ -101,79 +121,139 @@ defmodule Nola.Telegram do
# }
# , "update_id" => 218161568}
- #def handle_update(t, %{"callback_query" => cb = %{"data" => "resend", "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}) do
- #end
+ # def handle_update(t, %{"callback_query" => cb = %{"data" => "resend", "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}) do
+ # end
- def handle_update(%{"callback_query" => cb = %{"data" => "start-upload:"<>target, "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}, t, state) do
+ def handle_update(
+ %{
+ "callback_query" =>
+ cb = %{
+ "data" => "start-upload:" <> target,
+ "id" => id,
+ "message" =>
+ m = %{
+ "message_id" => m_id,
+ "chat" => %{"id" => chat_id},
+ "reply_to_message" => op
+ }
+ }
+ },
+ t,
+ state
+ ) do
account = Nola.Account.find_meta_account("telegram-id", chat_id)
+
if account do
- target = case String.split(target, "/") do
- ["everywhere"] -> Nola.Membership.of_account(account)
- [net, chan] -> [{net, chan}]
- end
- Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Processing...", reply_markup: %{})
-
- {content, type} = cond do
- op["photo"] -> {op["photo"], ""}
- op["voice"] -> {op["voice"], " a voice message"}
- op["video"] -> {op["video"], ""}
- op["document"] -> {op["document"], ""}
- op["animation"] -> {op["animation"], ""}
- end
+ target =
+ case String.split(target, "/") do
+ ["everywhere"] -> Nola.Membership.of_account(account)
+ [net, chan] -> [{net, chan}]
+ end
+
+ Telegram.Api.request(t, "editMessageText",
+ chat_id: chat_id,
+ message_id: m_id,
+ text: "Processing...",
+ reply_markup: %{}
+ )
+
+ {content, type} =
+ cond do
+ op["photo"] -> {op["photo"], ""}
+ op["voice"] -> {op["voice"], " a voice message"}
+ op["video"] -> {op["video"], ""}
+ op["document"] -> {op["document"], ""}
+ op["animation"] -> {op["animation"], ""}
+ end
+
+ file =
+ if is_list(content) && Enum.count(content) > 1 do
+ Enum.sort_by(content, fn p -> p["file_size"] end, &>=/2)
+ |> List.first()
+ else
+ content
+ end
- file = if is_list(content) && Enum.count(content) > 1 do
- Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2)
- |> List.first()
- else
- content
- end
file_id = file["file_id"]
file_unique_id = file["file_unique_id"]
- text = if(op["caption"], do: ": "<> op["caption"] <> "", else: "")
- resend = %{"inline_keyboard" => [ [%{"text" => "re-share", "callback_data" => "resend"}] ]}
- spawn(fn() ->
- with \
- {:ok, file} <- Telegram.Api.request(t, "getFile", file_id: file_id),
- path = "https://api.telegram.org/file/bot#{t}/#{file["file_path"]}",
- {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path),
- <<smol_body::binary-size(20), _::binary>> = body,
- {:ok, magic} <- GenMagic.Pool.perform(Nola.GenMagic, {:bytes, smol_body}),
- bucket = Application.get_env(:nola, :s3, []) |> Keyword.get(:bucket),
- ext = Path.extname(file["file_path"]),
- s3path = "#{account.id}/#{file_unique_id}#{ext}",
- Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "*Uploading...*", reply_markup: %{}, parse_mode: "MarkdownV2"),
- s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type),
- {:ok, _} <- ExAws.request(s3req)
- do
+ text = if(op["caption"], do: ": " <> op["caption"] <> "", else: "")
+ resend = %{"inline_keyboard" => [[%{"text" => "re-share", "callback_data" => "resend"}]]}
+
+ spawn(fn ->
+ with {:ok, file} <- Telegram.Api.request(t, "getFile", file_id: file_id),
+ path = "https://api.telegram.org/file/bot#{t}/#{file["file_path"]}",
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path),
+ <<smol_body::binary-size(20), _::binary>> = body,
+ {:ok, magic} <- GenMagic.Pool.perform(Nola.GenMagic, {:bytes, smol_body}),
+ bucket = Application.get_env(:nola, :s3, []) |> Keyword.get(:bucket),
+ ext = Path.extname(file["file_path"]),
+ s3path = "#{account.id}/#{file_unique_id}#{ext}",
+ Telegram.Api.request(t, "editMessageText",
+ chat_id: chat_id,
+ message_id: m_id,
+ text: "*Uploading...*",
+ reply_markup: %{},
+ parse_mode: "MarkdownV2"
+ ),
+ s3req =
+ ExAws.S3.put_object(bucket, s3path, body,
+ acl: :public_read,
+ content_type: magic.mime_type
+ ),
+ {:ok, _} <- ExAws.request(s3req) do
path = NolaWeb.Router.Helpers.url(NolaWeb.Endpoint) <> "/files/#{s3path}"
- sent = for {net, chan} <- target do
- txt = "sent#{type}#{text} #{path}"
- Nola.Irc.send_message_as(account, net, chan, txt)
- "#{net}/#{chan}"
- end
+
+ sent =
+ for {net, chan} <- target do
+ txt = "sent#{type}#{text} #{path}"
+ Nola.Irc.send_message_as(account, net, chan, txt)
+ "#{net}/#{chan}"
+ end
+
if caption = op["caption"], do: as_irc_message(chat_id, caption, account)
text = "Sent on " <> Enum.join(sent, ", ") <> " !"
- Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "_Sent!_", reply_markup: %{}, parse_mode: "MarkdownV2")
+
+ Telegram.Api.request(t, "editMessageText",
+ chat_id: chat_id,
+ message_id: m_id,
+ text: "_Sent!_",
+ reply_markup: %{},
+ parse_mode: "MarkdownV2"
+ )
else
error ->
- Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Something failed.", reply_markup: %{}, parse_mode: "MarkdownV2")
- Logger.error("Failed upload from Telegram: #{inspect error}")
+ Telegram.Api.request(t, "editMessageText",
+ chat_id: chat_id,
+ message_id: m_id,
+ text: "Something failed.",
+ reply_markup: %{},
+ parse_mode: "MarkdownV2"
+ )
+
+ Logger.error("Failed upload from Telegram: #{inspect(error)}")
end
end)
end
+
{:ok, state}
end
- def handle_update(%{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}, "text" => text}}, _, state) do
+ def handle_update(
+ %{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}, "text" => text}},
+ _,
+ state
+ ) do
account = Nola.Account.find_meta_account("telegram-id", id)
+
if account do
as_irc_message(id, text, account)
end
+
{:ok, state}
end
def handle_update(m, _, state) do
- Logger.debug("Unhandled update: #{inspect m}")
+ Logger.debug("Unhandled update: #{inspect(m)}")
{:ok, state}
end
@@ -188,49 +268,80 @@ defmodule Nola.Telegram do
end
defp as_irc_message(id, text, account) do
- reply_fun = fn(text) -> send_message(id, text) end
- trigger_text = cond do
- String.starts_with?(text, "/") ->
- "/"<>text = text
- "!"<>text
- Enum.any?(Nola.Irc.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(text, trigger) end) ->
- text
- true ->
- "!"<>text
- end
- message = %Nola.Message{
- id: FlakeId.get(),
- transport: :telegram,
+ reply_fun = fn text -> send_message(id, text) end
+
+ trigger_text =
+ cond do
+ String.starts_with?(text, "/") ->
+ "/" <> text = text
+ "!" <> text
+
+ Enum.any?(Nola.Irc.Connection.triggers(), fn {trigger, _} ->
+ String.starts_with?(text, trigger)
+ end) ->
+ text
+
+ true ->
+ "!" <> text
+ end
+
+ message = %Nola.Message{
+ id: FlakeId.get(),
+ transport: :telegram,
network: "telegram",
channel: nil,
text: text,
account: account,
sender: %ExIRC.SenderInfo{nick: account.name},
replyfun: reply_fun,
- trigger: Nola.Irc.Connection.extract_trigger(trigger_text),
- at: nil
+ trigger: Nola.Irc.Connection.extract_trigger(trigger_text),
+ at: nil
}
- Nola.Irc.Connection.publish(message, ["messages:private", "messages:telegram", "telegram/#{account.id}:messages"])
+
+ Nola.Irc.Connection.publish(message, [
+ "messages:private",
+ "messages:telegram",
+ "telegram/#{account.id}:messages"
+ ])
+
message
end
- defp start_upload(_type, %{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}}}, token, state) do
+ defp start_upload(
+ _type,
+ %{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}}},
+ token,
+ state
+ ) do
account = Nola.Account.find_meta_account("telegram-id", id)
+
if account do
text = if(m["text"], do: m["text"], else: nil)
- targets = Nola.Membership.of_account(account)
- |> Enum.map(fn({net, chan}) -> "#{net}/#{chan}" end)
- |> Enum.map(fn(i) -> %{"text" => i, "callback_data" => "start-upload:#{i}"} end)
- kb = if Enum.count(targets) > 1 do
- [%{"text" => "everywhere", "callback_data" => "start-upload:everywhere"}] ++ targets
- else
- targets
- end
- |> Enum.chunk_every(2)
+
+ targets =
+ Nola.Membership.of_account(account)
+ |> Enum.map(fn {net, chan} -> "#{net}/#{chan}" end)
+ |> Enum.map(fn i -> %{"text" => i, "callback_data" => "start-upload:#{i}"} end)
+
+ kb =
+ if Enum.count(targets) > 1 do
+ [%{"text" => "everywhere", "callback_data" => "start-upload:everywhere"}] ++ targets
+ else
+ targets
+ end
+ |> Enum.chunk_every(2)
+
keyboard = %{"inline_keyboard" => kb}
- Telegram.Api.request(token, "sendMessage", chat_id: id, text: "Where should I send this file?", reply_markup: keyboard, reply_to_message_id: m["message_id"], parse_mode: "MarkdownV2")
+
+ Telegram.Api.request(token, "sendMessage",
+ chat_id: id,
+ text: "Where should I send this file?",
+ reply_markup: keyboard,
+ reply_to_message_id: m["message_id"],
+ parse_mode: "MarkdownV2"
+ )
end
+
{:ok, state}
end
-
end
diff --git a/lib/telegram/room.ex b/lib/telegram/room.ex
index 9db551b..05c3eeb 100644
--- a/lib/telegram/room.ex
+++ b/lib/telegram/room.ex
@@ -7,7 +7,7 @@ defmodule Nola.TelegramRoom do
def rooms(), do: rooms(:with_docs)
- @spec rooms(:with_docs | :ids) :: [Map.t | integer( )]
+ @spec rooms(:with_docs | :ids) :: [Map.t() | integer()]
def rooms(:with_docs) do
case Couch.get(@couch, :all_docs, include_docs: true) do
{:ok, %{"rows" => rows}} -> {:ok, for(%{"doc" => doc} <- rows, do: doc)}
@@ -33,9 +33,13 @@ defmodule Nola.TelegramRoom do
def after_start() do
{:ok, rooms} = rooms(:ids)
+
for id <- rooms do
- spawn(fn() ->
- Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(Nola.Telegram, Integer.parse(id) |> elem(0))
+ spawn(fn ->
+ Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(
+ Nola.Telegram,
+ Integer.parse(id) |> elem(0)
+ )
end)
end
end
@@ -45,34 +49,50 @@ defmodule Nola.TelegramRoom do
token = Keyword.get(Application.get_env(:nola, :telegram, []), :key)
{:ok, chat} = Api.request(token, "getChat", chat_id: id)
Logger.metadata(transport: :telegram, id: id, telegram_room_id: id)
- tg_room = case room(to_string(id)) do
- {:ok, tg_room = %{"network" => _net, "channel" => _chan}} -> tg_room
- _ ->
- [net, chan] = String.split(chat["title"], "/", parts: 2)
- {net, chan} = case Nola.Irc.Connection.get_network(net, chan) do
- %Nola.Irc.Connection{} -> {net, chan}
- _ -> {nil, nil}
- end
- {:ok, _, _} = Couch.post(@couch, %{"_id" => id, "network" => net, "channel" => chan})
- {:ok, tg_room} = room(to_string(id))
- tg_room
- end
+
+ tg_room =
+ case room(to_string(id)) do
+ {:ok, tg_room = %{"network" => _net, "channel" => _chan}} ->
+ tg_room
+
+ _ ->
+ [net, chan] = String.split(chat["title"], "/", parts: 2)
+
+ {net, chan} =
+ case Nola.Irc.Connection.get_network(net, chan) do
+ %Nola.Irc.Connection{} -> {net, chan}
+ _ -> {nil, nil}
+ end
+
+ {:ok, _, _} = Couch.post(@couch, %{"_id" => id, "network" => net, "channel" => chan})
+ {:ok, tg_room} = room(to_string(id))
+ tg_room
+ end
+
%{"network" => net, "channel" => chan} = tg_room
- Logger.info("Starting ChatBot for room #{id} \"#{chat["title"]}\" #{inspect tg_room}")
- irc_plumbed = if net && chan do
+ Logger.info("Starting ChatBot for room #{id} \"#{chat["title"]}\" #{inspect(tg_room)}")
+
+ irc_plumbed =
+ if net && chan do
{:ok, _} = Registry.register(Nola.PubSub, "#{net}/#{chan}:messages", plugin: __MODULE__)
{:ok, _} = Registry.register(Nola.PubSub, "#{net}/#{chan}:triggers", plugin: __MODULE__)
{:ok, _} = Registry.register(Nola.PubSub, "#{net}/#{chan}:outputs", plugin: __MODULE__)
true
- else
- Logger.warn("Did not found telegram match for #{id} \"#{chat["title"]}\"")
- false
- end
+ else
+ Logger.warn("Did not found telegram match for #{id} \"#{chat["title"]}\"")
+ false
+ end
+
{:ok, %{id: id, net: net, chan: chan, irc: irc_plumbed}}
end
def init(id) do
- Logger.error("telegram_room: bad id (not room id)", transport: :telegram, id: id, telegram_room_id: id)
+ Logger.error("telegram_room: bad id (not room id)",
+ transport: :telegram,
+ id: id,
+ telegram_room_id: id
+ )
+
:ignore
end
@@ -82,15 +102,18 @@ defmodule Nola.TelegramRoom do
else
first_name = Map.get(from, "first_name")
last_name = Map.get(from, "last_name")
- name = [first_name, last_name]
- |> Enum.filter(& &1)
- |> Enum.join(" ")
+
+ name =
+ [first_name, last_name]
+ |> Enum.filter(& &1)
+ |> Enum.join(" ")
username = Map.get(from, "username", first_name)
- account = username
- |> Nola.Account.new_account()
- |> Nola.Account.update_account_name(name)
+ account =
+ username
+ |> Nola.Account.new_account()
+ |> Nola.Account.update_account_name(name)
Nola.Account.put_meta(account, "telegram-id", user_id)
@@ -99,17 +122,33 @@ defmodule Nola.TelegramRoom do
end
end
- def handle_update(%{"message" => %{"from" => from = %{"id" => user_id}, "text" => text}}, _token, state) do
+ def handle_update(
+ %{"message" => %{"from" => from = %{"id" => user_id}, "text" => text}},
+ _token,
+ state
+ ) do
account = find_or_create_meta_account(from, state)
- #connection = Nola.Irc.Connection.get_network(state.net)
+ # connection = Nola.Irc.Connection.get_network(state.net)
Nola.Irc.send_message_as(account, state.net, state.chan, text, true, origin: __MODULE__)
{:ok, state}
end
- def handle_update(data = %{"message" => %{"from" => from = %{"id" => user_id}, "location" => %{"latitude" => lat, "longitude" => lon}}}, _token, state) do
+ def handle_update(
+ data = %{
+ "message" => %{
+ "from" => from = %{"id" => user_id},
+ "location" => %{"latitude" => lat, "longitude" => lon}
+ }
+ },
+ _token,
+ state
+ ) do
account = find_or_create_meta_account(from, state)
- #connection = Nola.Irc.Connection.get_network(state.net)
- Nola.Irc.send_message_as(account, state.net, state.chan, "@ #{lat}, #{lon}", true, origin: __MODULE__)
+ # connection = Nola.Irc.Connection.get_network(state.net)
+ Nola.Irc.send_message_as(account, state.net, state.chan, "@ #{lat}, #{lon}", true,
+ origin: __MODULE__
+ )
+
{:ok, state}
end
@@ -133,62 +172,76 @@ defmodule Nola.TelegramRoom do
body = if Map.get(message.meta, :self), do: text, else: "<#{nick}> #{text}"
Nola.Telegram.send_message(state.id, body)
end
+
{:ok, state}
end
def handle_info(info, state) do
- Logger.info("UNhandled #{inspect info}")
+ Logger.info("UNhandled #{inspect(info)}")
{:ok, state}
end
- defp upload(_type, %{"message" => m = %{"chat" => %{"id" => chat_id}, "from" => from = %{"id" => user_id}}}, token, state) do
+ defp upload(
+ _type,
+ %{"message" => m = %{"chat" => %{"id" => chat_id}, "from" => from = %{"id" => user_id}}},
+ token,
+ state
+ ) do
account = find_or_create_meta_account(from, state)
+
if account do
- {content, type} = cond do
- m["photo"] -> {m["photo"], "photo"}
- m["voice"] -> {m["voice"], "voice message"}
- m["video"] -> {m["video"], "video"}
- m["document"] -> {m["document"], "file"}
- m["animation"] -> {m["animation"], "gif"}
- end
+ {content, type} =
+ cond do
+ m["photo"] -> {m["photo"], "photo"}
+ m["voice"] -> {m["voice"], "voice message"}
+ m["video"] -> {m["video"], "video"}
+ m["document"] -> {m["document"], "file"}
+ m["animation"] -> {m["animation"], "gif"}
+ end
- file = if is_list(content) && Enum.count(content) > 1 do
- Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2)
- |> List.first()
- else
- content
- end
+ file =
+ if is_list(content) && Enum.count(content) > 1 do
+ Enum.sort_by(content, fn p -> p["file_size"] end, &>=/2)
+ |> List.first()
+ else
+ content
+ end
file_id = file["file_id"]
file_unique_id = file["file_unique_id"]
text = if(m["caption"], do: m["caption"] <> " ", else: "")
- spawn(fn() ->
- with \
- {:ok, file} <- Telegram.Api.request(token, "getFile", file_id: file_id),
- path = "https://api.telegram.org/file/bot#{token}/#{file["file_path"]}",
- {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path),
- <<smol_body::binary-size(20), _::binary>> = body,
- {:ok, magic} <- GenMagic.Pool.perform(Nola.GenMagic, {:bytes, smol_body}),
- bucket = Application.get_env(:nola, :s3, []) |> Keyword.get(:bucket),
- ext = Path.extname(file["file_path"]),
- s3path = "#{account.id}/#{file_unique_id}#{ext}",
- s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type),
- {:ok, _} <- ExAws.request(s3req)
- do
+ spawn(fn ->
+ with {:ok, file} <- Telegram.Api.request(token, "getFile", file_id: file_id),
+ path = "https://api.telegram.org/file/bot#{token}/#{file["file_path"]}",
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path),
+ <<smol_body::binary-size(20), _::binary>> = body,
+ {:ok, magic} <- GenMagic.Pool.perform(Nola.GenMagic, {:bytes, smol_body}),
+ bucket = Application.get_env(:nola, :s3, []) |> Keyword.get(:bucket),
+ ext = Path.extname(file["file_path"]),
+ s3path = "#{account.id}/#{file_unique_id}#{ext}",
+ s3req =
+ ExAws.S3.put_object(bucket, s3path, body,
+ acl: :public_read,
+ content_type: magic.mime_type
+ ),
+ {:ok, _} <- ExAws.request(s3req) do
path = NolaWeb.Router.Helpers.url(NolaWeb.Endpoint) <> "/files/#{s3path}"
txt = "#{type}: #{text}#{path}"
- #connection = Nola.Irc.Connection.get_network(state.net)
+ # connection = Nola.Irc.Connection.get_network(state.net)
Nola.Irc.send_message_as(account, state.net, state.chan, txt, true, origin: __MODULE__)
else
error ->
- Telegram.Api.request(token, "sendMessage", chat_id: chat_id, text: "File upload failed, sorry.")
- Logger.error("Failed upload from Telegram: #{inspect error}")
+ Telegram.Api.request(token, "sendMessage",
+ chat_id: chat_id,
+ text: "File upload failed, sorry."
+ )
+
+ Logger.error("Failed upload from Telegram: #{inspect(error)}")
end
end)
{:ok, state}
end
end
-
end
diff --git a/lib/tmpl.ex b/lib/tmpl.ex
index e4489ac..881cbc8 100644
--- a/lib/tmpl.ex
+++ b/lib/tmpl.ex
@@ -24,10 +24,26 @@ defmodule Tmpl do
end
end
- @colors [:white, :black, :blue, :green, :red, :brown, :purple, :orange, :yellow, :light_green, :cyan, :light_blue, :pink, :grey, :light_grey]
+ @colors [
+ :white,
+ :black,
+ :blue,
+ :green,
+ :red,
+ :brown,
+ :purple,
+ :orange,
+ :yellow,
+ :light_green,
+ :cyan,
+ :light_blue,
+ :pink,
+ :grey,
+ :light_grey
+ ]
for {color, index} <- Enum.with_index(@colors) do
- code = 48+index
+ code = 48 + index
def color_code(unquote(color)) do
unquote(code)
@@ -42,7 +58,9 @@ defmodule Tmpl do
end
end
- def account_nick(%{"id" => id, "name" => name}, %{variables: %{"message" => %{"network" => network}}}) do
+ def account_nick(%{"id" => id, "name" => name}, %{
+ variables: %{"message" => %{"network" => network}}
+ }) do
if user = Nola.UserTrack.find_by_account(network, %Nola.Account{id: id}) do
user.nick
else
@@ -53,7 +71,6 @@ defmodule Tmpl do
def account_nick(val, ctx) do
"{{account_nick}}"
end
-
end
def render(template, msg = %Nola.Message{}, context \\ %{}, safe \\ true) do
@@ -64,20 +81,23 @@ defmodule Tmpl do
case Liquex.parse(template) do
{:ok, template_ast} ->
do_render(template_ast, context, safe)
+
{:error, err, pos} ->
- Logger.debug("Liquid error: #{pos} - #{inspect template}")
- "[liquid ast error (at #{pos}): #{inspect err}]"
+ Logger.debug("Liquid error: #{pos} - #{inspect(template)}")
+ "[liquid ast error (at #{pos}): #{inspect(err)}]"
end
end
defp do_render(template_ast, context, safe) when is_list(template_ast) do
- context = Liquex.Context.new(mapify(context, safe))
- |> Map.put(:filter_module, Tmpl.Filter)
+ context =
+ Liquex.Context.new(mapify(context, safe))
+ |> Map.put(:filter_module, Tmpl.Filter)
+
{content, _context} = Liquex.render(template_ast, context)
to_string(content)
rescue
e ->
- Logger.error("Liquid error: #{inspect e}")
+ Logger.error("Liquid error: #{inspect(e)}")
"[liquid rendering error]"
end
@@ -87,8 +107,9 @@ defmodule Tmpl do
defp mapify(map = %{}, safe) do
map
- |> Enum.reduce(Map.new, fn({k,v}, acc) ->
+ |> Enum.reduce(Map.new(), fn {k, v}, acc ->
k = to_string(k)
+
if safe?(k, safe) do
if v = mapify(v, safe) do
Map.put(acc, k, v)
@@ -121,4 +142,3 @@ defmodule Tmpl do
defp safe?("password", true), do: false
defp safe?(_, true), do: true
end
-
diff --git a/lib/untappd.ex b/lib/untappd.ex
index e603c25..c563fa9 100644
--- a/lib/untappd.ex
+++ b/lib/untappd.ex
@@ -1,12 +1,12 @@
defmodule Untappd do
-
- @env Mix.env
- @version Mix.Project.config[:version]
+ @env Mix.env()
+ @version Mix.Project.config()[:version]
require Logger
def auth_url() do
client_id = Keyword.get(env(), :client_id)
url = NolaWeb.Router.Helpers.untappd_callback_url(NolaWeb.Endpoint, :callback)
+
"https://untappd.com/oauth/authenticate/?client_id=#{client_id}&response_type=code&redirect_url=#{URI.encode(url)}"
end
@@ -14,6 +14,7 @@ defmodule Untappd do
client_id = Keyword.get(env(), :client_id)
client_secret = Keyword.get(env(), :client_secret)
url = NolaWeb.Router.Helpers.untappd_callback_url(NolaWeb.Endpoint, :callback)
+
params = %{
"client_id" => client_id,
"client_secret" => client_secret,
@@ -21,12 +22,14 @@ defmodule Untappd do
"redirect_url" => url,
"code" => code
}
+
case HTTPoison.get("https://untappd.com/oauth/authorize", headers(), params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
json = Poison.decode!(body)
{:ok, get_in(json, ["response", "access_token"])}
+
error ->
- Logger.error("Untappd auth callback failed: #{inspect error}")
+ Logger.error("Untappd auth callback failed: #{inspect(error)}")
:error
end
end
@@ -40,38 +43,56 @@ defmodule Untappd do
end
def checkin(token, beer_id) do
- params = get_params(token: token)
- |> Map.put("timezone", "CEST")
- |> Map.put("bid", beer_id)
- form_params = params
- |> Enum.into([])
- case HTTPoison.post("https://api.untappd.com/v4/checkin/add", {:form, form_params}, headers(), params: params) do
+ params =
+ get_params(token: token)
+ |> Map.put("timezone", "CEST")
+ |> Map.put("bid", beer_id)
+
+ form_params =
+ params
+ |> Enum.into([])
+
+ case HTTPoison.post("https://api.untappd.com/v4/checkin/add", {:form, form_params}, headers(),
+ params: params
+ ) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
- body = Jason.decode!(body)
- |> Map.get("response")
+ body =
+ Jason.decode!(body)
+ |> Map.get("response")
+
{:ok, body}
+
{:ok, resp = %HTTPoison.Response{status_code: code, body: body}} ->
- Logger.warn "Untappd checkin error: #{inspect resp}"
+ Logger.warn("Untappd checkin error: #{inspect(resp)}")
{:error, {:http_error, code}}
- {:error, error} -> {:error, {:http_error, error}}
+
+ {:error, error} ->
+ {:error, {:http_error, error}}
end
end
def search_beer(query, params \\ []) do
- params = get_params(params)
- |> Map.put("q", query)
- |> Map.put("limit", 10)
- #|> Map.put("sort", "name")
+ params =
+ get_params(params)
+ |> Map.put("q", query)
+ |> Map.put("limit", 10)
+
+ # |> Map.put("sort", "name")
case HTTPoison.get("https://api.untappd.com/v4/search/beer", headers(), params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, Jason.decode!(body)}
+
error ->
- Logger.error("Untappd search error: #{inspect error}")
+ Logger.error("Untappd search error: #{inspect(error)}")
end
end
def get_params(params) do
- auth = %{"client_id" => Keyword.get(env(), :client_id), "client_secret" => Keyword.get(env(), :client_secret)}
+ auth = %{
+ "client_id" => Keyword.get(env(), :client_id),
+ "client_secret" => Keyword.get(env(), :client_secret)
+ }
+
if token = Keyword.get(params, :token) do
Map.put(auth, "access_token", token)
else
@@ -81,14 +102,14 @@ defmodule Untappd do
def headers(extra \\ []) do
client_id = Keyword.get(env(), :client_id)
- extra
- ++ [
- {"user-agent", "dmzbot (#{client_id}; #{@version}-#{@env})"}
- ]
+
+ extra ++
+ [
+ {"user-agent", "dmzbot (#{client_id}; #{@version}-#{@env})"}
+ ]
end
def env() do
Application.get_env(:nola, :untappd)
end
-
end
diff --git a/lib/util.ex b/lib/util.ex
index 8bd3b9d..dea7834 100644
--- a/lib/util.ex
+++ b/lib/util.ex
@@ -1,7 +1,17 @@
defmodule Util do
+ defmodule Map do
+ def put_if_not_null(map, _key, nil) do
+ map
+ end
+
+ def put_if_not_null(map, key, value) do
+ Elixir.Map.put(map, key, value)
+ end
+ end
def to_naive_date_time(naive = %NaiveDateTime{}), do: naive
def to_naive_date_time(datetime = %DateTime{}), do: DateTime.to_naive(datetime)
+
def to_naive_date_time(timestamp) when is_integer(timestamp) do
timestamp
|> to_date_time()
@@ -30,7 +40,8 @@ defmodule Util do
def plusminus(number) when number < 0, do: "#{number}"
def float_paparse(float) when is_float(float), do: {float, ""}
- def float_paparse(int) when is_integer(int), do: {(int+0.0), ""}
+ def float_paparse(int) when is_integer(int), do: {int + 0.0, ""}
+
def float_paparse(string) when is_binary(string) do
string
|> String.replace(",", ".")
@@ -45,7 +56,7 @@ defmodule Util do
ets.safe_fixtable(table, false)
end
- defp do_ets_mutate_select_each(_, _, _, :'$end_of_table') do
+ defp do_ets_mutate_select_each(_, _, _, :"$end_of_table") do
:ok
end
@@ -54,7 +65,6 @@ defmodule Util do
do_ets_mutate_select_each(ets, table, fun, ets.select(continuation))
end
-
def ets_mutate_each(ets, table, fun) do
ets.safe_fixtable(table, true)
first = ets.first(table)
@@ -68,7 +78,20 @@ defmodule Util do
[elem] -> fun.(table, elem)
_ -> nil
end
+
do_ets_mutate_each(ets, table, fun, ets.next(table, key))
end
+ def read_file_list!(path) do
+ path
+ |> File.read!()
+ |> String.split("\n")
+ |> Enum.map(fn line ->
+ if !String.starts_with?(line, "#") do
+ String.trim(line)
+ end
+ end)
+ |> Enum.filter(& &1)
+ |> Enum.filter(&(&1 != ""))
+ end
end
diff --git a/lib/web.ex b/lib/web.ex
index c1720a0..e8cb26d 100644
--- a/lib/web.ex
+++ b/lib/web.ex
@@ -25,22 +25,24 @@ defmodule NolaWeb do
"♯"
end
- def format_chan("#"<>chan) do
+ def format_chan("#" <> chan) do
chan
end
- def format_chan(chan = "!"<>_), do: chan
+ def format_chan(chan = "!" <> _), do: chan
def reformat_chan("♯") do
"#"
end
+
def reformat_chan("♯♯") do
"##"
end
- def reformat_chan(chan = "!"<>_), do: chan
+
+ def reformat_chan(chan = "!" <> _), do: chan
def reformat_chan(chan) do
- "#"<>chan
+ "#" <> chan
end
def controller do
@@ -55,8 +57,9 @@ defmodule NolaWeb do
def view do
quote do
- use Phoenix.View, root: "lib/web/templates",
- namespace: NolaWeb
+ use Phoenix.View,
+ root: "lib/web/templates",
+ namespace: NolaWeb
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
diff --git a/lib/web/channels/user_socket.ex b/lib/web/channels/user_socket.ex
index eadd4e0..a910ffe 100644
--- a/lib/web/channels/user_socket.ex
+++ b/lib/web/channels/user_socket.ex
@@ -5,7 +5,7 @@ defmodule NolaWeb.UserSocket do
# channel "room:*", NolaWeb.RoomChannel
## Transports
- #transport :websocket, Phoenix.Transports.WebSocket
+ # transport :websocket, Phoenix.Transports.WebSocket
# transport :longpoll, Phoenix.Transports.LongPoll
# Socket params are passed from the client and can
diff --git a/lib/web/components/component.ex b/lib/web/components/component.ex
index fff8263..5894536 100644
--- a/lib/web/components/component.ex
+++ b/lib/web/components/component.ex
@@ -9,6 +9,7 @@ defmodule NolaWeb.Component do
def naive_date_time_utc(assigns = %{format: format}) do
assigns = assign(assigns, :format, Map.get(@date_time_formats, format, format))
+
~H"""
<time class="component"
id={"time-#{:erlang.phash2(@datetime)}"}
@@ -19,9 +20,11 @@ defmodule NolaWeb.Component do
</time>
"""
end
+
def naive_date_time_utc(assigns) do
naive_date_time_utc(assign(assigns, :format, "%F %H:%M"))
end
+
def get_luxon_format("%H:%M:%S"), do: "TIME_24_WITH_SECONDS"
def nick(assigns = %{self: false}) do
@@ -39,6 +42,4 @@ defmodule NolaWeb.Component do
</span>
"""
end
-
-
end
diff --git a/lib/web/components/event_component.ex b/lib/web/components/event_component.ex
index 8af3c67..32af856 100644
--- a/lib/web/components/event_component.ex
+++ b/lib/web/components/event_component.ex
@@ -38,6 +38,4 @@ defmodule NolaWeb.EventComponent do
joined
"""
end
-
-
end
diff --git a/lib/web/components/message_component.ex b/lib/web/components/message_component.ex
index 5d0386b..7f9bac7 100644
--- a/lib/web/components/message_component.ex
+++ b/lib/web/components/message_component.ex
@@ -8,5 +8,4 @@ defmodule NolaWeb.MessageComponent do
<div class="inline-block flex-grow cursor-default"><%= @text %></div>
"""
end
-
end
diff --git a/lib/web/context_plug.ex b/lib/web/context_plug.ex
index 0a16340..aca0431 100644
--- a/lib/web/context_plug.ex
+++ b/lib/web/context_plug.ex
@@ -8,21 +8,27 @@ defmodule NolaWeb.ContextPlug do
def get_account(conn) do
cond do
- get_session(conn, :account) -> get_session(conn, :account)
- get_session(conn, :oidc_id) -> if account = Nola.Account.find_meta_account("identity-id", get_session(conn, :oidc_id)), do: account.id
- true -> nil
+ get_session(conn, :account) ->
+ get_session(conn, :account)
+
+ get_session(conn, :oidc_id) ->
+ if account = Nola.Account.find_meta_account("identity-id", get_session(conn, :oidc_id)),
+ do: account.id
+
+ true ->
+ nil
end
end
def call(conn, opts) do
- account = with \
- {:account, account_id} when is_binary(account_id) <- {:account, get_account(conn)},
- {:account, account} when not is_nil(account) <- {:account, Nola.Account.get(account_id)}
- do
- account
- else
- _ -> nil
- end
+ account =
+ with {:account, account_id} when is_binary(account_id) <- {:account, get_account(conn)},
+ {:account, account} when not is_nil(account) <-
+ {:account, Nola.Account.get(account_id)} do
+ account
+ else
+ _ -> nil
+ end
network = Map.get(conn.params, "network")
network = if network == "-", do: nil, else: network
@@ -30,32 +36,46 @@ defmodule NolaWeb.ContextPlug do
oidc_account = Nola.Account.find_meta_account("identity-id", get_session(conn, :oidc_id))
conns = Nola.Irc.Connection.get_network(network)
- chan = if c = Map.get(conn.params, "chan") do
- NolaWeb.reformat_chan(c)
- end
+
+ chan =
+ if c = Map.get(conn.params, "chan") do
+ NolaWeb.reformat_chan(c)
+ end
+
chan_conn = Nola.Irc.Connection.get_network(network, chan)
- memberships = if account do
- Nola.Membership.of_account(account)
- end
+ memberships =
+ if account do
+ Nola.Membership.of_account(account)
+ end
- auth_required = cond do
- Keyword.get(opts, :restrict) == :public -> false
- account == nil -> true
- network == nil -> false
- Keyword.get(opts, :restrict) == :logged_in -> false
- network && chan ->
- !Enum.member?(memberships, {network, chan})
- network ->
- !Enum.any?(memberships, fn({n, _}) -> n == network end)
- end
+ auth_required =
+ cond do
+ Keyword.get(opts, :restrict) == :public ->
+ false
- bot = cond do
- network && chan && chan_conn -> chan_conn.nick
- network && conns -> conns.nick
- true -> nil
- end
+ account == nil ->
+ true
+ network == nil ->
+ false
+
+ Keyword.get(opts, :restrict) == :logged_in ->
+ false
+
+ network && chan ->
+ !Enum.member?(memberships, {network, chan})
+
+ network ->
+ !Enum.any?(memberships, fn {n, _} -> n == network end)
+ end
+
+ bot =
+ cond do
+ network && chan && chan_conn -> chan_conn.nick
+ network && conns -> conns.nick
+ true -> nil
+ end
cond do
account && auth_required ->
@@ -63,30 +83,34 @@ defmodule NolaWeb.ContextPlug do
|> put_status(404)
|> text("Page not found")
|> halt()
+
auth_required ->
conn
|> put_status(403)
|> render(NolaWeb.AlcoologView, "auth.html", bot: bot, no_header: true, network: network)
|> halt()
- (network && !conns) ->
+
+ network && !conns ->
conn
|> put_status(404)
|> text("Page not found")
|> halt()
- (chan && !chan_conn) ->
+
+ chan && !chan_conn ->
conn
|> put_status(404)
|> text("Page not found")
|> halt()
+
true ->
- conn = conn
- |> assign(:network, network)
- |> assign(:chan, chan)
- |> assign(:bot, bot)
- |> assign(:account, account)
- |> assign(:oidc_account, oidc_account)
- |> assign(:memberships, memberships)
+ conn =
+ conn
+ |> assign(:network, network)
+ |> assign(:chan, chan)
+ |> assign(:bot, bot)
+ |> assign(:account, account)
+ |> assign(:oidc_account, oidc_account)
+ |> assign(:memberships, memberships)
end
end
-
end
diff --git a/lib/web/controllers/alcoolog_controller.ex b/lib/web/controllers/alcoolog_controller.ex
index 8d7fc11..57fc16a 100644
--- a/lib/web/controllers/alcoolog_controller.ex
+++ b/lib/web/controllers/alcoolog_controller.ex
@@ -2,43 +2,63 @@ defmodule NolaWeb.AlcoologController do
use NolaWeb, :controller
require Logger
- plug NolaWeb.ContextPlug when action not in [:token]
- plug NolaWeb.ContextPlug, [restrict: :public] when action in [:token]
+ plug(NolaWeb.ContextPlug when action not in [:token])
+ plug(NolaWeb.ContextPlug, [restrict: :public] when action in [:token])
def token(conn, %{"token" => token}) do
case Nola.Token.lookup(token) do
- {:ok, {:alcoolog, :index, network, channel}} -> index(conn, nil, network, channel)
+ {:ok, {:alcoolog, :index, network, channel}} ->
+ index(conn, nil, network, channel)
+
err ->
- Logger.debug("AlcoologControler: token #{inspect err} invalid")
+ Logger.debug("AlcoologControler: token #{inspect(err)} invalid")
+
conn
|> put_status(404)
|> text("Page not found")
end
end
- def nick(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
+ def nick(
+ conn = %{assigns: %{account: account}},
+ params = %{"network" => network, "nick" => nick}
+ ) do
profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
days = String.to_integer(Map.get(params, "days", "180"))
friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
+
if friend? do
stats = Nola.Plugins.Alcoolog.get_full_statistics(profile_account.id)
- history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- Nola.Plugins.Alcoolog.nick_history(profile_account) do
- %{
- at: ts |> DateTime.from_unix!(:millisecond),
- points: points,
- active: active,
- cl: cl,
- deg: deg,
- type: type,
- description: descr,
- meta: meta
- }
- end
- history = Enum.sort(history, &(DateTime.compare(&1.at, &2.at) != :lt))
- |> IO.inspect()
+
+ history =
+ for {{nick, ts}, points, active, cl, deg, type, descr, meta} <-
+ Nola.Plugins.Alcoolog.nick_history(profile_account) do
+ %{
+ at: ts |> DateTime.from_unix!(:millisecond),
+ points: points,
+ active: active,
+ cl: cl,
+ deg: deg,
+ type: type,
+ description: descr,
+ meta: meta
+ }
+ end
+
+ history =
+ Enum.sort(history, &(DateTime.compare(&1.at, &2.at) != :lt))
+ |> IO.inspect()
+
conn
|> assign(:title, "alcoolog #{nick}")
- |> render("user.html", network: network, profile: profile_account, days: days, nick: nick, history: history, stats: stats)
+ |> render("user.html",
+ network: network,
+ profile: profile_account,
+ days: days,
+ nick: nick,
+ history: history,
+ stats: stats
+ )
else
conn
|> put_status(404)
@@ -46,9 +66,13 @@ defmodule NolaWeb.AlcoologController do
end
end
- def nick_stats_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
+ def nick_stats_json(
+ conn = %{assigns: %{account: account}},
+ params = %{"network" => network, "nick" => nick}
+ ) do
profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
+
if friend? do
stats = Nola.Plugins.Alcoolog.get_full_statistics(profile_account.id)
@@ -62,27 +86,39 @@ defmodule NolaWeb.AlcoologController do
end
end
- def nick_gls_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
+ def nick_gls_json(
+ conn = %{assigns: %{account: account}},
+ params = %{"network" => network, "nick" => nick}
+ ) do
profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
count = String.to_integer(Map.get(params, "days", "180"))
+
if friend? do
data = Nola.Plugins.Alcoolog.user_over_time_gl(profile_account, count)
- delay = count*((24 * 60)*60)
- now = DateTime.utc_now()
- start_date = DateTime.utc_now()
- |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
- |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
- |> DateTime.to_date()
- |> Date.to_erl()
- filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
- |> Enum.to_list
- |> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
- |> Enum.map(&Date.from_erl!(&1))
- |> Enum.map(fn(date) ->
- %{date: date, gls: Map.get(data, date, 0)}
- end)
- |> Enum.sort(&(Date.compare(&1.date, &2.date) != :gt))
+ delay = count * (24 * 60 * 60)
+ now = DateTime.utc_now()
+
+ start_date =
+ DateTime.utc_now()
+ |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
+ |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
+ |> DateTime.to_date()
+ |> Date.to_erl()
+
+ filled =
+ :calendar.date_to_gregorian_days(start_date)..:calendar.date_to_gregorian_days(
+ DateTime.utc_now()
+ |> DateTime.to_date()
+ |> Date.to_erl()
+ )
+ |> Enum.to_list()
+ |> Enum.map(&:calendar.gregorian_days_to_date(&1))
+ |> Enum.map(&Date.from_erl!(&1))
+ |> Enum.map(fn date ->
+ %{date: date, gls: Map.get(data, date, 0)}
+ end)
+ |> Enum.sort(&(Date.compare(&1.date, &2.date) != :gt))
conn
|> put_resp_content_type("application/json")
@@ -94,29 +130,39 @@ defmodule NolaWeb.AlcoologController do
end
end
-
-
- def nick_volumes_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
+ def nick_volumes_json(
+ conn = %{assigns: %{account: account}},
+ params = %{"network" => network, "nick" => nick}
+ ) do
profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
count = String.to_integer(Map.get(params, "days", "180"))
+
if friend? do
data = Nola.Plugins.Alcoolog.user_over_time(profile_account, count)
- delay = count*((24 * 60)*60)
- now = DateTime.utc_now()
- start_date = DateTime.utc_now()
- |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
- |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
- |> DateTime.to_date()
- |> Date.to_erl()
- filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
- |> Enum.to_list
- |> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
- |> Enum.map(&Date.from_erl!(&1))
- |> Enum.map(fn(date) ->
- %{date: date, volumes: Map.get(data, date, 0)}
- end)
- |> Enum.sort(&(Date.compare(&1.date, &2.date) != :gt))
+ delay = count * (24 * 60 * 60)
+ now = DateTime.utc_now()
+
+ start_date =
+ DateTime.utc_now()
+ |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
+ |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
+ |> DateTime.to_date()
+ |> Date.to_erl()
+
+ filled =
+ :calendar.date_to_gregorian_days(start_date)..:calendar.date_to_gregorian_days(
+ DateTime.utc_now()
+ |> DateTime.to_date()
+ |> Date.to_erl()
+ )
+ |> Enum.to_list()
+ |> Enum.map(&:calendar.gregorian_days_to_date(&1))
+ |> Enum.map(&Date.from_erl!(&1))
+ |> Enum.map(fn date ->
+ %{date: date, volumes: Map.get(data, date, 0)}
+ end)
+ |> Enum.sort(&(Date.compare(&1.date, &2.date) != :gt))
conn
|> put_resp_content_type("application/json")
@@ -128,22 +174,29 @@ defmodule NolaWeb.AlcoologController do
end
end
- def nick_log_json(conn = %{assigns: %{account: account}}, %{"network" => network, "nick" => nick}) do
+ def nick_log_json(conn = %{assigns: %{account: account}}, %{
+ "network" => network,
+ "nick" => nick
+ }) do
profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
+
if friend? do
- history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- Nola.Plugins.Alcoolog.nick_history(profile_account) do
- %{
- at: ts |> DateTime.from_unix!(:millisecond) |> DateTime.to_iso8601(),
- points: points,
- active: active,
- cl: cl,
- deg: deg,
- type: type,
- description: descr,
- meta: meta
- }
- end
+ history =
+ for {{nick, ts}, points, active, cl, deg, type, descr, meta} <-
+ Nola.Plugins.Alcoolog.nick_history(profile_account) do
+ %{
+ at: ts |> DateTime.from_unix!(:millisecond) |> DateTime.to_iso8601(),
+ points: points,
+ active: active,
+ cl: cl,
+ deg: deg,
+ type: type,
+ description: descr,
+ meta: meta
+ }
+ end
+
last = List.last(history)
{_, active} = Nola.Plugins.Alcoolog.user_stats(profile_account)
last = %{last | active: active, at: DateTime.utc_now() |> DateTime.to_iso8601()}
@@ -159,13 +212,19 @@ defmodule NolaWeb.AlcoologController do
end
end
- def nick_history_json(conn = %{assigns: %{account: account}}, %{"network" => network, "nick" => nick}) do
+ def nick_history_json(conn = %{assigns: %{account: account}}, %{
+ "network" => network,
+ "nick" => nick
+ }) do
profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
+
if friend? do
- history = for {_, date, value} <- Nola.Plugins.AlcoologAnnouncer.log(profile_account) do
- %{date: DateTime.to_iso8601(date), value: value}
- end
+ history =
+ for {_, date, value} <- Nola.Plugins.AlcoologAnnouncer.log(profile_account) do
+ %{date: DateTime.to_iso8601(date), value: value}
+ end
+
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(history))
@@ -184,7 +243,7 @@ defmodule NolaWeb.AlcoologController do
index(conn, account, nil, nil)
end
- #def index(conn, params) do
+ # def index(conn, params) do
# network = Map.get(params, "network")
# chan = if c = Map.get(params, "chan") do
# NolaWeb.reformat_chan(c)
@@ -197,117 +256,175 @@ defmodule NolaWeb.AlcoologController do
# conn
# |> put_status(403)
# |> render("auth.html", network: network, channel: chan, irc_conn: conn, bot: bot)
- #end
+ # end
def index(conn, account, network, channel) do
- aday = ((24 * 60)*60)
+ aday = 24 * 60 * 60
now = DateTime.utc_now()
- before7 = now
- |> DateTime.add(-(7*aday), :second)
- |> DateTime.to_unix(:millisecond)
- before15 = now
- |> DateTime.add(-(15*aday), :second)
- |> DateTime.to_unix(:millisecond)
- before31 = now
- |> DateTime.add(-(31*aday), :second)
- |> DateTime.to_unix(:millisecond)
- #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
+
+ before7 =
+ now
+ |> DateTime.add(-(7 * aday), :second)
+ |> DateTime.to_unix(:millisecond)
+
+ before15 =
+ now
+ |> DateTime.add(-(15 * aday), :second)
+ |> DateTime.to_unix(:millisecond)
+
+ before31 =
+ now
+ |> DateTime.add(-(31 * aday), :second)
+ |> DateTime.to_unix(:millisecond)
+
+ # match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
match = [
- {{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_},
- [
- {:>, :"$1", {:const, before15}},
- ], [:"$_"]}
+ {{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_},
+ [
+ {:>, :"$1", {:const, before15}}
+ ], [:"$_"]}
]
- # tuple ets: {{nick, date}, volumes, current, nom, commentaire}
+ # tuple ets: {{nick, date}, volumes, current, nom, commentaire}
members = Nola.Membership.expanded_members_or_friends(account, network, channel)
- members_ids = Enum.map(members, fn({account, _, nick}) -> account.id end)
- member_names = Enum.reduce(members, %{}, fn({account, _, nick}, acc) -> Map.put(acc, account.id, nick) end)
- drinks = :ets.select(Nola.Plugins.Alcoolog.ETS, match)
- |> Enum.filter(fn({{account, _}, _vol, _cur, _cl, _deg, _name, _cmt, _meta}) -> Enum.member?(members_ids, account) end)
- |> Enum.map(fn({{account, _}, _, _, _, _, _, _, _} = object) -> {object, Map.get(member_names, account)} end)
- |> Enum.sort_by(fn({{{_, ts}, _, _, _, _, _, _, _}, _}) -> ts end, &>/2)
+ members_ids = Enum.map(members, fn {account, _, nick} -> account.id end)
+
+ member_names =
+ Enum.reduce(members, %{}, fn {account, _, nick}, acc -> Map.put(acc, account.id, nick) end)
+
+ drinks =
+ :ets.select(Nola.Plugins.Alcoolog.ETS, match)
+ |> Enum.filter(fn {{account, _}, _vol, _cur, _cl, _deg, _name, _cmt, _meta} ->
+ Enum.member?(members_ids, account)
+ end)
+ |> Enum.map(fn {{account, _}, _, _, _, _, _, _, _} = object ->
+ {object, Map.get(member_names, account)}
+ end)
+ |> Enum.sort_by(fn {{{_, ts}, _, _, _, _, _, _, _}, _} -> ts end, &>/2)
stats = Nola.Plugins.Alcoolog.get_channel_statistics(account, network, channel)
- top = Enum.reduce(drinks, %{}, fn({{{account_id, _}, vol, _, _, _, _, _, _}, _}, acc) ->
- nick = Map.get(member_names, account_id)
- all = Map.get(acc, nick, 0)
- Map.put(acc, nick, all + vol)
- end)
- |> Enum.sort_by(fn({_nick, count}) -> count end, &>/2)
+ top =
+ Enum.reduce(drinks, %{}, fn {{{account_id, _}, vol, _, _, _, _, _, _}, _}, acc ->
+ nick = Map.get(member_names, account_id)
+ all = Map.get(acc, nick, 0)
+ Map.put(acc, nick, all + vol)
+ end)
+ |> Enum.sort_by(fn {_nick, count} -> count end, &>/2)
+
# {date, single_peak}
#
conn
|> assign(:title, "alcoolog")
- |> render("index.html", network: network, channel: channel, drinks: drinks, top: top, stats: stats)
+ |> render("index.html",
+ network: network,
+ channel: channel,
+ drinks: drinks,
+ top: top,
+ stats: stats
+ )
end
- def index_gls_json(conn = %{assigns: %{account: account}}, %{"network" => network, "chan" => channel}) do
+ def index_gls_json(conn = %{assigns: %{account: account}}, %{
+ "network" => network,
+ "chan" => channel
+ }) do
count = 30
channel = NolaWeb.reformat_chan(channel)
members = Nola.Membership.expanded_members_or_friends(account, network, channel)
- members_ids = Enum.map(members, fn({account, _, nick}) -> account.id end)
- member_names = Enum.reduce(members, %{}, fn({account, _, nick}, acc) -> Map.put(acc, account.id, nick) end)
- delay = count*((24 * 60)*60)
+ members_ids = Enum.map(members, fn {account, _, nick} -> account.id end)
+
+ member_names =
+ Enum.reduce(members, %{}, fn {account, _, nick}, acc -> Map.put(acc, account.id, nick) end)
+
+ delay = count * (24 * 60 * 60)
now = DateTime.utc_now()
- start_date = DateTime.utc_now()
- |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
- |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
- |> DateTime.to_date()
- |> Date.to_erl()
- filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
- |> Enum.to_list
- |> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
- |> Enum.map(&Date.from_erl!(&1))
- |> Enum.map(fn(date) ->
- {date, (for {a, _, _} <- members, into: Map.new, do: {Map.get(member_names, a.id, a.id), 0})}
- end)
- |> Enum.into(Map.new)
-
- gls = Enum.reduce(members, filled, fn({account, _, _}, gls) ->
- Enum.reduce(Nola.Plugins.Alcoolog.user_over_time_gl(account, count), gls, fn({date, gl}, gls) ->
- u = Map.get(gls, date, %{})
+
+ start_date =
+ DateTime.utc_now()
+ |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
+ |> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
+ |> DateTime.to_date()
+ |> Date.to_erl()
+
+ filled =
+ :calendar.date_to_gregorian_days(start_date)..:calendar.date_to_gregorian_days(
+ DateTime.utc_now()
+ |> DateTime.to_date()
+ |> Date.to_erl()
+ )
+ |> Enum.to_list()
+ |> Enum.map(&:calendar.gregorian_days_to_date(&1))
+ |> Enum.map(&Date.from_erl!(&1))
+ |> Enum.map(fn date ->
+ {date,
+ for({a, _, _} <- members, into: Map.new(), do: {Map.get(member_names, a.id, a.id), 0})}
+ end)
+ |> Enum.into(Map.new())
+
+ gls =
+ Enum.reduce(members, filled, fn {account, _, _}, gls ->
+ Enum.reduce(Nola.Plugins.Alcoolog.user_over_time_gl(account, count), gls, fn {date, gl},
+ gls ->
+ u =
+ Map.get(gls, date, %{})
|> Map.put(Map.get(member_names, account.id, account.id), gl)
- Map.put(gls, date, u)
+
+ Map.put(gls, date, u)
+ end)
end)
- end)
-
- dates = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
- |> Enum.to_list
- |> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
- |> Enum.map(&Date.from_erl!(&1))
-
- filled2 = Enum.map(member_names, fn({_, name}) ->
- history = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
- |> Enum.to_list
- |> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
- |> Enum.map(&Date.from_erl!(&1))
- |> Enum.map(fn(date) ->
- get_in(gls, [date, name]) #%{date: date, gl: get_in(gls, [date, name])}
- end)
- if Enum.all?(history, fn(x) -> x == 0 end) do
- nil
- else
- %{name: name, history: history}
- end
- end)
- |> Enum.filter(fn(x) -> x end)
- conn
- |> put_resp_content_type("application/json")
- |> text(Jason.encode!(%{labels: dates, data: filled2}))
+ dates =
+ :calendar.date_to_gregorian_days(start_date)..:calendar.date_to_gregorian_days(
+ DateTime.utc_now()
+ |> DateTime.to_date()
+ |> Date.to_erl()
+ )
+ |> Enum.to_list()
+ |> Enum.map(&:calendar.gregorian_days_to_date(&1))
+ |> Enum.map(&Date.from_erl!(&1))
+
+ filled2 =
+ Enum.map(member_names, fn {_, name} ->
+ history =
+ :calendar.date_to_gregorian_days(start_date)..:calendar.date_to_gregorian_days(
+ DateTime.utc_now()
+ |> DateTime.to_date()
+ |> Date.to_erl()
+ )
+ |> Enum.to_list()
+ |> Enum.map(&:calendar.gregorian_days_to_date(&1))
+ |> Enum.map(&Date.from_erl!(&1))
+ |> Enum.map(fn date ->
+ # %{date: date, gl: get_in(gls, [date, name])}
+ get_in(gls, [date, name])
+ end)
+
+ if Enum.all?(history, fn x -> x == 0 end) do
+ nil
+ else
+ %{name: name, history: history}
+ end
+ end)
+ |> Enum.filter(fn x -> x end)
+
+ conn
+ |> put_resp_content_type("application/json")
+ |> text(Jason.encode!(%{labels: dates, data: filled2}))
end
def minisync(conn, %{"user_id" => user_id, "key" => key, "value" => value}) do
account = Nola.Account.get(user_id)
+
if account do
ds = Nola.Plugins.Alcoolog.data_state()
meta = Nola.Plugins.Alcoolog.get_user_meta(ds, account.id)
+
case Float.parse(value) do
{val, _} ->
new_meta = Map.put(meta, String.to_existing_atom(key), val)
Nola.Plugins.Alcoolog.put_user_meta(ds, account.id, new_meta)
+
_ ->
conn
|> put_status(:unprocessable_entity)
@@ -319,5 +436,4 @@ defmodule NolaWeb.AlcoologController do
|> text("not found")
end
end
-
end
diff --git a/lib/web/controllers/gpt_controller.ex b/lib/web/controllers/gpt_controller.ex
deleted file mode 100644
index 810a875..0000000
--- a/lib/web/controllers/gpt_controller.ex
+++ /dev/null
@@ -1,33 +0,0 @@
-defmodule NolaWeb.GptController do
- use NolaWeb, :controller
- require Logger
-
- plug NolaWeb.ContextPlug
-
- def result(conn, params = %{"id" => result_id}) do
- case Nola.Plugins.Gpt.get_result(result_id) do
- {:ok, result} ->
- network = Map.get(params, "network")
- channel = if c = Map.get(params, "chan"), do: NolaWeb.reformat_chan(c)
- render(conn, "result.html", network: network, channel: channel, result: result)
- {:error, :not_found} ->
- conn
- |> put_status(404)
- |> text("Page not found")
- end
- end
-
- def prompt(conn, params = %{"id" => prompt_id}) do
- case Nola.Plugins.Gpt.get_prompt(prompt_id) do
- {:ok, prompt} ->
- network = Map.get(params, "network")
- channel = if c = Map.get(params, "chan"), do: NolaWeb.reformat_chan(c)
- render(conn, "prompt.html", network: network, channel: channel, prompt: prompt)
- {:error, :not_found} ->
- conn
- |> put_status(404)
- |> text("Page not found")
- end
- end
-
-end
diff --git a/lib/web/controllers/icecast_see_controller.ex b/lib/web/controllers/icecast_see_controller.ex
index 877ad4e..ca8fb2d 100644
--- a/lib/web/controllers/icecast_see_controller.ex
+++ b/lib/web/controllers/icecast_see_controller.ex
@@ -11,7 +11,7 @@ defmodule NolaWeb.IcecastSseController do
|> send_chunked(200)
|> subscribe
|> send_sse_message("ping", "ping")
- |> send_sse_message("icecast", Nola.IcecastAgent.get)
+ |> send_sse_message("icecast", Nola.IcecastAgent.get())
|> sse_loop
end
@@ -22,10 +22,11 @@ defmodule NolaWeb.IcecastSseController do
end
def sse_loop(conn) do
- {type, event} = receive do
- {:event, :ping} -> {"ping", "ping"}
- {:icecast, stats} -> {"icecast", stats}
- end
+ {type, event} =
+ receive do
+ {:event, :ping} -> {"ping", "ping"}
+ {:icecast, stats} -> {"icecast", stats}
+ end
conn
|> send_sse_message(type, event)
@@ -37,5 +38,4 @@ defmodule NolaWeb.IcecastSseController do
{:ok, conn} = chunk(conn, "event: #{type}\ndata: #{json}\n\n")
conn
end
-
end
diff --git a/lib/web/controllers/irc_auth_sse_controller.ex b/lib/web/controllers/irc_auth_sse_controller.ex
index 01c840b..f67a77f 100644
--- a/lib/web/controllers/irc_auth_sse_controller.ex
+++ b/lib/web/controllers/irc_auth_sse_controller.ex
@@ -6,12 +6,15 @@ defmodule NolaWeb.IrcAuthSseController do
@expire_delay :timer.minutes(3)
def sse(conn, params) do
- perks = if uri = Map.get(params, "redirect_to") do
- {:redirect, uri}
- else
- nil
- end
+ perks =
+ if uri = Map.get(params, "redirect_to") do
+ {:redirect, uri}
+ else
+ nil
+ end
+
token = String.downcase(EntropyString.random_string(65))
+
conn
|> assign(:token, token)
|> assign(:perks, perks)
@@ -31,25 +34,33 @@ defmodule NolaWeb.IrcAuthSseController do
end
def sse_loop(conn) do
- {type, event, exit} = receive do
- {:event, :ping} -> {"ping", "ping", false}
- {:event, :expire} -> {"expire", "expire", true}
- {:irc, :text, %{account: account, text: token} = m} ->
- if String.downcase(String.trim(token)) == conn.assigns.token do
- path = Nola.AuthToken.new_path(account.id, conn.assigns.perks)
- m.replyfun.("ok!")
- {"authenticated", path, true}
- else
+ {type, event, exit} =
+ receive do
+ {:event, :ping} ->
+ {"ping", "ping", false}
+
+ {:event, :expire} ->
+ {"expire", "expire", true}
+
+ {:irc, :text, %{account: account, text: token} = m} ->
+ if String.downcase(String.trim(token)) == conn.assigns.token do
+ path = Nola.AuthToken.new_path(account.id, conn.assigns.perks)
+ m.replyfun.("ok!")
+ {"authenticated", path, true}
+ else
+ {nil, nil, false}
+ end
+
+ _ ->
{nil, nil, false}
- end
- _ -> {nil, nil, false}
- end
+ end
- conn = if type do
- send_sse_message(conn, type, event)
- else
- conn
- end
+ conn =
+ if type do
+ send_sse_message(conn, type, event)
+ else
+ conn
+ end
if exit do
conn
@@ -62,5 +73,4 @@ defmodule NolaWeb.IrcAuthSseController do
{:ok, conn} = chunk(conn, "event: #{type}\ndata: #{data}\n\n")
conn
end
-
end
diff --git a/lib/web/controllers/irc_controller.ex b/lib/web/controllers/irc_controller.ex
index e87382b..9de807b 100644
--- a/lib/web/controllers/irc_controller.ex
+++ b/lib/web/controllers/irc_controller.ex
@@ -1,31 +1,46 @@
defmodule NolaWeb.IrcController do
use NolaWeb, :controller
- plug NolaWeb.ContextPlug
+ plug(NolaWeb.ContextPlug)
def index(conn, params) do
network = Map.get(params, "network")
channel = if c = Map.get(params, "chan"), do: NolaWeb.reformat_chan(c)
- commands = for mod <- Enum.uniq([Nola.Plugins.Account] ++ Nola.Plugins.enabled()) do
- if is_atom(mod) do
- identifier = Module.split(mod) |> List.last |> Macro.underscore
- {identifier, mod.irc_doc()}
+
+ commands =
+ for mod <- Enum.uniq([Nola.Plugins.Account] ++ Nola.Plugins.enabled()) do
+ if is_atom(mod) do
+ identifier = Module.split(mod) |> List.last() |> Macro.underscore()
+ if Kernel.function_exported?(mod, :irc_doc, 0), do: {identifier, mod.irc_doc()}
+ end
end
- end
- |> Enum.filter(& &1)
- |> Enum.filter(fn({_, doc}) -> doc end)
- members = cond do
- network && channel -> Enum.map(Nola.UserTrack.channel(network, channel), fn(tuple) -> Nola.UserTrack.User.from_tuple(tuple) end)
- true ->
- Nola.Membership.of_account(conn.assigns.account)
- end
- render conn, "index.html", network: network, commands: commands, channel: channel, members: members
+ |> Enum.filter(& &1)
+ |> Enum.filter(fn {_, doc} -> doc end)
+
+ members =
+ cond do
+ network && channel ->
+ Enum.map(Nola.UserTrack.channel(network, channel), fn tuple ->
+ Nola.UserTrack.User.from_tuple(tuple)
+ end)
+
+ true ->
+ Nola.Membership.of_account(conn.assigns.account)
+ end
+
+ render(conn, "index.html",
+ network: network,
+ commands: commands,
+ channel: channel,
+ members: members
+ )
end
def txt(conn, %{"name" => name}) do
if String.contains?(name, ".txt") do
name = String.replace(name, ".txt", "")
data = data()
+
if Map.has_key?(data, name) do
lines = Enum.join(data[name], "\n")
text(conn, lines)
@@ -38,32 +53,54 @@ defmodule NolaWeb.IrcController do
do_txt(conn, name)
end
end
- def txt(conn, _), do: do_txt(conn, nil)
+ def txt(conn, _), do: do_txt(conn, nil)
defp do_txt(conn, nil) do
doc = Nola.Plugins.Txt.irc_doc()
data = data()
- main = Enum.filter(data, fn({trigger, _}) -> !String.contains?(trigger, ".") end) |> Enum.into(Map.new)
- system = Enum.filter(data, fn({trigger, _}) -> String.contains?(trigger, ".") end) |> Enum.into(Map.new)
- lines = Enum.reduce(main, 0, fn({_, lines}, acc) -> acc + Enum.count(lines) end)
+
+ main =
+ Enum.filter(data, fn {trigger, _} -> !String.contains?(trigger, ".") end)
+ |> Enum.into(Map.new())
+
+ system =
+ Enum.filter(data, fn {trigger, _} -> String.contains?(trigger, ".") end)
+ |> Enum.into(Map.new())
+
+ lines = Enum.reduce(main, 0, fn {_, lines}, acc -> acc + Enum.count(lines) end)
+
conn
|> assign(:title, "txt")
- |> render("txts.html", data: main, doc: doc, files: Enum.count(main), lines: lines, system: system)
+ |> render("txts.html",
+ data: main,
+ doc: doc,
+ files: Enum.count(main),
+ lines: lines,
+ system: system
+ )
end
defp do_txt(conn, txt) do
data = data()
- base_url = cond do
- conn.assigns[:chan] -> "/#{conn.assigns.network}/#{NolaWeb.format_chan(conn.assigns.chan)}"
- true -> "/-"
- end
+
+ base_url =
+ cond do
+ conn.assigns[:chan] ->
+ "/#{conn.assigns.network}/#{NolaWeb.format_chan(conn.assigns.chan)}"
+
+ true ->
+ "/-"
+ end
+
if lines = Map.get(data, txt) do
- lines = Enum.map(lines, fn(line) ->
- line
- |> String.split("\\\\")
- |> Enum.intersperse(Phoenix.HTML.Tag.tag(:br))
- end)
+ lines =
+ Enum.map(lines, fn line ->
+ line
+ |> String.split("\\\\")
+ |> Enum.intersperse(Phoenix.HTML.Tag.tag(:br))
+ end)
+
conn
|> assign(:breadcrumbs, [{"txt", "#{base_url}/txt"}])
|> assign(:title, "#{txt}.txt")
@@ -77,25 +114,28 @@ defmodule NolaWeb.IrcController do
defp data() do
dir = Application.get_env(:nola, :data_path) <> "/irc.txt/"
+
Path.wildcard(dir <> "/*.txt")
- |> Enum.reduce(%{}, fn(path, m) ->
+ |> Enum.reduce(%{}, fn path, m ->
path = String.split(path, "/")
file = List.last(path)
key = String.replace(file, ".txt", "")
- data = dir <> file
- |> File.read!
- |> String.split("\n")
- |> Enum.reject(fn(line) ->
- cond do
- line == "" -> true
- !line -> true
- true -> false
- end
- end)
+
+ data =
+ (dir <> file)
+ |> File.read!()
+ |> String.split("\n")
+ |> Enum.reject(fn line ->
+ cond do
+ line == "" -> true
+ !line -> true
+ true -> false
+ end
+ end)
+
Map.put(m, key, data)
end)
- |> Enum.sort
- |> Enum.into(Map.new)
+ |> Enum.sort()
+ |> Enum.into(Map.new())
end
-
end
diff --git a/lib/web/controllers/network_controller.ex b/lib/web/controllers/network_controller.ex
index 800294f..07cc291 100644
--- a/lib/web/controllers/network_controller.ex
+++ b/lib/web/controllers/network_controller.ex
@@ -1,11 +1,10 @@
defmodule NolaWeb.NetworkController do
use NolaWeb, :controller
- plug NolaWeb.ContextPlug
+ plug(NolaWeb.ContextPlug)
def index(conn, %{"network" => network}) do
conn
|> assign(:title, network)
|> render("index.html")
end
-
end
diff --git a/lib/web/controllers/open_id_controller.ex b/lib/web/controllers/open_id_controller.ex
index 24dc1a5..b8ea505 100644
--- a/lib/web/controllers/open_id_controller.ex
+++ b/lib/web/controllers/open_id_controller.ex
@@ -1,10 +1,15 @@
defmodule NolaWeb.OpenIdController do
use NolaWeb, :controller
- plug NolaWeb.ContextPlug, restrict: :public
+ plug(NolaWeb.ContextPlug, restrict: :public)
require Logger
def login(conn, _) do
- url = OAuth2.Client.authorize_url!(new_client(), scope: "openid", state: Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false))
+ url =
+ OAuth2.Client.authorize_url!(new_client(),
+ scope: "openid",
+ state: Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false)
+ )
+
redirect(conn, external: url)
end
@@ -14,19 +19,21 @@ defmodule NolaWeb.OpenIdController do
end
def callback(conn, %{"code" => code, "state" => state}) do
- with \
- client = %{token: %OAuth2.AccessToken{access_token: json}} = OAuth2.Client.get_token!(new_client(), state: state, code: code),
+ with client =
+ %{token: %OAuth2.AccessToken{access_token: json}} =
+ OAuth2.Client.get_token!(new_client(), state: state, code: code),
{:ok, %{"access_token" => token}} <- Jason.decode(json),
- client = %OAuth2.Client{client | token: %OAuth2.AccessToken{access_token: token}},
+ client = %OAuth2.Client{client | token: %OAuth2.AccessToken{access_token: token}},
{:ok, %OAuth2.Response{body: body}} <- OAuth2.Client.get(client, "/userinfo"),
- {:ok, %{"sub" => id, "preferred_username" => username}} <- Jason.decode(body)
- do
+ {:ok, %{"sub" => id, "preferred_username" => username}} <- Jason.decode(body) do
if account = conn.assigns.account do
- if !Nola.Account.get_meta(account, "identity-id") do # XXX: And oidc id not linked yet
- Nola.Account.put_meta(account, "identity-id", id)
- end
- Nola.Account.put_meta(account, "identity-username", username)
- conn
+ # XXX: And oidc id not linked yet
+ if !Nola.Account.get_meta(account, "identity-id") do
+ Nola.Account.put_meta(account, "identity-id", id)
+ end
+
+ Nola.Account.put_meta(account, "identity-username", username)
+ conn
else
conn
end
@@ -39,8 +46,9 @@ defmodule NolaWeb.OpenIdController do
{:error, %OAuth2.Response{status_code: 401}} ->
Logger.error("OpenID: Unauthorized token")
render(conn, "error.html", error: "The token is invalid.")
+
{:error, %OAuth2.Error{reason: reason}} ->
- Logger.error("Error: #{inspect reason}")
+ Logger.error("Error: #{inspect(reason)}")
render(conn, "error.html", error: reason)
end
end
@@ -51,7 +59,8 @@ defmodule NolaWeb.OpenIdController do
defp new_client() do
config = Application.get_env(:nola, :oidc)
- OAuth2.Client.new([
+
+ OAuth2.Client.new(
strategy: OAuth2.Strategy.AuthCode,
client_id: config[:client_id],
client_secret: config[:client_secret],
@@ -59,6 +68,6 @@ defmodule NolaWeb.OpenIdController do
authorize_url: config[:authorize_url],
token_url: config[:token_url],
redirect_uri: Routes.open_id_url(NolaWeb.Endpoint, :callback)
- ])
+ )
end
end
diff --git a/lib/web/controllers/page_controller.ex b/lib/web/controllers/page_controller.ex
index cb46b8f..4f3c44e 100644
--- a/lib/web/controllers/page_controller.ex
+++ b/lib/web/controllers/page_controller.ex
@@ -1,15 +1,14 @@
defmodule NolaWeb.PageController do
use NolaWeb, :controller
- plug NolaWeb.ContextPlug when action not in [:token]
- plug NolaWeb.ContextPlug, [restrict: :public] when action in [:token]
+ plug(NolaWeb.ContextPlug when action not in [:token])
+ plug(NolaWeb.ContextPlug, [restrict: :public] when action in [:token])
def token(conn, %{"token" => token}) do
- with \
- {:ok, account, perks} <- Nola.AuthToken.lookup(token)
- do
- IO.puts("Authenticated account #{inspect account}")
+ with {:ok, account, perks} <- Nola.AuthToken.lookup(token) do
+ IO.puts("Authenticated account #{inspect(account)}")
conn = put_session(conn, :account, account)
+
case perks do
nil -> redirect(conn, to: "/")
{:redirect, path} -> redirect(conn, to: path)
@@ -27,27 +26,33 @@ defmodule NolaWeb.PageController do
users = Nola.UserTrack.find_by_account(account)
metas = Nola.Account.get_all_meta(account)
predicates = Nola.Account.get_predicates(account)
+
conn
|> assign(:title, account.name)
- |> render("user.html", users: users, memberships: memberships, metas: metas, predicates: predicates)
+ |> render("user.html",
+ users: users,
+ memberships: memberships,
+ metas: metas,
+ predicates: predicates
+ )
end
def irc(conn, _) do
- bot_helps = for mod <- Nola.Irc.env(:handlers) do
- mod.irc_doc()
- end
- render conn, "irc.html", bot_helps: bot_helps
+ bot_helps =
+ for mod <- Nola.Irc.env(:handlers) do
+ mod.irc_doc()
+ end
+
+ render(conn, "irc.html", bot_helps: bot_helps)
end
def authenticate(conn, _) do
- with \
- {:account, account_id} when is_binary(account_id) <- {:account, get_session(conn, :account)},
- {:account, account} when not is_nil(account) <- {:account, Nola.Account.get(account_id)}
- do
+ with {:account, account_id} when is_binary(account_id) <-
+ {:account, get_session(conn, :account)},
+ {:account, account} when not is_nil(account) <- {:account, Nola.Account.get(account_id)} do
assign(conn, :account, account)
else
_ -> conn
end
end
-
end
diff --git a/lib/web/controllers/sms_controller.ex b/lib/web/controllers/sms_controller.ex
index 0fffa23..20a58bd 100644
--- a/lib/web/controllers/sms_controller.ex
+++ b/lib/web/controllers/sms_controller.ex
@@ -3,8 +3,7 @@ defmodule NolaWeb.SmsController do
require Logger
def ovh_callback(conn, %{"senderid" => from, "message" => message}) do
- spawn(fn() -> Nola.Plugins.Sms.incoming(from, String.trim(message)) end)
+ spawn(fn -> Nola.Plugins.Sms.incoming(from, String.trim(message)) end)
text(conn, "")
end
-
end
diff --git a/lib/web/controllers/untappd_controller.ex b/lib/web/controllers/untappd_controller.ex
index e2e1596..f47f526 100644
--- a/lib/web/controllers/untappd_controller.ex
+++ b/lib/web/controllers/untappd_controller.ex
@@ -2,11 +2,10 @@ defmodule NolaWeb.UntappdController do
use NolaWeb, :controller
def callback(conn, %{"code" => code}) do
- with \
- {:account, account_id} when is_binary(account_id) <- {:account, get_session(conn, :account)},
+ with {:account, account_id} when is_binary(account_id) <-
+ {:account, get_session(conn, :account)},
{:account, account} when not is_nil(account) <- {:account, Nola.Account.get(account_id)},
- {:ok, auth_token} <- Untappd.auth_callback(code)
- do
+ {:ok, auth_token} <- Untappd.auth_callback(code) do
Nola.Account.put_meta(account, "untappd-token", auth_token)
text(conn, "OK!")
else
@@ -14,5 +13,4 @@ defmodule NolaWeb.UntappdController do
:error -> text(conn, "Error: untappd authentication failed")
end
end
-
end
diff --git a/lib/web/endpoint.ex b/lib/web/endpoint.ex
index a401f54..b54458d 100644
--- a/lib/web/endpoint.ex
+++ b/lib/web/endpoint.ex
@@ -1,49 +1,47 @@
defmodule NolaWeb.Endpoint do
- use Sentry.PlugCapture
use Phoenix.Endpoint, otp_app: :nola
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
- plug Plug.Static,
- at: "/", from: :nola, gzip: false,
+ plug(Plug.Static,
+ at: "/",
+ from: :nola,
+ gzip: false,
only: ~w(assets css js fonts images favicon.ico robots.txt)
+ )
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
- if 42==43 && code_reloading? do
- socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
- plug Phoenix.LiveReloader
- plug Phoenix.CodeReloader
+ if 42 == 43 && code_reloading? do
+ socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket)
+ plug(Phoenix.LiveReloader)
+ plug(Phoenix.CodeReloader)
end
- plug Plug.RequestId
- plug Plug.Logger
+ plug(Plug.RequestId)
+ plug(Plug.Logger)
- plug Plug.Parsers,
+ plug(Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Jason
+ )
- plug Sentry.PlugContext
- plug Plug.MethodOverride
- plug Plug.Head
+ plug(Plug.MethodOverride)
+ plug(Plug.Head)
- @session_options [store: :cookie,
- key: "_nola_key",
- signing_salt: "+p7K3wrj"]
+ @session_options [store: :cookie, key: "_nola_key", signing_salt: "+p7K3wrj"]
-
- socket "/live", Phoenix.LiveView.Socket,
- websocket: [connect_info: [session: @session_options]]
+ socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
- plug Plug.Session, @session_options
+ plug(Plug.Session, @session_options)
- plug NolaWeb.Router
+ plug(NolaWeb.Router)
@doc """
Callback invoked for dynamically configuring the endpoint.
diff --git a/lib/web/live/chat_live.ex b/lib/web/live/chat_live.ex
index 8a9f6f2..0d45428 100644
--- a/lib/web/live/chat_live.ex
+++ b/lib/web/live/chat_live.ex
@@ -7,38 +7,47 @@ defmodule NolaWeb.ChatLive do
chan = NolaWeb.reformat_chan(chan)
connection = Nola.Irc.Connection.get_network(network, chan)
account = Nola.Account.get(account_id)
- membership = Nola.Membership.of_account(Nola.Account.get("DRgpD4fLf8PDJMLp8Dtb"))
+ membership = Nola.Membership.of_account(Nola.Account.get(account.id))
+
if account && connection && Enum.member?(membership, {connection.network, chan}) do
- {:ok, _} = Registry.register(Nola.PubSub, "#{connection.network}:events", plugin: __MODULE__)
+ {:ok, _} =
+ Registry.register(Nola.PubSub, "#{connection.network}:events", plugin: __MODULE__)
+
for t <- ["messages", "triggers", "outputs", "events"] do
- {:ok, _} = Registry.register(Nola.PubSub, "#{connection.network}/#{chan}:#{t}", plugin: __MODULE__)
+ {:ok, _} =
+ Registry.register(Nola.PubSub, "#{connection.network}/#{chan}:#{t}", plugin: __MODULE__)
end
Nola.Irc.PuppetConnection.start(account, connection)
- users = Nola.UserTrack.channel(connection.network, chan)
- |> Enum.map(fn(tuple) -> Nola.UserTrack.User.from_tuple(tuple) end)
- |> Enum.reduce(Map.new, fn(user = %{id: id}, acc) ->
- Map.put(acc, id, user)
- end)
-
- backlog = case Nola.Plugins.Buffer.select_buffer(connection.network, chan) do
- {backlog, _} ->
- {backlog, _} = Enum.reduce(backlog, {backlog, nil}, &reduce_contextual_event/2)
- Enum.reverse(backlog)
- _ -> []
- end
-
- socket = socket
- |> assign(:connection_id, connection.id)
- |> assign(:network, connection.network)
- |> assign(:chan, chan)
- |> assign(:title, "live")
- |> assign(:channel, chan)
- |> assign(:account_id, account.id)
- |> assign(:backlog, backlog)
- |> assign(:users, users)
- |> assign(:counter, 0)
+ users =
+ Nola.UserTrack.channel(connection.network, chan)
+ |> Enum.map(fn tuple -> Nola.UserTrack.User.from_tuple(tuple) end)
+ |> Enum.reduce(Map.new(), fn user = %{id: id}, acc ->
+ Map.put(acc, id, user)
+ end)
+
+ backlog =
+ case Nola.Plugins.Buffer.select_buffer(connection.network, chan) do
+ {backlog, _} ->
+ {backlog, _} = Enum.reduce(backlog, {backlog, nil}, &reduce_contextual_event/2)
+ Enum.reverse(backlog)
+
+ _ ->
+ []
+ end
+
+ socket =
+ socket
+ |> assign(:connection_id, connection.id)
+ |> assign(:network, connection.network)
+ |> assign(:chan, chan)
+ |> assign(:title, "live")
+ |> assign(:channel, chan)
+ |> assign(:account_id, account.id)
+ |> assign(:backlog, backlog)
+ |> assign(:users, users)
+ |> assign(:counter, 0)
{:ok, socket}
else
@@ -54,9 +63,11 @@ defmodule NolaWeb.ChatLive do
def handle_info({:irc, :event, event = %{type: :join, user_id: id}}, socket) do
if user = Nola.UserTrack.lookup(id) do
- socket = socket
- |> assign(:users, Map.put(socket.assigns.users, id, user))
- |> append_to_backlog(event)
+ socket =
+ socket
+ |> assign(:users, Map.put(socket.assigns.users, id, user))
+ |> append_to_backlog(event)
+
{:noreply, socket}
else
{:noreply, socket}
@@ -64,23 +75,29 @@ defmodule NolaWeb.ChatLive do
end
def handle_info({:irc, :event, event = %{type: :nick, user_id: id, nick: nick}}, socket) do
- socket = socket
- |> assign(:users, update_in(socket.assigns.users, [id, :nick], nick))
- |> append_to_backlog(event)
+ socket =
+ socket
+ |> assign(:users, update_in(socket.assigns.users, [id, :nick], nick))
+ |> append_to_backlog(event)
+
{:noreply, socket}
end
def handle_info({:irc, :event, event = %{type: :quit, user_id: id}}, socket) do
- socket = socket
- |> assign(:users, Map.delete(socket.assigns.users, id))
- |> append_to_backlog(event)
+ socket =
+ socket
+ |> assign(:users, Map.delete(socket.assigns.users, id))
+ |> append_to_backlog(event)
+
{:noreply, socket}
end
def handle_info({:irc, :event, event = %{type: :part, user_id: id}}, socket) do
- socket = socket
- |> assign(:users, Map.delete(socket.assigns.users, id))
- |> append_to_backlog(event)
+ socket =
+ socket
+ |> assign(:users, Map.delete(socket.assigns.users, id))
+ |> append_to_backlog(event)
+
{:noreply, socket}
end
@@ -88,15 +105,19 @@ defmodule NolaWeb.ChatLive do
handle_info({:irc, nil, message}, socket)
end
- def handle_info({:irc, :text, message}, socket) do
- IO.inspect({:live_message, message})
- socket = socket
- |> append_to_backlog(message)
+ # type is text, out, or nil if it's self?
+ def handle_info({:irc, type, message = %Nola.Message{}}, socket) do
+ IO.inspect({:live_message, type, message})
+
+ socket =
+ socket
+ |> append_to_backlog(message)
+
{:noreply, socket}
end
def handle_info(info, socket) do
- Logger.debug("Unhandled info: #{inspect info}")
+ Logger.debug("Unhandled info: #{inspect(info)}")
{:noreply, socket}
end
@@ -108,13 +129,12 @@ defmodule NolaWeb.ChatLive do
defp reduce_contextual_event(line, {acc, nil}) do
{[line | acc], line}
end
+
defp reduce_contextual_event(line, {acc, last}) do
if NaiveDateTime.to_date(last.at) != NaiveDateTime.to_date(line.at) do
{[%{type: :day_changed, date: NaiveDateTime.to_date(line.at), at: nil}, line | acc], line}
- else
- {[line | acc], line}
- end
-
+ else
+ {[line | acc], line}
+ end
end
-
end
diff --git a/lib/web/live/chat_live.html.heex b/lib/web/live/chat_live.html.heex
index 470604f..c3bb030 100644
--- a/lib/web/live/chat_live.html.heex
+++ b/lib/web/live/chat_live.html.heex
@@ -26,10 +26,10 @@
<%= for message <- @backlog do %>
<%= if is_map(message) && Map.get(message, :__struct__) == Nola.Message do %>
<li class="flex gap-2 place-items-center message"
- data-account-id={message.account.id}>
+ data-account-id={if(message.account, do: message.account.id, else: "bot")}>
<NolaWeb.MessageComponent.content
message={message}
- self={message.account.id == @account_id}
+ self={message.account && message.account.id == @account_id}
text={message.text}
/>
</li>
diff --git a/lib/web/router.ex b/lib/web/router.ex
index 5658fda..18509ac 100644
--- a/lib/web/router.ex
+++ b/lib/web/router.ex
@@ -2,84 +2,86 @@ defmodule NolaWeb.Router do
use NolaWeb, :router
pipeline :browser do
- plug :accepts, ["html", "txt"]
- plug :fetch_session
- plug :fetch_flash
- plug :fetch_live_flash
- plug :protect_from_forgery
- plug :put_secure_browser_headers
- plug :put_root_layout, {NolaWeb.LayoutView, :root}
+ plug(:accepts, ["html", "txt"])
+ plug(:fetch_session)
+ plug(:fetch_flash)
+ plug(:fetch_live_flash)
+ plug(:protect_from_forgery)
+ plug(:put_secure_browser_headers)
+ plug(:put_root_layout, {NolaWeb.LayoutView, :root})
end
pipeline :api do
- plug :accepts, ["json", "sse"]
+ plug(:accepts, ["json", "sse"])
end
pipeline :matrix_app_service do
- plug :accepts, ["json"]
- plug Nola.Matrix.Plug.Auth
- plug Nola.Matrix.Plug.SetConfig
+ plug(:accepts, ["json"])
+ plug(Nola.Matrix.Plug.Auth)
+ plug(Nola.Matrix.Plug.SetConfig)
end
scope "/api", NolaWeb do
- pipe_through :api
- get "/irc-auth.sse", IrcAuthSseController, :sse
- post "/sms/callback/Ovh", SmsController, :ovh_callback, as: :sms
+ pipe_through(:api)
+ get("/irc-auth.sse", IrcAuthSseController, :sse)
+ post("/sms/callback/Ovh", SmsController, :ovh_callback, as: :sms)
end
scope "/", NolaWeb do
- pipe_through :browser
- get "/", PageController, :index
-
- get "/login/irc/:token", PageController, :token, as: :login
- get "/login/oidc", OpenIdController, :login
- get "/login/oidc/callback", OpenIdController, :callback
-
- get "/api/untappd/callback", UntappdController, :callback, as: :untappd_callback
-
- get "/-", IrcController, :index
- get "/-/txt", IrcController, :txt
- get "/-/txt/:name", IrcController, :txt
- get "/-/gpt/prompt/:id", GptController, :task
- get "/-/gpt/result/:id", GptController, :result
-
- get "/-/alcoolog", AlcoologController, :index
- get "/-/alcoolog/~/:account_name", AlcoologController, :index
- get "/:network", NetworkController, :index
- get "/:network/~:nick/alcoolog", AlcoologController, :nick
- get "/:network/~:nick/alcoolog/log.json", AlcoologController, :nick_log_json
- get "/:network/~:nick/alcoolog/gls.json", AlcoologController, :nick_gls_json
- get "/:network/~:nick/alcoolog/volumes.json", AlcoologController, :nick_volumes_json
- get "/:network/~:nick/alcoolog/history.json", AlcoologController, :nick_history_json
- get "/:network/~:nick/alcoolog/stats.json", AlcoologController, :nick_stats_json
- get "/:network/:chan/alcoolog", AlcoologController, :index
- get "/:network/:chan/alcoolog/gls.json", AlcoologController, :index_gls_json
- get "/:network/:chan/gpt/prompt/:id", GptController, :task
- get "/:network/:chan/gpt/result/:id", GptController, :result
- put "/api/alcoolog/minisync/:user_id/meta/:key", AlcoologController, :minisync_put_meta
-
- get "/:network/:chan", IrcController, :index
- live "/:network/:chan/live", ChatLive
- get "/:network/:chan/txt", IrcController, :txt
- get "/:network/:chan/txt/:name", IrcController, :txt
- get "/:network/:channel/preums", IrcController, :preums
- get "/:network/:chan/alcoolog/t/:token", AlcoologController, :token
- end
+ pipe_through(:browser)
+ get("/", PageController, :index)
- scope "/_matrix/:appservice", MatrixAppServiceWeb.V1, as: :matrix do
- pipe_through :matrix_app_service
+ get("/login/irc/:token", PageController, :token, as: :login)
+ get("/login/oidc", OpenIdController, :login)
+ get("/login/oidc/callback", OpenIdController, :callback)
+
+ get("/api/untappd/callback", UntappdController, :callback, as: :untappd_callback)
+
+ get("/-", IrcController, :index)
+ get("/-/txt", IrcController, :txt)
+ get("/-/txt/:name", IrcController, :txt)
+
+ get("/-/gpt", GptController, :index)
+ get("/-/gpt/p/:id", GptController, :task)
+ get("/-/gpt/r/:id", GptController, :result)
+
+ get("/-/alcoolog", AlcoologController, :index)
+ get("/-/alcoolog/~/:account_name", AlcoologController, :index)
- put "/transactions/:txn_id", TransactionController, :push
+ get("/:network", NetworkController, :index)
- get "/users/:user_id", UserController, :query
- get "/rooms/*room_alias", RoomController, :query
+ get("/:network/~:nick/alcoolog", AlcoologController, :nick)
+ get("/:network/~:nick/alcoolog/log.json", AlcoologController, :nick_log_json)
+ get("/:network/~:nick/alcoolog/gls.json", AlcoologController, :nick_gls_json)
+ get("/:network/~:nick/alcoolog/volumes.json", AlcoologController, :nick_volumes_json)
+ get("/:network/~:nick/alcoolog/history.json", AlcoologController, :nick_history_json)
+ get("/:network/~:nick/alcoolog/stats.json", AlcoologController, :nick_stats_json)
- get "/thirdparty/protocol/:protocol", ThirdPartyController, :query_protocol
- get "/thirdparty/user/:protocol", ThirdPartyController, :query_users
- get "/thirdparty/location/:protocol", ThirdPartyController, :query_locations
- get "/thirdparty/location", ThirdPartyController, :query_location_by_alias
- get "/thirdparty/user", ThirdPartyController, :query_user_by_id
+ get("/:network/:chan/alcoolog", AlcoologController, :index)
+ get("/:network/:chan/alcoolog/gls.json", AlcoologController, :index_gls_json)
+
+ put("/api/alcoolog/minisync/:user_id/meta/:key", AlcoologController, :minisync_put_meta)
+
+ get("/:network/:chan", IrcController, :index)
+ live("/:network/:chan/live", ChatLive)
+ get("/:network/:chan/txt", IrcController, :txt)
+ get("/:network/:chan/txt/:name", IrcController, :txt)
+ get("/:network/:channel/preums", IrcController, :preums)
+ get("/:network/:chan/alcoolog/t/:token", AlcoologController, :token)
end
+ scope "/_matrix/:appservice", MatrixAppServiceWeb.V1, as: :matrix do
+ pipe_through(:matrix_app_service)
+
+ put("/transactions/:txn_id", TransactionController, :push)
+
+ get("/users/:user_id", UserController, :query)
+ get("/rooms/*room_alias", RoomController, :query)
+ get("/thirdparty/protocol/:protocol", ThirdPartyController, :query_protocol)
+ get("/thirdparty/user/:protocol", ThirdPartyController, :query_users)
+ get("/thirdparty/location/:protocol", ThirdPartyController, :query_locations)
+ get("/thirdparty/location", ThirdPartyController, :query_location_by_alias)
+ get("/thirdparty/user", ThirdPartyController, :query_user_by_id)
+ end
end
diff --git a/lib/web/templates/layout/app.html.eex b/lib/web/templates/layout/app.html.eex
index b3199e2..99dbe9e 100644
--- a/lib/web/templates/layout/app.html.eex
+++ b/lib/web/templates/layout/app.html.eex
@@ -121,7 +121,7 @@
<%= @inner_content %>
</div>
<div mt-4 text-grey text-small">
- v<%= Nola.version() %> <a href="<%= Nola.source_url() %>">source</a>
+ v<%= Nola.version() %> &mdash; <a href="<%= Nola.source_url() %>">source</a>
</div>
<!-- /End replace -->
</div>
diff --git a/lib/web/views/alcoolog_view.ex b/lib/web/views/alcoolog_view.ex
index ad52472..3a86038 100644
--- a/lib/web/views/alcoolog_view.ex
+++ b/lib/web/views/alcoolog_view.ex
@@ -1,6 +1,4 @@
defmodule NolaWeb.AlcoologView do
use NolaWeb, :view
require Integer
-
end
-
diff --git a/lib/web/views/error_helpers.ex b/lib/web/views/error_helpers.ex
index 25214bd..156b801 100644
--- a/lib/web/views/error_helpers.ex
+++ b/lib/web/views/error_helpers.ex
@@ -9,8 +9,8 @@ defmodule NolaWeb.ErrorHelpers do
Generates tag for inlined form input errors.
"""
def error_tag(form, field) do
- Enum.map(Keyword.get_values(form.errors, field), fn (error) ->
- content_tag :span, translate_error(error), class: "help-block"
+ Enum.map(Keyword.get_values(form.errors, field), fn error ->
+ content_tag(:span, translate_error(error), class: "help-block")
end)
end
diff --git a/lib/web/views/error_view.ex b/lib/web/views/error_view.ex
index 5cad939..5acd333 100644
--- a/lib/web/views/error_view.ex
+++ b/lib/web/views/error_view.ex
@@ -12,6 +12,6 @@ defmodule NolaWeb.ErrorView do
# In case no render clause matches or no
# template is found, let's render it as 500
def template_not_found(_template, assigns) do
- render "500.html", assigns
+ render("500.html", assigns)
end
end
diff --git a/lib/web/views/layout_view.ex b/lib/web/views/layout_view.ex
index 663eccf..747740f 100644
--- a/lib/web/views/layout_view.ex
+++ b/lib/web/views/layout_view.ex
@@ -2,17 +2,27 @@ defmodule NolaWeb.LayoutView do
use NolaWeb, :view
def liquid_markdown(conn, text) do
- context_path = cond do
- conn.assigns[:chan] -> "/#{conn.assigns[:network]}/#{NolaWeb.format_chan(conn.assigns[:chan])}"
- conn.assigns[:network] -> "/#{conn.assigns[:network]}/-"
- true -> "/-"
- end
+ context_path =
+ cond do
+ conn.assigns[:chan] ->
+ "/#{conn.assigns[:network]}/#{NolaWeb.format_chan(conn.assigns[:chan])}"
+
+ conn.assigns[:network] ->
+ "/#{conn.assigns[:network]}/-"
+
+ true ->
+ "/-"
+ end
{:ok, ast} = Liquex.parse(text)
- context = Liquex.Context.new(%{
- "context_path" => context_path
- })
+
+ context =
+ Liquex.Context.new(%{
+ "context_path" => context_path
+ })
+
{content, _} = Liquex.render(ast, context)
+
content
|> to_string()
|> Earmark.as_html!()
@@ -20,21 +30,28 @@ defmodule NolaWeb.LayoutView do
end
def page_title(conn) do
- target = cond do
- conn.assigns[:chan] ->
- "#{conn.assigns.chan} @ #{conn.assigns.network}"
- conn.assigns[:network] -> conn.assigns.network
- true -> Nola.name()
- end
-
- breadcrumb_title = Enum.map(Map.get(conn.assigns, :breadcrumbs)||[], fn({title, _href}) -> title end)
-
- title = [conn.assigns[:title], breadcrumb_title, target]
- |> List.flatten()
- |> Enum.uniq()
- |> Enum.filter(fn(x) -> x end)
- |> Enum.intersperse(" / ")
- |> Enum.join()
+ target =
+ cond do
+ conn.assigns[:chan] ->
+ "#{conn.assigns.chan} @ #{conn.assigns.network}"
+
+ conn.assigns[:network] ->
+ conn.assigns.network
+
+ true ->
+ Nola.name()
+ end
+
+ breadcrumb_title =
+ Enum.map(Map.get(conn.assigns, :breadcrumbs) || [], fn {title, _href} -> title end)
+
+ title =
+ [conn.assigns[:title], breadcrumb_title, target]
+ |> List.flatten()
+ |> Enum.uniq()
+ |> Enum.filter(fn x -> x end)
+ |> Enum.intersperse(" / ")
+ |> Enum.join()
content_tag(:title, title)
end
@@ -42,14 +59,16 @@ defmodule NolaWeb.LayoutView do
def format_time(date, with_relative \\ true) do
alias Timex.Format.DateTime.Formatters
alias Timex.Timezone
- date = if is_integer(date) do
- date
- |> DateTime.from_unix!(:millisecond)
- |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
- else
- date
- |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
- end
+
+ date =
+ if is_integer(date) do
+ date
+ |> DateTime.from_unix!(:millisecond)
+ |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
+ else
+ date
+ |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
+ end
now = DateTime.now!("Europe/Paris", Tzdata.TimeZoneDatabase)
@@ -57,25 +76,39 @@ defmodule NolaWeb.LayoutView do
date_week = Timex.iso_week(date)
{y, w} = now_week
- now_last_week = {y, w-1}
- now_last_roll = 7-Timex.days_to_beginning_of_week(now)
+ now_last_week = {y, w - 1}
+ now_last_roll = 7 - Timex.days_to_beginning_of_week(now)
date_date = DateTime.to_date(date)
now_date = DateTime.to_date(date)
- format = cond do
- date.year != now.year -> "{D}/{M}/{YYYY} {h24}:{m}"
- date_date == now_date -> "{h24}:{m}"
- (now_week == date_week) || (date_week == now_last_week && (Date.day_of_week(date) >= now_last_roll)) -> "{WDfull} {h24}:{m}"
- (now.year == date.year && now.month == date.month) -> "{WDfull} {D} {h24}:{m}"
- true -> "{WDfull} {D} {M} {h24}:{m}"
- end
+ format =
+ cond do
+ date.year != now.year ->
+ "{D}/{M}/{YYYY} {h24}:{m}"
- {:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
- {:ok, full} = Formatters.Default.lformat(date, "{WDfull} {D} {YYYY} {h24}:{m}", "fr") #"{h24}:{m} {WDfull} {D}", "fr")
- {:ok, detail} = Formatters.Default.lformat(date, format, "fr") #"{h24}:{m} {WDfull} {D}", "fr")
+ date_date == now_date ->
+ "{h24}:{m}"
- content_tag(:time, if(with_relative, do: relative, else: detail), [title: full])
- end
+ now_week == date_week ||
+ (date_week == now_last_week && Date.day_of_week(date) >= now_last_roll) ->
+ "{WDfull} {h24}:{m}"
+
+ now.year == date.year && now.month == date.month ->
+ "{WDfull} {D} {h24}:{m}"
+ true ->
+ "{WDfull} {D} {M} {h24}:{m}"
+ end
+
+ {:ok, relative} =
+ Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
+
+ # "{h24}:{m} {WDfull} {D}", "fr")
+ {:ok, full} = Formatters.Default.lformat(date, "{WDfull} {D} {YYYY} {h24}:{m}", "fr")
+ # "{h24}:{m} {WDfull} {D}", "fr")
+ {:ok, detail} = Formatters.Default.lformat(date, format, "fr")
+
+ content_tag(:time, if(with_relative, do: relative, else: detail), title: full)
+ end
end
diff --git a/lib/web/views/network_view.ex b/lib/web/views/network_view.ex
index 7a24db1..58b0e55 100644
--- a/lib/web/views/network_view.ex
+++ b/lib/web/views/network_view.ex
@@ -1,4 +1,3 @@
defmodule NolaWeb.NetworkView do
use NolaWeb, :view
-
end
diff --git a/lib/web/views/open_id_view.ex b/lib/web/views/open_id_view.ex
index bd8089b..b847202 100644
--- a/lib/web/views/open_id_view.ex
+++ b/lib/web/views/open_id_view.ex
@@ -1,4 +1,3 @@
defmodule NolaWeb.OpenIdView do
use NolaWeb, :view
-
end
diff --git a/mix.exs b/mix.exs
index 765b008..ee3e14b 100644
--- a/mix.exs
+++ b/mix.exs
@@ -6,9 +6,9 @@ defmodule Nola.Mixfile do
app: :nola,
version: version("0.2.7"),
elixir: "~> 1.4",
- elixirc_paths: elixirc_paths(Mix.env),
- compilers: [:phoenix, :gettext] ++ Mix.compilers,
- start_permanent: Mix.env == :prod,
+ elixirc_paths: elixirc_paths(Mix.env()),
+ compilers: [:phoenix, :gettext] ++ Mix.compilers(),
+ start_permanent: Mix.env() == :prod,
deps: deps()
]
end
@@ -21,7 +21,7 @@ defmodule Nola.Mixfile do
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
- defp elixirc_paths(_), do: ["lib"]
+ defp elixirc_paths(_), do: ["lib"]
defp aliases do
[
@@ -41,22 +41,22 @@ defmodule Nola.Mixfile do
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 0.5"},
{:plug_cowboy, "~> 2.0"},
- {:cowlib, "~> 2.9.1", override: true},
+ {:cowlib, "~> 2.15.0", override: true},
{:plug, "~> 1.7"},
{:gettext, "~> 0.11"},
{:httpoison, "~> 1.8", override: true},
{:jason, "~> 1.0"},
{:poison, "~> 4.0", override: true},
- {:floki, "~> 0.19.3"},
- {:ecto, "~> 3.4"},
+ {:floki, "~> 0.38.0"},
+ {:fast_html, "~> 2.0"},
+ {:ecto, "~> 3.13"},
{:exirc, git: "https://git.random.sh/ircbot/exirc.git", branch: "fix-who-nick"},
{:distillery, "~> 2.0"},
{:earmark, "~> 1.2"},
{:oauther, "~> 1.1"},
{:extwitter, "~> 0.14.0"},
{:entropy_string, "~> 1.0.0"},
- {:abacus, "~> 0.3.3"},
- {:ex_chain, github: "eljojo/ex_chain"},
+ {:abacus, "~> 2.1"},
{:timex, "~> 3.6"},
{:muontrap, "~> 0.5.1"},
{:tzdata, "~> 1.0"},
@@ -70,14 +70,18 @@ defmodule Nola.Mixfile do
{:html_entities, "0.4.0", override: true},
{:file_size, "~> 3.0"},
{:ex2ms, "~> 1.0"},
- {:polyjuice_client, git: "https://git.random.sh/ircbot/polyjuice_client.git", branch: "master", override: true},
- {:matrix_app_service, git: "https://git.random.sh/ircbot/matrix_app_service.ex.git", branch: "master"},
- {:sentry, "~> 8.0.5"},
+ {:polyjuice_client,
+ git: "https://git.random.sh/ircbot/polyjuice_client.git", branch: "master", override: true},
+ {:matrix_app_service,
+ git: "https://git.random.sh/ircbot/matrix_app_service.ex.git", branch: "master"},
{:logger_json, "~> 4.3"},
{:oauth2, "~> 2.0"},
- {:powerdnsex, git: "https://git.random.sh/ircbot/powerdnsex.git", branch: "master"},
- {:pfx, "~> 0.7.0"},
- {:flake_id, "~> 0.1.0"}
+ # {:powerdnsex, git: "https://git.random.sh/ircbot/powerdnsex.git", branch: "master"},
+ # {:pfx, "~> 0.7.0"},
+ {:flake_id, "~> 0.1.0"},
+ {:gun, "~> 1.3"},
+ {:idna, "~> 6.0"},
+ {:castore, "~> 0.1"}
]
end
@@ -85,22 +89,25 @@ defmodule Nola.Mixfile do
{describe, 0} = System.cmd("git", ~w(describe --dirty --broken --all --tags --long))
[_, rest] = String.split(describe, "/", parts: 2)
- info = rest
- |> String.trim()
- |> String.replace("/", "-")
- env = cond do
- Mix.env() == :prod -> ""
- true -> "." <> to_string(Mix.env())
- end
+ info =
+ rest
+ |> String.trim()
+ |> String.replace("/", "-")
- build_timestamp = DateTime.utc_now()
- |> DateTime.to_unix()
- |> to_string()
+ env =
+ cond do
+ Mix.env() == :prod -> ""
+ true -> "." <> to_string(Mix.env())
+ end
+
+ build_timestamp =
+ DateTime.utc_now()
+ |> DateTime.to_unix()
+ |> to_string()
build_date_tag = ".build" <> build_timestamp
v <> "+" <> info <> env <> build_date_tag
end
-
end
diff --git a/mix.lock b/mix.lock
index 5d9507c..a577036 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,90 +1,98 @@
%{
- "abacus": {:hex, :abacus, "0.3.3", "f2f11e23073f5e16af36ac425cd9fa9a338695e2f8014684239fa14a9171d5f6", [:mix], [], "hexpm", "a41110183de16eda239f2187e7bb0c91c50658a8ae8254b85352287eb7034d88"},
+ "abacus": {:hex, :abacus, "2.1.0", "b6db5c989ba3d9dd8c36d1cb269e2f0058f34768d47c67eb8ce06697ecb36dd4", [:mix], [], "hexpm", "255de08b02884e8383f1eed8aa31df884ce0fb5eb394db81ff888089f2a1bbff"},
"artificery": {:hex, :artificery, "0.4.3", "0bc4260f988dcb9dda4b23f9fc3c6c8b99a6220a331534fdf5bf2fd0d4333b02", [:mix], [], "hexpm", "12e95333a30e20884e937abdbefa3e7f5e05609c2ba8cf37b33f000b9ffc0504"},
"backoff": {:git, "https://github.com/ferd/backoff", "4b8c02d038de1055481b0193665944e11fec337e", [branch: "master"]},
"base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"},
- "castore": {:hex, :castore, "0.1.11", "c0665858e0e1c3e8c27178e73dffea699a5b28eb72239a3b2642d208e8594914", [:mix], [], "hexpm", "91b009ba61973b532b84f7c09ce441cba7aa15cb8b006cf06c6f4bba18220081"},
- "certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
+ "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
+ "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
- "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
+ "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
- "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
+ "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
- "date_time_parser": {:hex, :date_time_parser, "1.1.1", "cd7a04eb8f413a63cfb16892575d08a23651de1118c95278c13f84c105247901", [:mix], [{:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:timex, ">= 3.2.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2ede6de7994c1589bcf118954999ed6ff5de97415b33827ea5b30804c7e512ef"},
- "db_connection": {:hex, :db_connection, "2.4.0", "d04b1b73795dae60cead94189f1b8a51cc9e1f911c234cc23074017c43c031e5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad416c21ad9f61b3103d254a71b63696ecadb6a917b36f563921e0de00d7d7c8"},
- "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
+ "date_time_parser": {:hex, :date_time_parser, "1.2.0", "3d5a816b91967f51e0f94dcb16a34b2cb780f22cd48931779e81d72f7d3eadb1", [:mix], [{:kday, "~> 1.0", [hex: :kday, repo: "hexpm", optional: false]}], "hexpm", "0cf09ada9f42c0b3bfba02dc0ea2e4b4d2f543d9d2bf99b831a29e6b4a4160e5"},
+ "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"},
+ "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm", "bbc7008b0161a6f130d8d903b5b3232351fccc9c31a991f8fcbf2a12ace22995"},
- "earmark": {:hex, :earmark, "1.4.15", "2c7f924bf495ec1f65bd144b355d0949a05a254d0ec561740308a54946a67888", [:mix], [{:earmark_parser, ">= 1.4.13", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "3b1209b85bc9f3586f370f7c363f6533788fb4e51db23aa79565875e7f9999ee"},
+ "earmark": {:hex, :earmark, "1.4.48", "5f41e579d85ef812351211842b6e005f6e0cef111216dea7d4b9d58af4608434", [:mix], [], "hexpm", "a461a0ddfdc5432381c876af1c86c411fd78a25790c75023c7a4c035fdc858f9"},
"earmark_parser": {:hex, :earmark_parser, "1.4.15", "b29e8e729f4aa4a00436580dcc2c9c5c51890613457c193cc8525c388ccb2f06", [:mix], [], "hexpm", "044523d6438ea19c1b8ec877ec221b008661d3c27e3b848f4c879f500421ca5c"},
- "ecto": {:hex, :ecto, "3.7.1", "a20598862351b29f80f285b21ec5297da1181c0442687f9b8329f0445d228892", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d36e5b39fc479e654cffd4dbe1865d9716e4a9b6311faff799b6f90ab81b8638"},
- "ecto_sql": {:hex, :ecto_sql, "3.7.0", "2fcaad4ab0c8d76a5afbef078162806adbe709c04160aca58400d5cbbe8eeac6", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a26135dfa1d99bf87a928c464cfa25bba6535a4fe761eefa56077a4febc60f70"},
- "elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
+ "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"},
+ "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"},
+ "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
"entropy_string": {:hex, :entropy_string, "1.0.7", "61a5a989e78fd2798e35a17a98a17f81fb504e8d4ba620bcd4f19063eb782943", [:mix], [], "hexpm", "c497fc9cf6bae2075c4c985e66b4306baa4cb19f142d97e0aa1d7a993ae3bb47"},
- "ex2ms": {:hex, :ex2ms, "1.6.1", "66d472eb14da43087c156e0396bac3cc7176b4f24590a251db53f84e9a0f5f72", [:mix], [], "hexpm", "a7192899d84af03823a8ec2f306fa858cbcce2c2e7fd0f1c49e05168fb9c740e"},
- "ex_aws": {:hex, :ex_aws, "2.2.4", "b6b9a73468205c67851f6c195429b435741ec3d5f45be4cfdaa8f54a62491f15", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc9af9199d869c0532819f87678a547c7dd7c57088d932b43dcbdbc315886601"},
- "ex_aws_s3": {:hex, :ex_aws_s3, "2.3.0", "5dfe50116bad048240bae7cd9418bfe23296542ff72a01b9138113a1cd31451c", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "0b13b11478825d62d2f6e57ae763695331be06f2216468f31bb304316758b096"},
+ "ex2ms": {:hex, :ex2ms, "1.7.0", "45b9f523d0b777667ded60070d82d871a37e294f0b6c5b8eca86771f00f82ee1", [:mix], [], "hexpm", "2589eee51f81f1b1caa6d08c990b1ad409215fe6f64c73f73c67d36ed10be827"},
+ "ex_aws": {:hex, :ex_aws, "2.5.10", "d3f8ca8959dad6533a2a934dfdf380df1b1bef425feeb215a47a5176dee8736c", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "88fcd9cc1b2e0fcea65106bdaa8340ac56c6e29bf72f46cf7ef174027532d3da"},
+ "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.7", "e571424d2f345299753382f3a01b005c422b1a460a8bc3ed47659b3d3ef91e9e", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "858e51241e50181e29aa2bc128fef548873a3a9cd580471f57eda5b64dec937f"},
"ex_chain": {:git, "https://github.com/eljojo/ex_chain.git", "09d88a10613b6acc33340c9fa1b3540493e431b8", []},
+ "ex_json_schema": {:hex, :ex_json_schema, "0.9.2", "c9a42e04e70cd70eb11a8903a22e8ec344df16edef4cb8e6ec84ed0caffc9f0f", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "4854329cb352b6c01c4c4b8dbfb3be14dc5bea19ea13e0eafade4ff22ba55224"},
"exirc": {:git, "https://git.random.sh/ircbot/exirc.git", "ae1de0025dc0184697c0a440eb3cbbf505b154b6", [branch: "fix-who-nick"]},
+ "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
"extwitter": {:hex, :extwitter, "0.14.0", "5e15c5ea5e6a09baaf03fd5da6de1431eeeca0ef05a9c0f6cc62872e5b8816ed", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:oauther, "~> 1.3", [hex: :oauther, repo: "hexpm", optional: false]}], "hexpm", "5e9bbb491531317062df42ab56ccb8368addb00931b0785c8a5e1d652813b0a8"},
+ "fast_html": {:hex, :fast_html, "2.3.0", "08c1d8ead840dd3060ba02c761bed9f37f456a1ddfe30bcdcfee8f651cec06a6", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "f18e3c7668f82d3ae0b15f48d48feeb257e28aa5ab1b0dbf781c7312e5da029d"},
"file_size": {:hex, :file_size, "3.0.1", "ad447a69442a82fc701765a73992d7b1110136fa0d4a9d3190ea685d60034dcd", [:mix], [{:decimal, ">= 1.0.0 and < 3.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:number, "~> 1.0", [hex: :number, repo: "hexpm", optional: false]}], "hexpm", "64dd665bc37920480c249785788265f5d42e98830d757c6a477b3246703b8e20"},
- "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
+ "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
- "floki": {:hex, :floki, "0.19.3", "652d1447767f783bd6cae1d882fd2145f25db28c6841ab87659225b468cff101", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "1c8da482a0848c55a1d22af49ce6547790077adac2a04cf265e1f26583781adb"},
+ "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"},
"gen_magic": {:git, "https://github.com/hrefhref/gen_magic", "48a12cca10305c8d357fe16b10fd7ead9b64a56a", [branch: "develop"]},
- "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
+ "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
"gun": {:hex, :gun, "1.3.3", "cf8b51beb36c22b9c8df1921e3f2bc4d2b1f68b49ad4fbc64e91875aa14e16b4", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "3106ce167f9c9723f849e4fb54ea4a4d814e3996ae243a1c828b256e749041e0"},
- "hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"},
+ "hackney": {:hex, :hackney, "1.24.1", "f5205a125bba6ed4587f9db3cc7c729d11316fa8f215d3e57ed1c067a9703fa9", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "f4a7392a0b53d8bbc3eb855bdcc919cd677358e65b2afd3840b5b3690c4c8a39"},
+ "html5ever": {:hex, :html5ever, "0.16.1", "3dccc3349e0c3e5f5542bcc09253e6246d174391aca692bdecccd446a1c62132", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6.0 or ~> 0.7.0", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "6eb06b7796eb100bc815dffd3f500de376a426a088a8405402305cdd8e7cc08a"},
"html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm", "3e3d7156a272950373ce5a4018b1490bea26676f8d6a7d409f6fac8568b8cb9a"},
- "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "abfb393ad888d57700f4d0f119c2643c8a9d98856f9b8a92001be7efad1419d6"},
- "httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},
- "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
- "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
- "liquex": {:hex, :liquex, "0.6.1", "2e07fc177dfb2ecafe326f11bd641373f3f6b62704a0231832d8634e162e852a", [:mix], [{:date_time_parser, "~> 1.1", [hex: :date_time_parser, repo: "hexpm", optional: false]}, {:html_entities, "~> 0.5.1", [hex: :html_entities, repo: "hexpm", optional: false]}, {:html_sanitize_ex, "~> 1.3.0", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:timex, "~> 3.6", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2ec6c68fce04e10ca1fd3874d146991cf1b44adc0c8451615873263944353772"},
+ "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.3", "67b3d9fa8691b727317e0cc96b9b3093be00ee45419ffb221cdeee88e75d1360", [:mix], [{:mochiweb, "~> 2.15 or ~> 3.1", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "87748d3c4afe949c7c6eb7150c958c2bcba43fc5b2a02686af30e636b74bccb7"},
+ "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
+ "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
+ "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
+ "kday": {:hex, :kday, "1.1.0", "64efac85279a12283eaaf3ad6f13001ca2dff943eda8c53288179775a8c057a0", [:mix], [{:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}], "hexpm", "69703055d63b8d5b260479266c78b0b3e66f7aecdd2022906cd9bf09892a266d"},
+ "liquex": {:hex, :liquex, "0.13.1", "49f90d0b85fb2908f2558f35cd49d78497fe77a895eb55b360889940e1d7afb9", [:mix], [{:date_time_parser, "~> 1.2", [hex: :date_time_parser, repo: "hexpm", optional: false]}, {:html_entities, "~> 0.5.2", [hex: :html_entities, repo: "hexpm", optional: false]}, {:html_sanitize_ex, "~> 1.4.3", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fbea5b9db264c1758a69bfafdcc8aaebcd56e168365bb9575392cd55d800108f"},
"logger_json": {:hex, :logger_json, "4.3.0", "41aaaab2c2e1c071bfddbcc5a3f567884fdf312d222c7f1a7e3de6ab667774f7", [:mix], [{:ecto, "~> 2.1 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "001bbc34d7c451cfeed298c8384cb3aab10b364db2eb095c466c7a1a28bee6e0"},
"matrix_app_service": {:git, "https://git.random.sh/ircbot/matrix_app_service.ex.git", "5a4efc102f97abad6b60fab7bf761acef6acc78d", [branch: "master"]},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
- "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
- "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
- "mochiweb": {:hex, :mochiweb, "2.22.0", "f104d6747c01a330c38613561977e565b788b9170055c5241ac9dd6e4617cba5", [:rebar3], [], "hexpm", "cbbd1fd315d283c576d1c8a13e0738f6dafb63dc840611249608697502a07655"},
+ "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
+ "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
+ "mochiweb": {:hex, :mochiweb, "3.2.2", "bb435384b3b9fd1f92f2f3fe652ea644432877a3e8a81ed6459ce951e0482ad3", [:rebar3], [], "hexpm", "4114e51f1b44c270b3242d91294fe174ce1ed989100e8b65a1fab58e0cba41d5"},
"muontrap": {:hex, :muontrap, "0.5.1", "98fe96d0e616ee518860803a37a29eb23ffc2ca900047cb1bb7fd37521010093", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3c11b7f151b202148912c73cbdd633b76fa68fabc26cc441c9d6d140e22290dc"},
"mutex": {:hex, :mutex, "1.1.3", "d7e19f96fe19d6d97583bf12ca1ec182bbf14619b7568592cc461135de1c3b81", [:mix], [], "hexpm", "2b83b92784add2611c23dd527073b5e8dfe3c9c6c94c5bf9e3081b5c41c3ff3e"},
"nimble_csv": {:hex, :nimble_csv, "0.7.0", "52f23ce46eee304d063d1716e19e45ea544bd751536bc53e5d41cb7fc0ca9405", [:mix], [], "hexpm", "e7051e7a95b5c4f26512af5805c320ee9185e752d949f048bf318fedef86cccc"},
- "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
- "nimble_pool": {:hex, :nimble_pool, "0.2.4", "1db8e9f8a53d967d595e0b32a17030cdb6c0dc4a451b8ac787bf601d3f7704c3", [:mix], [], "hexpm", "367e8071e137b787764e6a9992ccb57b276dc2282535f767a07d881951ebeac6"},
+ "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
+ "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
"nimble_strftime": {:hex, :nimble_strftime, "0.1.1", "b988184d1bd945bc139b2c27dd00a6c0774ec94f6b0b580083abd62d5d07818b", [:mix], [], "hexpm", "89e599c9b8b4d1203b7bb5c79eb51ef7c6a28fbc6228230b312f8b796310d755"},
- "number": {:hex, :number, "1.0.3", "932c8a2d478a181c624138958ca88a78070332191b8061717270d939778c9857", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "dd397bbc096b2ca965a6a430126cc9cf7b9ef7421130def69bcf572232ca0f18"},
- "oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"},
+ "number": {:hex, :number, "1.0.5", "d92136f9b9382aeb50145782f116112078b3465b7be58df1f85952b8bb399b0f", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c0733a0a90773a66582b9e92a3f01290987f395c972cb7d685f51dd927cd5169"},
+ "oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"},
"oauther": {:hex, :oauther, "1.3.0", "82b399607f0ca9d01c640438b34d74ebd9e4acd716508f868e864537ecdb1f76", [:mix], [], "hexpm", "78eb888ea875c72ca27b0864a6f550bc6ee84f2eeca37b093d3d833fbcaec04e"},
- "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
+ "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"pfx": {:hex, :pfx, "0.7.0", "551ead4c303d6e4943d315bba349ee2a7cecf05a5311d8a8e6a2661cc9e64951", [:mix], [], "hexpm", "4497f1625c0b71d5749bebca0acf564ae60e5ea374645088c7c57079165379ae"},
- "phoenix": {:hex, :phoenix, "1.6.0-rc.0", "87dc1bb400588019a878ecf32c2d229c7d7f31a520c574860a059934663ffa70", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2a0d344d2a2f654a9300b2b09dbf9c3821762e1364e26fce12d76fcd498b92c0"},
- "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
- "phoenix_html": {:hex, :phoenix_html, "3.0.2", "0d71bd7dfa5fad2103142206e25e16accd64f41bcbd0002af3f0da17e530968d", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d6c6e85d9bef8d52a5a66fcccd15529651f379eaccbf10500343a17f6f814f82"},
- "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.5.0", "3282d8646e1bfc1ef1218f508d9fcefd48cf47f9081b7667bd9b281b688a49cf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.6", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.16.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "609740be43de94ae0abd2c4300ff0356a6e8a9487bf340e69967643a59fa7ec8"},
- "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
- "phoenix_live_view": {:hex, :phoenix_live_view, "0.16.1", "a17652e936718b6b6b52ef64d4b9860bc30c41b9a491e25f2b49a70604efa436", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "94bbc572471ad151b756b38dd10acbf91e0bcc132ad8b78240baa0dcf77cea74"},
- "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
- "phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"},
- "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},
- "plug_cowboy": {:hex, :plug_cowboy, "2.5.1", "7cc96ff645158a94cf3ec9744464414f02287f832d6847079adfe0b58761cbd0", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "107d0a5865fa92bcb48e631cc0729ae9ccfa0a9f9a1bd8f01acb513abf1c2d64"},
- "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
+ "phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"},
+ "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.5.3", "ff153c46aee237dd7244f07e9b98d557fe0d1de7a5916438e634c3be2d13c607", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.16.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "e36e62b1f61c19b645853af78290a5e7900f7cae1e676714ff69f9836e2f2e76"},
+ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", "2791fac0e2776b640192308cc90c0dbcf67843ad51387ed4ecae2038263d708d", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b3a1fa036d7eb2f956774eda7a7638cf5123f8f2175aca6d6420a7f95e598e1c"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "0.16.4", "5692edd0bac247a9a816eee7394e32e7a764959c7d0cf9190662fc8b0cd24c97", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "754ba49aa2e8601afd4f151492c93eb72df69b0b9856bab17711b8397e43bba0"},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
+ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
+ "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
+ "plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"},
+ "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"},
+ "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"},
"polyjuice_client": {:git, "https://git.random.sh/ircbot/polyjuice_client.git", "92c949be2def3cd0280cbc78849b109d34c8fcaa", [branch: "master"]},
"polyjuice_util": {:hex, :polyjuice_util, "0.1.0", "69901959c143245b47829c8302d0605dff6c0e1c3b116730c162982e0f512ee0", [:mix], [], "hexpm", "af5d1f614f52ce1da59a1f5a7c49249a2dbfda279d99d52d1b4e83e84c19a8d5"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"powerdnsex": {:git, "https://git.random.sh/ircbot/powerdnsex.git", "1dad0c28ac0af45f0b5b1171af2a117fc6b341bf", [branch: "master"]},
- "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
- "retry": {:hex, :retry, "0.14.1", "722d1b0cf87096b71213f5801d99fface7ca76adc83fc9dbf3e1daee952aef10", [:mix], [], "hexpm", "b3a609f286f6fe4f6b2c15f32cd4a8a60427d78d05d7b68c2dd9110981111ae0"},
+ "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
+ "retry": {:hex, :retry, "0.19.0", "aeb326d87f62295d950f41e1255fe6f43280a1b390d36e280b7c9b00601ccbc2", [:mix], [], "hexpm", "85ef376aa60007e7bff565c366310966ec1bd38078765a0e7f20ec8a220d02ca"},
+ "rustler_precompiled": {:hex, :rustler_precompiled, "0.7.3", "42cb9449785cd86c87453e39afdd27a0bdfa5c77a4ec5dc5ce45112e06b9f89b", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "cbc4b3777682e5f6f43ed39b0e0b4a42dccde8053aba91b4514e8f5ff9a5ac6d"},
"sentry": {:hex, :sentry, "8.0.5", "5ca922b9238a50c7258b52f47364b2d545beda5e436c7a43965b34577f1ef61f", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "4972839fdbf52e886d7b3e694c8adf421f764f2fa79036b88fb4742049bd4b7c"},
- "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
+ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.6", "45866d958d9ae51cfe8fef0050ab8054d25cba23ace43b88046092aa2c714645", [:make], [], "hexpm", "72b2fc8a8e23d77eed4441137fefa491bbf4a6dc52e9c0045f3f8e92e66243b5"},
"telegram": {:git, "https://github.com/hrefhref/telegram.git", "21c81460a633b656d2de8aa33beff49c3bb87670", [branch: "master"]},
"telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
- "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
- "tesla": {:hex, :tesla, "1.4.3", "f5a494e08fb1abe4fd9c28abb17f3d9b62b8f6fc492860baa91efb1aab61c8a0", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "e0755bb664bf4d664af72931f320c97adbf89da4586670f4864bf259b5750386"},
- "timex": {:hex, :timex, "3.7.6", "502d2347ec550e77fdf419bc12d15bdccd31266bb7d925b30bf478268098282f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "a296327f79cb1ec795b896698c56e662ed7210cc9eb31f0ab365eb3a62e2c589"},
- "tzdata": {:hex, :tzdata, "1.1.0", "72f5babaa9390d0f131465c8702fa76da0919e37ba32baa90d93c583301a8359", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "18f453739b48d3dc5bcf0e8906d2dc112bb40baafe2c707596d89f3c8dd14034"},
- "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
+ "tesla": {:hex, :tesla, "1.14.3", "b27ba2814cc08b5c4eb5f0245120198542cd023f575c490fa14447ae6763ea8d", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "4f419aa2ab908cff43a117d3b5de99edad8fd54690211cbbdc7d2941c03a1458"},
+ "timex": {:hex, :timex, "3.7.13", "0688ce11950f5b65e154e42b47bf67b15d3bc0e0c3def62199991b8a8079a1e2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "09588e0522669328e973b8b4fd8741246321b3f0d32735b589f78b136e6d4c54"},
+ "tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"},
+ "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
+ "wasmex": {:hex, :wasmex, "0.8.2", "5e781944b32105b1f606b04e99a1acb39d65ba8660ad5bc4ba233340842f8de5", [:mix], [{:rustler, "~> 0.26.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.5.5", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "44a5c3452565d08453433f8d66fa19c46bc2f87ac9125fd24eb535677cf58313"},
}