Page MenuHomePhabricator

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/lib/nola_irc/plugin_supervisor.ex b/lib/nola/plugins.ex
similarity index 88%
rename from lib/nola_irc/plugin_supervisor.ex
rename to lib/nola/plugins.ex
index a65ad09..4b55f4c 100644
--- a/lib/nola_irc/plugin_supervisor.ex
+++ b/lib/nola/plugins.ex
@@ -1,99 +1,99 @@
-defmodule IRC.Plugin do
+defmodule Nola.Plugins do
require Logger
defmodule Supervisor do
use DynamicSupervisor
require Logger
def start_link() do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def start_child(module, opts \\ []) do
Logger.info("Starting #{module}")
- spec = %{id: {IRC.Plugin,module}, start: {IRC.Plugin, :start_link, [module, opts]}, name: module, restart: :transient}
+ spec = %{id: {__MODULE__,module}, start: {__MODULE__, :start_link, [module, opts]}, name: module, restart: :transient}
case DynamicSupervisor.start_child(__MODULE__, spec) do
{:ok, _} = res -> res
:ignore ->
Logger.warn("Ignored #{module}")
:ignore
{:error,_} = res ->
Logger.error("Could not start #{module}: #{inspect(res, pretty: true)}")
res
end
end
@impl true
def init(_init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
max_restarts: 10,
max_seconds: 1
)
end
end
def dets(), do: to_charlist(Nola.data_path("/plugins.dets"))
def setup() do
:dets.open_file(dets(), [])
end
def enabled() do
:dets.foldl(fn
{name, true, _}, acc -> [name | acc]
_, acc -> acc
end, [], dets())
end
def start_all() do
- for mod <- enabled(), do: {mod, IRC.Plugin.Supervisor.start_child(mod)}
+ for mod <- enabled(), do: {mod, __MODULE__.Supervisor.start_child(mod)}
end
def declare(module) do
case get(module) do
:disabled -> :dets.insert(dets(), {module, true, nil})
_ -> nil
end
end
def start(module, opts \\ []) do
- IRC.Plugin.Supervisor.start_child(module)
+ __MODULE__.Supervisor.start_child(module)
end
@doc "Enables a plugin"
def enable(name), do: switch(name, true)
@doc "Disables a plugin"
def disable(name), do: switch(name, false)
@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
:dets.insert(dets(), {name, value, last})
end
@spec get(module()) :: {:ok, last_start :: nil | non_neg_integer()} | :disabled
def get(name) do
case :dets.lookup(dets(), name) do
[{name, enabled, last_start}] -> {:ok, enabled, last_start}
_ -> :disabled
end
end
def start_link(module, options \\ []) do
with {:disabled, {_, true, last}} <- {:disabled, get(module)},
{:throttled, false} <- {:throttled, false}
do
module.start_link()
else
{error, _} ->
- Logger.info("Plugin: #{to_string(module)} ignored start: #{to_string(error)}")
+ Logger.info("#{__MODULE__}: #{to_string(module)} ignored start: #{to_string(error)}")
:ignore
end
end
end
diff --git a/lib/nola_irc/account.ex b/lib/nola_irc/account.ex
index 45680f8..a39733c 100644
--- a/lib/nola_irc/account.ex
+++ b/lib/nola_irc/account.ex
@@ -1,451 +1,263 @@
defmodule IRC.Account do
alias IRC.UserTrack.User
@moduledoc """
Account registry....
Maps a network predicate:
* `{net, {:nick, nickname}}`
* `{net, {:account, account}}`
* `{net, {:mask, user@host}}`
to an unique identifier, that can be shared over multiple networks.
If a predicate cannot be found for an existing account, a new account will be made in the database.
To link two existing accounts from different network onto a different one, a merge operation is provided.
"""
# FIXME: Ensure uniqueness of name?
@derive {Poison.Encoder, except: [:token]}
defstruct [:id, :name, :token]
@type t :: %__MODULE__{id: id(), name: String.t()}
@type id :: String.t()
defimpl Inspect, for: __MODULE__ do
import Inspect.Algebra
def inspect(%{id: id, name: name}, opts) do
concat(["#IRC.Account[", id, " ", name, "]"])
end
end
def file(base) do
to_charlist(Nola.data_path() <> "/account_#{base}.dets")
end
defp from_struct(%__MODULE__{id: id, name: name, token: token}) do
{id, name, token}
end
defp from_tuple({id, name, token}) do
%__MODULE__{id: id, name: name, token: token}
end
def start_link() do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def init(_) do
{:ok, accounts} = :dets.open_file(file("db"), [])
{:ok, meta} = :dets.open_file(file("meta"), [])
{:ok, predicates} = :dets.open_file(file("predicates"), [{:type, :set}])
{:ok, %{accounts: accounts, meta: meta, predicates: predicates}}
end
def get(id) do
case :dets.lookup(file("db"), id) do
[account] -> from_tuple(account)
_ -> nil
end
end
def get_by_name(name) do
spec = [{{:_, :"$1", :_}, [{:==, :"$1", {:const, name}}], [:"$_"]}]
case :dets.select(file("db"), spec) do
[account] -> from_tuple(account)
_ -> nil
end
end
def get_meta(%__MODULE__{id: id}, key, default \\ nil) do
case :dets.lookup(file("meta"), {id, key}) do
[{_, value}] -> (value || default)
_ -> default
end
end
@spec find_meta_accounts(String.t()) :: [{account :: t(), value :: String.t()}, ...]
@doc "Find all accounts that have a meta of `key`."
def find_meta_accounts(key) do
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$2", {:const, key}}], [{{:"$1", :"$3"}}]}]
for {id, val} <- :dets.select(file("meta"), spec), do: {get(id), val}
end
@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"]}]
case :dets.select(file("meta"), spec) do
[id] -> get(id)
_ -> nil
end
end
def get_all_meta(%__MODULE__{id: id}) do
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$1", {:const, id}}], [{{:"$2", :"$3"}}]}]
:dets.select(file("meta"), spec)
end
def put_user_meta(account = %__MODULE__{}, key, value) do
put_meta(account, "u:"<>key, value)
end
def put_meta(%__MODULE__{id: id}, key, value) do
:dets.insert(file("meta"), {{id, key}, value})
end
def delete_meta(%__MODULE__{id: id}, key) do
:dets.delete(file("meta"), {id, key})
end
def all_accounts() do
: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)
end
def all_meta() do
:dets.traverse(file("meta"), fn(obj) -> {:continue, obj} end)
end
def merge_account(old_id, new_id) do
if old_id != new_id do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, old_id}}], [:"$1"]}]
predicates = :dets.select(file("predicates"), spec)
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
:dets.delete(file("meta"), {{old_id, k}})
:ok = :dets.insert(file("meta"), {{new_id, k}, v})
end
:dets.delete(file("db"), old_id)
IRC.Membership.merge_account(old_id, new_id)
IRC.UserTrack.merge_account(old_id, new_id)
IRC.Connection.dispatch("account", {:account_change, old_id, new_id})
IRC.Connection.dispatch("conn", {:account_change, old_id, new_id})
end
:ok
end
@doc "Find an account by a logged in user"
def find_by_nick(network, nick) do
do_lookup(%ExIRC.SenderInfo{nick: nick, network: network}, false)
end
@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),
nil <- do_lookup(%User{network: network, nick: nick}, false),
nil <- get_by_name(nick)
do
nil
else
%__MODULE__{} = account ->
memberships = IRC.Membership.of_account(account)
if Enum.any?(memberships, fn({net, ch}) -> (net == network) or (chan && chan == ch) end) do
account
else
nil
end
end
end
def find(something) do
do_lookup(something, false)
end
def lookup(something, make_default \\ true) do
account = do_lookup(something, make_default)
if account && Map.get(something, :nick) do
IRC.Connection.dispatch("account", {:account_auth, Map.get(something, :nick), account.id})
end
account
end
def handle_info(_, state) do
{:noreply, state}
end
def handle_cast(_, state) do
{:noreply, state}
end
def handle_call(_, _, state) do
{:noreply, state}
end
def terminate(_, state) do
for {_, dets} <- state do
:dets.sync(dets)
:dets.close(dets)
end
end
defp do_lookup(message = %IRC.Message{account: account_id}, make_default) when is_binary(account_id) do
get(account_id)
end
defp do_lookup(sender = %ExIRC.Who{}, make_default) do
if user = IRC.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)
end
end
defp do_lookup(sender = %ExIRC.SenderInfo{}, make_default) do
lookup(IRC.UserTrack.find_by_nick(sender), make_default)
end
defp do_lookup(user = %User{account: id}, make_default) when is_binary(id) do
get(id)
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)
end
defp do_lookup(nil, _) do
nil
end
defp lookup_by_nick(_, [{_, id}], _make_default) do
get(id)
end
defp lookup_by_nick(user, _, make_default) do
#authenticate_by_host(user)
if make_default, do: new_account(user), else: nil
end
def new_account(nick) do
id = EntropyString.large_id()
:dets.insert(file("db"), {id, nick, EntropyString.token()})
get(id)
end
def new_account(%{nick: nick, network: server}) do
id = EntropyString.large_id()
:dets.insert(file("db"), {id, nick, EntropyString.token()})
:dets.insert(file("predicates"), {{server, {:nick, nick}}, id})
get(id)
end
def update_account_name(account = %__MODULE__{id: id}, name) do
account = %__MODULE__{account | name: name}
:dets.insert(file("db"), from_struct(account))
get(id)
end
def get_predicates(%__MODULE__{} = account) do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
:dets.select(file("predicates"), spec)
end
- defmodule AccountPlugin 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(IRC.PubSub, "messages:private", [])
- {:ok, nil}
- end
-
- def handle_info({:irc, :text, m = %IRC.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 = %IRC.Message{account: account, text: "auth"}}, state) do
- spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
- predicates = :dets.select(IRC.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 = %IRC.Message{account: account, text: "whoami"}}, state) do
- users = for user <- IRC.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 = %IRC.Message{account: account, text: "account"}}, state) do
- account = IRC.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
-
- def handle_info({:irc, :text, m = %IRC.Message{sender: sender, text: "auth"<>_}}, state) do
- #account = IRC.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 = %IRC.Message{account: account, text: "set-name "<>name}}, state) do
- IRC.Account.update_account_name(account, name)
- m.replyfun.("Name changed: #{name}")
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{text: "disable-sms"}}, state) do
- if IRC.Account.get_meta(m.account, "sms-number") do
- IRC.Account.delete_meta(m.account, "sms-number")
- m.replfyun.("SMS disabled.")
- else
- m.replyfun.("SMS already disabled.")
- end
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{text: "web"}}, state) do
- auth_url = Untappd.auth_url()
- login_url = Nola.AuthToken.new_url(m.account.id, nil)
- m.replyfun.("-> " <> login_url)
- {:noreply, state}
- end
-
- def handle_info({:irc, :text, m = %IRC.Message{text: "enable-sms"}}, state) do
- code = String.downcase(EntropyString.small_id())
- IRC.Account.put_meta(m.account, "sms-validation-code", code)
- IRC.Account.put_meta(m.account, "sms-validation-target", m.network)
- number = Nola.IRC.SmsPlugin.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 = %IRC.Message{text: "enable-telegram"}}, state) do
- code = String.downcase(EntropyString.small_id())
- IRC.Account.delete_meta(m.account, "telegram-id")
- IRC.Account.put_meta(m.account, "telegram-validation-code", code)
- IRC.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 = %IRC.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 = %IRC.Message{text: "getmeta"<>_}}, state) do
- result = case String.split(m.text, " ") do
- ["getmeta"] ->
- for {k, v} <- IRC.Account.get_all_meta(m.account) do
- case k do
- "u:"<>key -> "(user) #{key}: #{v}"
- key -> "#{key}: #{v}"
- end
- end
- ["getmeta", key] ->
- value = IRC.Account.get_meta(m.account, key)
- 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 = %IRC.Message{text: "setusermet"<>_}}, state) do
- result = case String.split(m.text, " ") do
- ["setusermeta", key, value] ->
- IRC.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
-
- defp join_account(m, id, token) do
- old_account = IRC.Account.lookup(m.sender)
- new_account = IRC.Account.get(id)
- if new_account && token == new_account.token do
- case IRC.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
-
end
diff --git a/lib/nola_plugins/admin_handler.ex b/lib/nola_irc/admin_handler.ex
similarity index 95%
rename from lib/nola_plugins/admin_handler.ex
rename to lib/nola_irc/admin_handler.ex
index 9a5d557..282f3c2 100644
--- a/lib/nola_plugins/admin_handler.ex
+++ b/lib/nola_irc/admin_handler.ex
@@ -1,41 +1,41 @@
-defmodule Nola.IRC.AdminHandler do
+defmodule Nola.Irc.AdminHandler do
@moduledoc """
# admin
!op
op; requiert admin
"""
def irc_doc, do: nil
def start_link(client) do
GenServer.start_link(__MODULE__, [client])
end
def init([client]) do
ExIRC.Client.add_handler client, self
:ok = IRC.register("op")
{:ok, client}
end
def handle_info({:irc, :trigger, "op", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}, sender: sender}}, client) do
if IRC.admin?(sender) do
m.replyfun.({:mode, "+o"})
else
m.replyfun.({:kick, "non"})
end
{:noreply, client}
end
def handle_info({:joined, chan, sender}, client) do
if IRC.admin?(sender) do
ExIRC.Client.mode(client, chan, "+o", sender.nick)
end
{:noreply, client}
end
def handle_info(msg, client) do
{:noreply, client}
end
end
diff --git a/lib/nola_irc/nola_irc.ex b/lib/nola_irc/nola_irc.ex
index f64978a..870b99d 100644
--- a/lib/nola_irc/nola_irc.ex
+++ b/lib/nola_irc/nola_irc.ex
@@ -1,34 +1,34 @@
defmodule Nola.IRC do
require Logger
def env(), do: Nola.env(:irc)
def env(key, default \\ nil), do: Keyword.get(env(), key, default)
def application_childs do
import Supervisor.Spec
IRC.Connection.setup()
- IRC.Plugin.setup()
+ Nola.Plugins.setup()
[
worker(Registry, [[keys: :duplicate, name: IRC.ConnectionPubSub]], id: :registr_irc_conn),
worker(Registry, [[keys: :duplicate, name: IRC.PubSub]], id: :registry_irc),
worker(IRC.Membership, []),
worker(IRC.Account, []),
worker(IRC.UserTrack.Storage, []),
- worker(IRC.Account.AccountPlugin, []),
- supervisor(IRC.Plugin.Supervisor, [], [name: IRC.Plugin.Supervisor]),
+ worker(Nola.Plugins.Account, []),
+ supervisor(Nola.Plugins.Supervisor, [], [name: Nola.Plugins.Supervisor]),
supervisor(IRC.Connection.Supervisor, [], [name: IRC.Connection.Supervisor]),
supervisor(IRC.PuppetConnection.Supervisor, [], [name: IRC.PuppetConnection.Supervisor]),
]
end
# Start plugins first to let them get on connection events.
def after_start() do
Logger.info("Starting plugins")
- IRC.Plugin.start_all()
+ Nola.Plugins.start_all()
Logger.info("Starting connections")
IRC.Connection.start_all()
end
end
diff --git a/lib/nola_plugins/README.md b/lib/nola_plugins/README.md
new file mode 100644
index 0000000..82adba8
--- /dev/null
+++ b/lib/nola_plugins/README.md
@@ -0,0 +1 @@
+## Nola Plugins directory
diff --git a/lib/nola_plugins/account.ex b/lib/nola_plugins/account.ex
new file mode 100644
index 0000000..c11d077
--- /dev/null
+++ b/lib/nola_plugins/account.ex
@@ -0,0 +1,188 @@
+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(IRC.PubSub, "messages:private", [])
+ {:ok, nil}
+ end
+
+ def handle_info({:irc, :text, m = %IRC.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 = %IRC.Message{account: account, text: "auth"}}, state) do
+ spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
+ predicates = :dets.select(IRC.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 = %IRC.Message{account: account, text: "whoami"}}, state) do
+ users = for user <- IRC.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 = %IRC.Message{account: account, text: "account"}}, state) do
+ account = IRC.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
+
+ def handle_info({:irc, :text, m = %IRC.Message{sender: sender, text: "auth"<>_}}, state) do
+ #account = IRC.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 = %IRC.Message{account: account, text: "set-name "<>name}}, state) do
+ IRC.Account.update_account_name(account, name)
+ m.replyfun.("Name changed: #{name}")
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :text, m = %IRC.Message{text: "disable-sms"}}, state) do
+ if IRC.Account.get_meta(m.account, "sms-number") do
+ IRC.Account.delete_meta(m.account, "sms-number")
+ m.replfyun.("SMS disabled.")
+ else
+ m.replyfun.("SMS already disabled.")
+ end
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :text, m = %IRC.Message{text: "web"}}, state) do
+ auth_url = Untappd.auth_url()
+ login_url = Nola.AuthToken.new_url(m.account.id, nil)
+ m.replyfun.("-> " <> login_url)
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :text, m = %IRC.Message{text: "enable-sms"}}, state) do
+ code = String.downcase(EntropyString.small_id())
+ IRC.Account.put_meta(m.account, "sms-validation-code", code)
+ IRC.Account.put_meta(m.account, "sms-validation-target", m.network)
+ number = Nola.IRC.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 = %IRC.Message{text: "enable-telegram"}}, state) do
+ code = String.downcase(EntropyString.small_id())
+ IRC.Account.delete_meta(m.account, "telegram-id")
+ IRC.Account.put_meta(m.account, "telegram-validation-code", code)
+ IRC.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 = %IRC.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 = %IRC.Message{text: "getmeta"<>_}}, state) do
+ result = case String.split(m.text, " ") do
+ ["getmeta"] ->
+ for {k, v} <- IRC.Account.get_all_meta(m.account) do
+ case k do
+ "u:"<>key -> "(user) #{key}: #{v}"
+ key -> "#{key}: #{v}"
+ end
+ end
+ ["getmeta", key] ->
+ value = IRC.Account.get_meta(m.account, key)
+ 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 = %IRC.Message{text: "setusermeta"<>_}}, state) do
+ result = case String.split(m.text, " ") do
+ ["setusermeta", key, value] ->
+ IRC.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
+
+ defp join_account(m, id, token) do
+ old_account = IRC.Account.lookup(m.sender)
+ new_account = IRC.Account.get(id)
+ if new_account && token == new_account.token do
+ case IRC.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/nola_plugins/alcolog_plugin.ex b/lib/nola_plugins/alcoolog.ex
similarity index 98%
rename from lib/nola_plugins/alcolog_plugin.ex
rename to lib/nola_plugins/alcoolog.ex
index 145e4fc..7f44ca2 100644
--- a/lib/nola_plugins/alcolog_plugin.ex
+++ b/lib/nola_plugins/alcoolog.ex
@@ -1,1229 +1,1229 @@
-defmodule Nola.IRC.AlcoologPlugin do
+defmodule Nola.Plugins.Alcoolog do
require Logger
@moduledoc """
# [alcoolog]({{context_path}}/alcoolog)
* **!santai `<cl | (calc)>` `<degrés d'alcool> [annotation]`**: enregistre un nouveau verre de `montant` d'une boisson à `degrés d'alcool`.
* **!santai `<cl | (calc)>` `<beer name>`**: enregistre un nouveau verre de `cl` de la bière `beer name`, et checkin sur Untappd.com.
* **!moar `[cl]` : enregistre un verre équivalent au dernier !santai.
* **-santai**: annule la dernière entrée d'alcoolisme.
* **.alcoolisme**: état du channel en temps réel.
* **.alcoolisme `<semaine | Xj>`**: points par jour, sur X j.
* **!alcoolisme `[pseudo]`**: affiche les points d'alcoolisme.
* **!alcoolisme `[pseudo]` `<semaine | Xj>`**: affiche les points d'alcoolisme par jour sur X j.
* **+alcoolisme `<h|f>` `<poids en kg>` `[facteur de perte en mg/l (10, 15, 20, 25)]`**: Configure votre profil d'alcoolisme.
* **.sobre**: affiche quand la sobriété frappera sur le chan.
* **!sobre `[pseudo]`**: affiche quand la sobriété frappera pour `[pseudo]`.
* **!sobrepour `<date>`**: affiche tu pourras être sobre pour `<date>`, et si oui, combien de volumes d'alcool peuvent encore être consommés.
* **!alcoolog**: ([voir]({{context_path}}/alcoolog)) lien pour voir l'état/statistiques et historique de l'alcoolémie du channel.
* **!alcool `<cl>` `<degrés>`**: donne le nombre d'unités d'alcool dans `<cl>` à `<degrés>°`.
* **!soif**: c'est quand l'apéro ?
1 point = 1 volume d'alcool.
Annotation: champ libre!
---
## `!txt`s
* status utilisateur: `alcoolog.user_(sober|legal|legalhigh|high|toohigh|sick)(|_rising)`
* mauvaises boissons: `alcoolog.drink_(negative|zero|negative)`
* santo: `alcoolog.santo`
* santai: `alcoolog.santai`
* plus gros, moins gros: `alcoolog.(fatter|thinner)`
"""
def irc_doc, do: @moduledoc
def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
# tuple dets: {nick, date, volumes, current_alcohol_level, nom, commentaire}
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
# tuple meta dets: {nick, map}
# %{:weight => float, :sex => true(h),false(f)}
@pubsub ~w(account)
@pubsub_triggers ~w(santai moar again bis santo santeau alcoolog sobre sobrepour soif alcoolisme alcool)
@default_user_meta %{weight: 77.4, sex: true, loss_factor: 15}
def data_state() do
dets_filename = (Nola.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
dets_meta_filename = (Nola.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
%{dets: dets_filename, meta: dets_meta_filename, ets: __MODULE__.ETS}
end
def init(_) do
triggers = for(t <- @pubsub_triggers, do: "trigger:"<>t)
for sub <- @pubsub ++ triggers do
{:ok, _} = Registry.register(IRC.PubSub, sub, plugin: __MODULE__)
end
dets_filename = (Nola.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
ets = :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
dets_meta_filename = (Nola.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
{:ok, meta} = :dets.open_file(dets_meta_filename, [{:type,:set}])
traverse_fun = fn(obj, dets) ->
case obj do
object = {nick, naive = %NaiveDateTime{}, volumes, active, cl, deg, name, comment} ->
date = naive
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_unix()
new = {nick, date, volumes, active, cl, deg, name, comment, Map.new()}
:dets.delete_object(dets, object)
:dets.insert(dets, new)
:ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, Map.new()})
dets
object = {nick, naive = %NaiveDateTime{}, volumes, active, cl, deg, name, comment, meta} ->
date = naive
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_unix()
new = {nick, date, volumes, active, cl, deg, name, comment, Map.new()}
:dets.delete_object(dets, object)
:dets.insert(dets, new)
:ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, Map.new()})
dets
object = {nick, date, volumes, active, cl, deg, name, comment, meta} ->
:ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, meta})
dets
_ ->
dets
end
end
:dets.foldl(traverse_fun, dets, dets)
:dets.sync(dets)
state = %{dets: dets, meta: meta, ets: ets}
{:ok, state}
end
@eau ["santo", "santeau"]
def handle_info({:irc, :trigger, santeau, m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) when santeau in @eau do
- Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.santo")
+ Nola.Plugins.Txt.reply_random(m, "alcoolog.santo")
{:noreply, state}
end
def handle_info({:irc, :trigger, "soif", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do
now = DateTime.utc_now()
|> Timex.Timezone.convert("Europe/Paris")
apero = format_duration_from_now(%DateTime{now | hour: 18, minute: 0, second: 0}, false)
day_of_week = Date.day_of_week(now)
{txt, apero?} = cond do
now.hour >= 0 && now.hour < 6 ->
{["apéro tardif ? Je dis OUI ! SANTAI !"], true}
now.hour >= 6 && now.hour < 12 ->
if day_of_week >= 6 do
{["de l'alcool pour le petit dej ? le week-end, pas de problème !"], true}
else
{["C'est quand même un peu tôt non ? Prochain apéro #{apero}"], false}
end
now.hour >= 12 && (now.hour < 14) ->
{["oui! c'est l'apéro de midi! (et apéro #{apero})",
"tu peux attendre #{apero} ou y aller, il est midi !"
], true}
now.hour == 17 ->
{[
"ÇA APPROCHE !!! Apéro #{apero}",
"BIENTÔT !!! Apéro #{apero}",
"achetez vite les teilles, apéro dans #{apero}!",
"préparez les teilles, apéro dans #{apero}!"
], false}
now.hour >= 14 && now.hour < 18 ->
weekend = if day_of_week >= 6 do
" ... ou maintenant en fait, c'est le week-end!"
else
""
end
{["tiens bon! apéro #{apero}#{weekend}",
"courage... apéro dans #{apero}#{weekend}",
"pas encore :'( apéro dans #{apero}#{weekend}"
], false}
true ->
{[
"C'EST L'HEURE DE L'APÉRO !!! SANTAIIIIIIIIIIII !!!!"
], true}
end
txt = txt
|> Enum.shuffle()
|> Enum.random()
m.replyfun.(txt)
stats = get_full_statistics(state, m.account.id)
if !apero? && stats.active > 0.1 do
m.replyfun.("(... ou continue en fait, je suis pas ta mère !)")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "sobrepour", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
args = Enum.join(args, " ")
{:ok, now} = DateTime.now("Europe/Paris", Tzdata.TimeZoneDatabase)
time = case args do
"demain " <> time ->
{h, m} = case String.split(time, [":", "h"]) do
[hour, ""] ->
IO.puts ("h #{inspect hour}")
{h, _} = Integer.parse(hour)
{h, 0}
[hour, min] when min != "" ->
{h, _} = Integer.parse(hour)
{m, _} = Integer.parse(min)
{h, m}
[hour] ->
IO.puts ("h #{inspect hour}")
{h, _} = Integer.parse(hour)
{h, 0}
_ -> {0, 0}
end
secs = ((60*60)*24)
day = DateTime.add(now, secs, :second, Tzdata.TimeZoneDatabase)
%DateTime{day | hour: h, minute: m, second: 0}
"après demain " <> time ->
secs = 2*((60*60)*24)
DateTime.add(now, secs, :second, Tzdata.TimeZoneDatabase)
datetime ->
case Timex.Parse.DateTime.Parser.parse(datetime, "{}") do
{:ok, dt} -> dt
_ -> nil
end
end
if time do
meta = get_user_meta(state, m.account.id)
stats = get_full_statistics(state, m.account.id)
duration = round(DateTime.diff(time, now)/60.0)
IO.puts "diff #{inspect duration} sober in #{inspect stats.sober_in}"
if duration < stats.sober_in do
int = stats.sober_in - duration
m.replyfun.("désolé, aucune chance! tu seras sobre #{format_minute_duration(int)} après!")
else
remaining = duration - stats.sober_in
if remaining < 30 do
m.replyfun.("moins de 30 minutes de sobriété, c'est impossible de boire plus")
else
loss_per_minute = ((meta.loss_factor/100)/60)
remaining_gl = (remaining-30)*loss_per_minute
m.replyfun.("marge de boisson: #{inspect remaining} minutes, #{remaining_gl} g/l")
end
end
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolog", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do
{:ok, token} = Nola.Token.new({:alcoolog, :index, m.sender.network, m.channel})
url = NolaWeb.Router.Helpers.alcoolog_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel), token)
m.replyfun.("-> #{url}")
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolog", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :bang}}}, state) do
url = NolaWeb.Router.Helpers.alcoolog_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel))
m.replyfun.("-> #{url}")
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcool", m = %IRC.Message{trigger: %IRC.Trigger{args: args = [cl, deg], type: :bang}}}, state) do
{cl, _} = Util.float_paparse(cl)
{deg, _} = Util.float_paparse(deg)
points = Alcool.units(cl, deg)
meta = get_user_meta(state, m.account.id)
k = if meta.sex, do: 0.7, else: 0.6
weight = meta.weight
gl = (10*points)/(k*weight)
duration = round(gl/((meta.loss_factor/100)/60))+30
sober_in_s = if duration > 0 do
duration = Timex.Duration.from_minutes(duration)
Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
else
""
end
m.replyfun.("Il y a #{Float.round(points+0.0, 4)} unités d'alcool dans #{cl}cl à #{deg}° (#{Float.round(gl + 0.0, 4)} g/l, #{sober_in_s})")
{:noreply, state}
end
def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: [cl, deg | comment], type: :bang}}}, state) do
santai(m, state, cl, deg, comment)
{:noreply, state}
end
@moar [
"{{message.sender.nick}}: la même donc ?",
"{{message.sender.nick}}: et voilà la petite sœur !"
]
def handle_info({:irc, :trigger, "bis", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
handle_info({:irc, :trigger, "moar", m}, state)
end
def handle_info({:irc, :trigger, "again", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
handle_info({:irc, :trigger, "moar", m}, state)
end
def handle_info({:irc, :trigger, "moar", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
case get_statistics_for_nick(state, m.account.id) do
{_, obj = {_, _date, _points, _active, cl, deg, _name, comment, _meta}} ->
cl = case args do
[cls] ->
case Util.float_paparse(cls) do
{cl, _} -> cl
_ -> cl
end
_ -> cl
end
moar = @moar |> Enum.shuffle() |> Enum.random() |> Tmpl.render(m) |> m.replyfun.()
santai(m, state, cl, deg, comment, auto_set: true)
{_, obj = {_, date, points, _last_active, type, descr}} ->
case Regex.named_captures(~r/^(?<cl>\d+[.]\d+)cl\s+(?<deg>\d+[.]\d+)°$/, type) do
nil -> m.replyfun.("suce")
u ->
moar = @moar |> Enum.shuffle() |> Enum.random() |> Tmpl.render(m) |> m.replyfun.()
santai(m, state, u["cl"], u["deg"], descr, auto_set: true)
end
_ -> nil
end
{:noreply, state}
end
defp santai(m, state, cl, deg, comment, options \\ []) do
comment = cond do
comment == [] -> nil
is_binary(comment) -> comment
comment == nil -> nil
true -> Enum.join(comment, " ")
end
{cl, cl_extra} = case {Util.float_paparse(cl), cl} do
{{cl, extra}, _} -> {cl, extra}
{:error, "("<>_} ->
try do
{:ok, result} = Abacus.eval(cl)
{result, nil}
rescue
_ -> {nil, "cl: invalid calc expression"}
end
{:error, _} -> {nil, "cl: invalid value"}
end
{deg, comment, auto_set, beer_id} = case Util.float_paparse(deg) do
{deg, _} -> {deg, comment, Keyword.get(options, :auto_set, false), nil}
:error ->
beername = if(comment, do: "#{deg} #{comment}", else: deg)
case Untappd.search_beer(beername, limit: 1) do
{:ok, %{"response" => %{"beers" => %{"count" => count, "items" => [%{"beer" => beer, "brewery" => brewery} | _]}}}} ->
{Map.get(beer, "beer_abv"), "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")}", true, Map.get(beer, "bid")}
_ ->
{deg, "could not find beer", false, nil}
end
end
cond do
cl == nil -> m.replyfun.(cl_extra)
deg == nil -> m.replyfun.(comment)
- cl >= 500 || deg >= 100 -> Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.drink_toohuge")
- cl == 0 || deg == 0 -> Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.drink_zero")
- cl < 0 || deg < 0 -> Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.drink_negative")
+ 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)
user_meta = get_user_meta(state, m.account.id)
name = "#{cl}cl #{deg}°"
old_stats = get_full_statistics(state, m.account.id)
meta = %{}
meta = Map.put(meta, "timestamp", now)
meta = Map.put(meta, "weight", user_meta.weight)
meta = Map.put(meta, "sex", user_meta.sex)
:ok = :dets.insert(state.dets, {m.account.id, now, points, if(old_stats, do: old_stats.active, else: 0), cl, deg, name, comment, meta})
true = :ets.insert(state.ets, {{m.account.id, now}, points, if(old_stats, do: old_stats.active, else: 0),cl, deg, name, comment, meta})
#sante = @santai |> Enum.map(fn(s) -> String.trim(String.upcase(s)) end) |> Enum.shuffle() |> Enum.random()
- sante = Nola.IRC.TxtPlugin.random("alcoolog.santai")
+ 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)
stats = get_full_statistics(state, m.account.id)
sober_add = if old_stats && Map.get(old_stats || %{}, :sober_in) do
mins = round(stats.sober_in - old_stats.sober_in)
" [+#{mins}m]"
else
""
end
nonow = DateTime.utc_now()
sober = nonow |> DateTime.add(round(stats.sober_in*60), :second)
|> Timex.Timezone.convert("Europe/Paris")
at = if nonow.day == sober.day do
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
detail
else
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
detail
end
up = if stats.active_drinks > 1 do
" " <> Enum.join(for(_ <- 1..stats.active_drinks, do: "▲")) <> ""
else
""
end
since_str = if stats.since && stats.since_min > 180 do
"(depuis: #{stats.since_s}) "
else
""
end
msg = fn(nick, extra) ->
"#{sante} #{nick} #{extra}#{up} #{format_points(points)} @#{stats.active}g/l [+#{peak} g/l]"
<> " (15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) #{since_str}(sobriété #{at} (dans #{stats.sober_in_s})#{sober_add}) !"
<> " (aujourd'hui #{stats.daily_volumes} points - #{stats.daily_gl} g/l)"
end
meta = if beer_id do
Map.put(meta, "untappd:beer_id", beer_id)
else
meta
end
if beer_id do
spawn(fn() ->
case Untappd.maybe_checkin(m.account, beer_id) do
{:ok, body} ->
badges = get_in(body, ["badges", "items"])
if badges != [] do
badges_s = Enum.map(badges, fn(badge) -> Map.get(badge, "badge_name") end)
|> Enum.filter(fn(b) -> b end)
|> Enum.intersperse(", ")
|> Enum.join("")
badge = if(length(badges) > 1, do: "badges", else: "badge")
m.replyfun.("\\O/ Unlocked untappd #{badge}: #{badges_s}")
end
:ok
{:error, {:http_error, error}} when is_integer(error) -> m.replyfun.("Checkin to Untappd failed: #{to_string(error)}")
{:error, {:http_error, error}} -> m.replyfun.("Checkin to Untappd failed: #{inspect error}")
_ -> :error
end
end)
end
local_extra = if auto_set do
if comment do
" #{comment} (#{cl}cl @ #{deg}°)"
else
"#{cl}cl @ #{deg}°"
end
else
""
end
m.replyfun.(msg.(m.sender.nick, local_extra))
notify = IRC.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
for {net, chan} <- notify do
user = IRC.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
extra = " " <> present_type(name, comment) <> ""
IRC.Connection.broadcast_message(net, chan, msg.(nick, extra))
end
miss = cond do
points <= 0.6 -> :small
stats.active30m >= 2.9 && stats.active30m < 3 -> :miss3
stats.active30m >= 1.9 && stats.active30m < 2 -> :miss2
stats.active30m >= 0.9 && stats.active30m < 1 -> :miss1
stats.active30m >= 0.45 && stats.active30m < 0.5 -> :miss05
stats.active30m >= 0.20 && stats.active30m < 0.20 -> :miss025
stats.active30m >= 3 && stats.active1h < 3.15 -> :small3
stats.active30m >= 2 && stats.active1h < 2.15 -> :small2
stats.active30m >= 1.5 && stats.active1h < 1.5 -> :small15
stats.active30m >= 1 && stats.active1h < 1.15 -> :small1
stats.active30m >= 0.5 && stats.active1h <= 0.51 -> :small05
stats.active30m >= 0.25 && stats.active30m <= 0.255 -> :small025
true -> nil
end
if miss do
- miss = Nola.IRC.TxtPlugin.random("alcoolog.#{to_string(miss)}")
+ miss = Nola.Plugins.Txt.random("alcoolog.#{to_string(miss)}")
if miss do
for {net, chan} <- IRC.Membership.notify_channels(m.account) do
user = IRC.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
IRC.Connection.broadcast_message(net, chan, "#{nick}: #{miss}")
end
end
end
end
end
def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do
m.replyfun.("!santai <cl> <degrés> [commentaire]")
{:noreply, state}
end
def get_all_stats() do
IRC.Account.all_accounts()
|> Enum.map(fn(account) -> {account.id, get_full_statistics(account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
|> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
end
def get_channel_statistics(account, network, nil) do
IRC.Membership.expanded_members_or_friends(account, network, nil)
|> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
|> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
end
def get_channel_statistics(_, network, channel), do: get_channel_statistics(network, channel)
def get_channel_statistics(network, channel) do
IRC.Membership.expanded_members(network, channel)
|> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
|> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
end
@spec since() :: %{IRC.Account.id() => DateTime.t()}
@doc "Returns the last time the user was at 0 g/l"
def since() do
:ets.foldr(fn({{acct, timestamp_or_date}, _vol, current, _cl, _deg, _name, _comment, _m}, acc) ->
if !Map.get(acc, acct) && current == 0 do
date = Util.to_date_time(timestamp_or_date)
Map.put(acc, acct, date)
else
acc
end
end, %{}, __MODULE__.ETS)
end
def get_full_statistics(nick) do
get_full_statistics(data_state(), nick)
end
defp get_full_statistics(state, nick) do
case get_statistics_for_nick(state, nick) do
{count, {_, last_at, last_points, last_active, last_cl, last_deg, last_type, last_descr, _meta}} ->
{active, active_drinks} = current_alcohol_level(state, nick)
{_, m30} = alcohol_level_rising(state, nick)
{rising, m15} = alcohol_level_rising(state, nick, 15)
{_, m5} = alcohol_level_rising(state, nick, 5)
{_, h1} = alcohol_level_rising(state, nick, 60)
trend = if rising do
"▲"
else
"▼"
end
user_state = cond do
active <= 0.0 -> :sober
active <= 0.25 -> :low
active <= 0.50 -> :legal
active <= 1.0 -> :legalhigh
active <= 2.5 -> :high
active < 3 -> :toohigh
true -> :sick
end
rising_file_key = if rising, do: "_rising", else: ""
txt_file = "alcoolog." <> "user_" <> to_string(user_state) <> rising_file_key
- user_status = Nola.IRC.TxtPlugin.random(txt_file)
+ 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
since_diff = if since, do: Timex.diff(DateTime.utc_now(), since, :minutes)
since_duration = if since, do: Timex.Duration.from_minutes(since_diff)
since_s = if since, do: Timex.Format.Duration.Formatter.lformat(since_duration, "fr", :humanized)
{total_volumes, total_gl} = user_stats(state, nick)
%{active: active, last_at: last_at, last_cl: last_cl, last_deg: last_deg, last_points: last_points, last_type: last_type, last_descr: last_descr,
trend_symbol: trend,
active5m: m5, active15m: m15, active30m: m30, active1h: h1,
rising: rising,
active_drinks: active_drinks,
user_status: user_status,
daily_gl: total_gl, daily_volumes: total_volumes,
sober_in: minutes_til_sober, sober_in_s: sober_in_s,
since: since, since_min: since_diff, since_s: since_s,
}
_ ->
nil
end
end
def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :dot}}}, state) do
nicks = IRC.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
|> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(state, account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && status.sober_in && status.sober_in > 0 end)
|> Enum.sort_by(fn({_, status}) -> status.sober_in end, &</2)
|> Enum.map(fn({nick, stats}) ->
now = DateTime.utc_now()
sober = now |> DateTime.add(round(stats.sober_in*60), :second)
|> Timex.Timezone.convert("Europe/Paris")
at = if now.day == sober.day do
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
detail
else
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
detail
end
"#{nick} sobre #{at} (dans #{stats.sober_in_s})"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
|> (fn(line) ->
case line do
"" -> "tout le monde est sobre......."
line -> line
end
end).()
|> m.replyfun.()
{:noreply, state}
end
def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
account = case args do
[nick] -> IRC.Account.find_always_by_nick(m.network, m.channel, nick)
[] -> m.account
end
if account do
user = IRC.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
stats = get_full_statistics(state, account.id)
if stats && stats.sober_in > 0 do
now = DateTime.utc_now()
sober = now |> DateTime.add(round(stats.sober_in*60), :second)
|> Timex.Timezone.convert("Europe/Paris")
at = if now.day == sober.day do
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
detail
else
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
detail
end
m.replyfun.("#{nick} sera sobre #{at} (dans #{stats.sober_in_s})!")
else
m.replyfun.("#{nick} est déjà sobre. aidez le !")
end
else
m.replyfun.("inconnu")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :dot}}}, state) do
nicks = IRC.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
|> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(state, account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
|> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
|> Enum.map(fn({nick, status}) ->
trend_symbol = if status.active_drinks > 1 do
Enum.join(for(_ <- 1..status.active_drinks, do: status.trend_symbol))
else
status.trend_symbol
end
since_str = if status.since_min > 180 do
"depuis: #{status.since_s} | "
else
""
end
"#{nick} #{status.user_status} #{trend_symbol} #{Float.round(status.active, 4)} g/l [#{since_str}sobre dans: #{status.sober_in_s}]"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
msg = if nicks == "" do
"wtf?!?! personne n'a bu!"
else
nicks
end
m.replyfun.(msg)
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [time], type: :dot}}}, state) do
time = case time do
"semaine" -> 7
string ->
case Integer.parse(string) do
{time, "j"} -> time
{time, "J"} -> time
_ -> nil
end
end
if time do
aday = time*((24 * 60)*60)
now = DateTime.utc_now()
before = now
|> DateTime.add(-aday, :second)
|> DateTime.to_unix(:millisecond)
over_time_stats(before, time, m, state)
else
m.replyfun.(".alcooolisme semaine|Xj")
end
{:noreply, state}
end
def user_over_time(account, count) do
user_over_time(data_state(), account, count)
end
def user_over_time(state, account, count) do
delay = count*((24 * 60)*60)
now = DateTime.utc_now()
before = DateTime.utc_now()
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
|> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
|> DateTime.to_unix(:millisecond)
#[
# {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
# [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]}
#]
match = [{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
[{:andalso, {:>, :"$2", {:const, before}}, {:==, :"$1", {:const, account.id}}}], [:"$_"]}
]
:ets.select(state.ets, match)
|> Enum.reduce(Map.new, fn({{_, ts}, vol, _, _, _, _, _, _}, acc) ->
date = DateTime.from_unix!(ts, :millisecond)
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
date = if date.hour <= 8 do
DateTime.add(date, -(60*(60*(date.hour+1))), :second, Tzdata.TimeZoneDatabase)
else
date
end
|> DateTime.to_date()
Map.put(acc, date, Map.get(acc, date, 0) + vol)
end)
end
def user_over_time_gl(account, count) do
state = data_state()
meta = get_user_meta(state, account.id)
delay = count*((24 * 60)*60)
now = DateTime.utc_now()
before = DateTime.utc_now()
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
|> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
|> DateTime.to_unix(:millisecond)
#[
# {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
# [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]}
#]
match = [{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
[{:andalso, {:>, :"$2", {:const, before}}, {:==, :"$1", {:const, account.id}}}], [:"$_"]}
]
:ets.select(state.ets, match)
|> Enum.reduce(Map.new, fn({{_, ts}, vol, _, _, _, _, _, _}, acc) ->
date = DateTime.from_unix!(ts, :millisecond)
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
date = if date.hour <= 8 do
DateTime.add(date, -(60*(60*(date.hour+1))), :second, Tzdata.TimeZoneDatabase)
else
date
end
|> DateTime.to_date()
weight = meta.weight
k = if meta.sex, do: 0.7, else: 0.6
gl = (10*vol)/(k*weight)
Map.put(acc, date, Map.get(acc, date, 0) + gl)
end)
end
defp over_time_stats(before, j, m, state) do
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _, _, _, _}) when date > before -> obj end)
match = [{{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_},
[{:>, :"$1", {:const, before}}], [:"$_"]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
members = IRC.Membership.members_or_friends(m.account, m.network, m.channel)
drinks = :ets.select(state.ets, match)
|> Enum.filter(fn({{account, _}, _, _, _, _, _, _, _}) -> Enum.member?(members, account) end)
|> Enum.sort_by(fn({{_, ts}, _, _, _, _, _, _, _}) -> ts end, &>/2)
top = Enum.reduce(drinks, %{}, fn({{nick, _}, vol, _, _, _, _, _, _}, acc) ->
all = Map.get(acc, nick, 0)
Map.put(acc, nick, all + vol)
end)
|> Enum.sort_by(fn({_nick, count}) -> count end, &>/2)
|> Enum.map(fn({nick, count}) ->
account = IRC.Account.get(nick)
user = IRC.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
"#{nick}: #{Float.round(count, 4)}"
end)
|> Enum.intersperse(", ")
m.replyfun.("sur #{j} jours: #{top}")
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do
meta = get_user_meta(state, m.account.id)
hf = if meta.sex, do: "h", else: "f"
m.replyfun.("+alcoolisme sexe: #{hf} poids: #{meta.weight} facteur de perte: #{meta.loss_factor}")
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [h, weight | rest], type: :plus}}}, state) do
h = case h do
"h" -> true
"f" -> false
_ -> nil
end
weight = case Util.float_paparse(weight) do
{weight, _} -> weight
_ -> nil
end
{factor} = case rest do
[factor] ->
case Util.float_paparse(factor) do
{float, _} -> {float}
_ -> {@default_user_meta.loss_factor}
end
_ -> {@default_user_meta.loss_factor}
end
if h == nil || weight == nil do
m.replyfun.("paramètres invalides")
else
old_meta = get_user_meta(state, m.account.id)
meta = Map.merge(@default_user_meta, %{sex: h, weight: weight, loss_factor: factor})
put_user_meta(state, m.account.id, meta)
cond do
old_meta.weight < meta.weight ->
- Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.fatter")
+ Nola.Plugins.Txt.reply_random(m, "alcoolog.fatter")
old_meta.weight == meta.weight ->
m.replyfun.("aucun changement!")
true ->
- Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.thinner")
+ Nola.Plugins.Txt.reply_random(m, "alcoolog.thinner")
end
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :minus}}}, state) do
case get_statistics_for_nick(state, m.account.id) do
{_, obj = {_, date, points, _last_active, _cl, _deg, type, descr, _meta}} ->
:dets.delete_object(state.dets, obj)
:ets.delete(state.ets, {m.account.id, date})
m.replyfun.("supprimé: #{m.sender.nick} #{points} #{type} #{descr}")
- Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.delete")
+ Nola.Plugins.Txt.reply_random(m, "alcoolog.delete")
notify = IRC.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
for {net, chan} <- notify do
user = IRC.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
IRC.Connection.broadcast_message(net, chan, "#{nick} -santai #{points} #{type} #{descr}")
end
{:noreply, state}
_ ->
{:noreply, state}
end
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
{account, duration} = case args do
[nick | rest] -> {IRC.Account.find_always_by_nick(m.network, m.channel, nick), rest}
[] -> {m.account, []}
end
if account do
duration = case duration do
["semaine"] -> 7
[j] ->
case Integer.parse(j) do
{j, "j"} -> j
_ -> nil
end
_ -> nil
end
user = IRC.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
if duration do
if duration > 90 do
m.replyfun.("trop gros, ça rentrera pas")
else
# duration stats
stats = user_over_time(state, account, duration)
|> Enum.sort_by(fn({k,_v}) -> k end, {:asc, Date})
|> Enum.map(fn({date, count}) ->
"#{date.day}: #{Float.round(count, 2)}"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
if stats == "" do
m.replyfun.("alcoolisme a zéro sur #{duration}j :/")
else
m.replyfun.("alcoolisme de #{nick}, #{duration} derniers jours: #{stats}")
end
end
else
if stats = get_full_statistics(state, account.id) do
trend_symbol = if stats.active_drinks > 1 do
Enum.join(for(_ <- 1..stats.active_drinks, do: stats.trend_symbol))
else
stats.trend_symbol
end
# TODO: Lookup nick for account_id
msg = "#{nick} #{stats.user_status} "
<> (if stats.active > 0 || stats.active15m > 0 || stats.active30m > 0 || stats.active1h > 0, do: ": #{trend_symbol} #{Float.round(stats.active, 4)}g/l ", else: "")
<> (if stats.active30m > 0 || stats.active1h > 0, do: "(15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) ", else: "")
<> (if stats.sober_in > 0, do: "— Sobre dans #{stats.sober_in_s} ", else: "")
<> (if stats.since && stats.since_min > 180, do: "— Paitai depuis #{stats.since_s} ", else: "")
<> "— Dernier verre: #{present_type(stats.last_type, stats.last_descr)} [#{Float.round(stats.last_points+0.0, 4)}] "
<> "#{format_duration_from_now(stats.last_at)} "
<> (if stats.daily_volumes > 0, do: "— Aujourd'hui: #{stats.daily_volumes} #{stats.daily_gl}g/l", else: "")
m.replyfun.(msg)
else
m.replyfun.("honteux mais #{nick} n'a pas l'air alcoolique du tout. /kick")
end
end
else
m.replyfun.("je ne connais pas cet utilisateur")
end
{:noreply, state}
end
# Account merge
def handle_info({:account_change, old_id, new_id}, state) do
spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
Logger.debug("alcolog/account_change:: merging #{old_id} -> #{new_id}")
rename_object_owner(table, state.ets, obj, old_id, new_id)
end)
case :dets.lookup(state.meta, {:meta, old_id}) do
[{_, meta}] ->
:dets.delete(state.meta, {:meta, old_id})
:dets.insert(state.meta, {{:meta, new_id}, meta})
_ ->
:ok
end
{:noreply, state}
end
def terminate(_, state) do
for dets <- [state.dets, state.meta] do
:dets.sync(dets)
:dets.close(dets)
end
end
defp rename_object_owner(table, ets, object = {old_id, date, volume, current, cl, deg, name, comment, meta}, old_id, new_id) do
:dets.delete_object(table, object)
:ets.delete(ets, {old_id, date})
:dets.insert(table, {new_id, date, volume, current, cl, deg, name, comment, meta})
:ets.insert(ets, {{new_id, date}, volume, current, cl, deg, name, comment, meta})
end
# Account: move from nick to account id
def handle_info({:accounts, accounts}, state) do
#for x={:account, _, _, _, _} <- accounts, do: handle_info(x, state)
#{:noreply, state}
mapping = Enum.reduce(accounts, Map.new, fn({:account, _net, _chan, nick, account_id}, acc) ->
Map.put(acc, String.downcase(nick), account_id)
end)
spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [], [:"$_"]}]
Logger.debug("accounts:: mappings #{inspect mapping}")
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj = {nick, _date, _vol, _cur, _cl, _deg, _name, _comment, _meta}) ->
#Logger.debug("accounts:: item #{inspect(obj)}")
if new_id = Map.get(mapping, nick) do
Logger.debug("alcolog/accounts:: merging #{nick} -> #{new_id}")
rename_object_owner(table, state.ets, obj, nick, new_id)
end
end)
{:noreply, state}
end
def handle_info({:account, _net, _chan, nick, account_id}, state) do
nick = String.downcase(nick)
spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
Logger.debug("alcoolog/account:: merging #{nick} -> #{account_id}")
rename_object_owner(table, state.ets, obj, nick, account_id)
end)
case :dets.lookup(state.meta, {:meta, nick}) do
[{_, meta}] ->
:dets.delete(state.meta, {:meta, nick})
:dets.insert(state.meta, {{:meta, account_id}, meta})
_ ->
:ok
end
{:noreply, state}
end
def handle_info(t, state) do
- Logger.debug("AlcoologPlugin: unhandled info #{inspect t}")
+ Logger.debug("#{__MODULE__}: unhandled info #{inspect t}")
{:noreply, state}
end
def nick_history(account) do
spec = [
{{{:"$1", :_}, :_, :_, :_, :_, :_, :_, :_},
[{:==, :"$1", {:const, account.id}}],
[:"$_"]}
]
:ets.select(data_state().ets, spec)
end
defp get_statistics_for_nick(state, account_id) do
qvc = :dets.lookup(state.dets, account_id)
|> Enum.sort_by(fn({_, ts, _, _, _, _, _, _, _}) -> ts end, &</2)
count = Enum.reduce(qvc, 0, fn({_nick, _ts, points, _active, _cl, _deg, _type, _descr, _meta}, acc) -> acc + (points||0) end)
last = List.last(qvc) || nil
{count, last}
end
def present_type(type, descr) when descr in [nil, ""], do: "#{type}"
def present_type(type, description), do: "#{type} (#{description})"
def format_points(int) when is_integer(int) and int > 0 do
"+#{Integer.to_string(int)}"
end
def format_points(int) when is_integer(int) and int < 0 do
Integer.to_string(int)
end
def format_points(int) when is_float(int) and int > 0 do
"+#{Float.to_string(Float.round(int,4))}"
end
def format_points(int) when is_float(int) and int < 0 do
Float.to_string(Float.round(int,4))
end
def format_points(0), do: "0"
def format_points(0.0), do: "0"
defp format_relative_timestamp(timestamp) do
alias Timex.Format.DateTime.Formatters
alias Timex.Timezone
date = timestamp
|> DateTime.from_unix!(:millisecond)
|> Timezone.convert("Europe/Paris")
{:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
{:ok, detail} = Formatters.Default.lformat(date, " ({h24}:{m})", "fr")
relative <> detail
end
defp put_user_meta(state, account_id, meta) do
:dets.insert(state.meta, {{:meta, account_id}, meta})
:ok
end
defp get_user_meta(%{meta: meta}, account_id) do
case :dets.lookup(meta, {:meta, account_id}) do
[{{:meta, _}, meta}] ->
Map.merge(@default_user_meta, meta)
_ ->
@default_user_meta
end
end
# Calcul g/l actuel:
# 1. load user meta
# 2. foldr ets
# for each object
# get_current_alcohol
# ((object g/l) - 0,15/l/60)* minutes_since_drink
# if minutes_since_drink < 10, reduce g/l (?!)
# acc + current_alcohol
# stop folding when ?
#
def user_stats(account) do
user_stats(data_state(), account.id)
end
defp user_stats(state = %{ets: ets}, account_id) do
meta = get_user_meta(state, account_id)
aday = (10 * 60)*60
now = DateTime.utc_now()
before = now
|> DateTime.add(-aday, :second)
|> DateTime.to_unix(:millisecond)
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
match = [
{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
[
{:>, :"$2", {:const, before}},
{:"=:=", {:const, account_id}, :"$1"}
], [:"$_"]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
drinks = :ets.select(ets, match)
# {date, single_peak}
total_volume = Enum.reduce(drinks, 0.0, fn({{_, date}, volume, _, _, _, _, _, _}, acc) ->
acc + volume
end)
k = if meta.sex, do: 0.7, else: 0.6
weight = meta.weight
gl = (10*total_volume)/(k*weight)
{Float.round(total_volume + 0.0, 4), Float.round(gl + 0.0, 4)}
end
defp alcohol_level_rising(state, account_id, minutes \\ 30) do
{now, _} = current_alcohol_level(state, account_id)
soon_date = DateTime.utc_now
|> DateTime.add(minutes*60, :second)
{soon, _} = current_alcohol_level(state, account_id, soon_date)
soon = cond do
soon < 0 -> 0.0
true -> soon
end
#IO.puts "soon #{soon_date} - #{inspect soon} #{inspect now}"
{soon > now, Float.round(soon+0.0, 4)}
end
defp current_alcohol_level(state = %{ets: ets}, account_id, now \\ nil) do
meta = get_user_meta(state, account_id)
aday = ((24*7) * 60)*60
now = if now do
now
else
DateTime.utc_now()
end
before = now
|> DateTime.add(-aday, :second)
|> DateTime.to_unix(:millisecond)
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
match = [
{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
[
{:>, :"$2", {:const, before}},
{:"=:=", {:const, account_id}, :"$1"}
], [:"$_"]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
drinks = :ets.select(ets, match)
|> Enum.sort_by(fn({{_, date}, _, _, _, _, _, _, _}) -> date end, &</2)
# {date, single_peak}
{all, last_drink_at, gl, active_drinks} = Enum.reduce(drinks, {0.0, nil, [], 0}, fn({{_, date}, volume, _, _, _, _, _, _}, {all, last_at, acc, active_drinks}) ->
k = if meta.sex, do: 0.7, else: 0.6
weight = meta.weight
peak = (10*volume)/(k*weight)
date = case date do
ts when is_integer(ts) -> DateTime.from_unix!(ts, :millisecond)
date = %NaiveDateTime{} -> DateTime.from_naive!(date, "Etc/UTC")
date = %DateTime{} -> date
end
last_at = last_at || date
mins_since = round(DateTime.diff(now, date)/60.0)
#IO.puts "Drink: #{inspect({date, volume})} - mins since: #{inspect mins_since} - last drink at #{inspect last_at}"
# Apply loss since `last_at` on `all`
#
all = if last_at do
mins_since_last = round(DateTime.diff(date, last_at)/60.0)
loss = ((meta.loss_factor/100)/60)*(mins_since_last)
#IO.puts "Applying last drink loss: from #{all}, loss of #{inspect loss} (mins since #{inspect mins_since_first})"
cond do
(all-loss) > 0 -> all - loss
true -> 0.0
end
else
all
end
#IO.puts "Applying last drink current before drink: #{inspect all}"
if mins_since < 30 do
per_min = (peak)/30.0
current = (per_min*mins_since)
#IO.puts "Applying current drink 30m: from #{peak}, loss of #{inspect per_min}/min (mins since #{inspect mins_since})"
{all + current, date, [{date, current} | acc], active_drinks + 1}
else
{all + peak, date, [{date, peak} | acc], active_drinks}
end
end)
#IO.puts "last drink #{inspect last_drink_at}"
mins_since_last = if last_drink_at do
round(DateTime.diff(now, last_drink_at)/60.0)
else
0
end
# Si on a déjà bu y'a déjà moins 15 minutes (big up le binge drinking), on applique plus de perte
level = if mins_since_last > 15 do
loss = ((meta.loss_factor/100)/60)*(mins_since_last)
Float.round(all - loss, 4)
else
all
end
#IO.puts "\n LEVEL #{inspect level}\n\n\n\n"
cond do
level < 0 -> {0.0, 0}
true -> {level, active_drinks}
end
end
defp format_duration_from_now(date, with_detail \\ true) do
date = if is_integer(date) do
date = DateTime.from_unix!(date, :millisecond)
|> Timex.Timezone.convert("Europe/Paris")
else
Util.to_naive_date_time(date)
end
now = DateTime.utc_now()
|> Timex.Timezone.convert("Europe/Paris")
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(date, "({h24}:{m})", "fr")
mins_since = round(DateTime.diff(now, date)/60.0)
if ago = format_minute_duration(mins_since) do
word = if mins_since > 0 do
"il y a "
else
"dans "
end
word <> ago <> if(with_detail, do: " #{detail}", else: "")
else
"maintenant #{detail}"
end
end
defp format_minute_duration(minutes) do
sober_in_s = if (minutes != 0) do
duration = Timex.Duration.from_minutes(minutes)
Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
else
nil
end
end
end
diff --git a/lib/nola_plugins/alcoolog_announcer_plugin.ex b/lib/nola_plugins/alcoolog_announcer.ex
similarity index 96%
rename from lib/nola_plugins/alcoolog_announcer_plugin.ex
rename to lib/nola_plugins/alcoolog_announcer.ex
index f90dc42..674f19b 100644
--- a/lib/nola_plugins/alcoolog_announcer_plugin.ex
+++ b/lib/nola_plugins/alcoolog_announcer.ex
@@ -1,272 +1,269 @@
-defmodule Nola.IRC.AlcoologAnnouncerPlugin do
+defmodule Nola.Plugins.AlcoologAnnouncer do
require Logger
@moduledoc """
Annonce changements d'alcoolog
"""
@channel "#dmz"
@seconds 30
@apero [
"C'EST L'HEURE DE L'APÉRRROOOOOOOO !!",
"SAAAAANNNNNNNTTTTTTTTAAAAAAAAIIIIIIIIIIIIIIIIIIIIII",
"APÉRO ? APÉRO !",
{:timed, ["/!\\ à vos tires bouchons…", "… débouchez …", "… BUVEZ! SANTAIII!"]},
"/!\\ ALERTE APÉRO /!\\",
"CED !!! VASE DE ROUGE !",
"DIDI UN PETIT RICARD™??!",
"ALLEZ GUIGUI UNE PETITE BIERE ?",
{:timed, ["/!\\ à vos tires bouchons…", "… débouchez …", "… BUVEZ! SANTAIII!"]},
"APPPPAIIIRRREAAUUUUUUUUUUU"
]
def irc_doc, do: nil
def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
def log(account) do
dets_filename = (Nola.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
from = ~U[2020-08-23 19:41:40.524154Z]
to = ~U[2020-08-24 19:41:40.524154Z]
select = [
{{:"$1", :"$2", :_},
[
{:andalso,
{:andalso, {:==, :"$1", {:const, account.id}},
{:>, :"$2", {:const, DateTime.to_unix(from)}}},
{:<, :"$2", {:const, DateTime.to_unix(to)}}}
], [:"$_"]}
]
res = :dets.select(dets, select)
:dets.close(dets)
res
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "account", [])
stats = get_stats()
Process.send_after(self(), :stats, :timer.seconds(30))
dets_filename = (Nola.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
ets = nil # :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
- #:ok = Nola.IRC.SettingPlugin.declare("alcoolog.alerts", __MODULE__, true, :boolean)
- #:ok = Nola.IRC.SettingPlugin.declare("alcoolog.aperoalert", __MODULE__, true, :boolean)
- #
{:ok, {stats, now(), dets, ets}}#, {:continue, :traverse}}
end
def handle_continue(:traverse, state = {_, _, dets, ets}) do
traverse_fun = fn(obj, dets) ->
case obj do
{nick, %DateTime{} = dt, active} ->
:dets.delete_object(dets, obj)
:dets.insert(dets, {nick, DateTime.to_unix(dt), active})
IO.puts("ok #{inspect obj}")
dets
{nick, ts, value} ->
:ets.insert(ets, { {nick, ts}, value })
dets
end
end
:dets.foldl(traverse_fun, dets, dets)
:dets.sync(dets)
IO.puts("alcoolog announcer fixed")
{:noreply, state}
end
def alcohol_reached(old, new, level) do
(old.active < level && new.active >= level) && (new.active5m >= level)
end
def alcohol_below(old, new, level) do
(old.active > level && new.active <= level) && (new.active5m <= level)
end
def handle_info(:stats, {old_stats, old_now, dets, ets}) do
stats = get_stats()
now = now()
if old_now.hour < 18 && now.hour == 18 do
apero = Enum.shuffle(@apero)
|> Enum.random()
case apero do
{:timed, list} ->
spawn(fn() ->
for line <- list do
IRC.Connection.broadcast_message("evolu.net", "#dmz", line)
:timer.sleep(:timer.seconds(5))
end
end)
string ->
IRC.Connection.broadcast_message("evolu.net", "#dmz", string)
end
end
#IO.puts "newstats #{inspect stats}"
events = for {acct, old} <- old_stats do
new = Map.get(stats, acct, nil)
#IO.puts "#{acct}: #{inspect(old)} -> #{inspect(new)}"
now = DateTime.to_unix(DateTime.utc_now())
if new && new[:active] do
:dets.insert(dets, {acct, now, new[:active]})
:ets.insert(ets, {{acct, now}, new[:active]})
else
:dets.insert(dets, {acct, now, 0.0})
:ets.insert(ets, {{acct, now}, new[:active]})
end
event = cond do
old == nil -> nil
(old.active > 0) && (new == nil) -> :sober
new == nil -> nil
alcohol_reached(old, new, 0.5) -> :stopconduire
alcohol_reached(old, new, 1.0) -> :g1
alcohol_reached(old, new, 2.0) -> :g2
alcohol_reached(old, new, 3.0) -> :g3
alcohol_reached(old, new, 4.0) -> :g4
alcohol_reached(old, new, 5.0) -> :g5
alcohol_reached(old, new, 6.0) -> :g6
alcohol_reached(old, new, 7.0) -> :g7
alcohol_reached(old, new, 10.0) -> :g10
alcohol_reached(old, new, 13.74) -> :record
alcohol_below(old, new, 0.5) -> :conduire
alcohol_below(old, new, 1.0) -> :fini1g
alcohol_below(old, new, 2.0) -> :fini2g
alcohol_below(old, new, 3.0) -> :fini3g
alcohol_below(old, new, 4.0) -> :fini4g
(old.rising) && (!new.rising) -> :lowering
true -> nil
end
{acct, event}
end
for {acct, event} <- events do
message = case event do
:g1 -> [
"[vigicuite jaune] LE GRAMME! LE GRAMME O/",
"début de vigicuite jaune ! LE GRAMME ! \\O/",
"waiiiiiiii le grammmeee",
"bourraiiiiiiiiiiide 1 grammeeeeeeeeeee",
]
:g2 -> [
"[vigicuite orange] \\o_YAY 2 GRAMMES ! _o/",
"PAITAIIIIIIIIII DEUX GRAMMEESSSSSSSSSSSSSSSSS",
"bourrrrrraiiiiiiiiiiiiiiiide 2 grammeeeeeeeeeees",
]
:g3 -> [
"et un ! et deux ! et TROIS GRAMMEEESSSSSSS",
"[vigicuite rouge] _o/ BOURRAIIDDDEEEE 3 GRAMMESSSSSSSSS \\o/ \\o/"
]
:g4 -> [
"[vigicuite écarlate] et un, et deux, et trois, ET QUATRES GRAMMEESSSSSSSSSSSSSSSSSSSssssss"
]
:g5 -> "[vigicuite écarlate+] PUTAIN 5 GRAMMES !"
:g6 -> "[vigicuite écarlate++] 6 grammes ? Vous pouvez joindre Alcool info service au 0 980 980 930"
:g7 -> "[vigicuite c'est la merde] 7 grammes. Le SAMU, c'est le 15."
:g10 -> "BORDLE 10 GRAMMES"
:record -> "RECORD DU MONDE BATTU ! >13.74g/l !!"
:fini1g -> [
"fin d'alerte vigicuite jaune, passage en vert (<1g/l)",
"/!\\ alerte moins de 1g/l /!\\"
]
:fini2g -> [
"t'as moins de 2 g/l, faut se reprendre là [vigicuite jaune]"
]
:fini3g -> [
"fin d'alerte vigicuite rouge, passage en orange (<3g/l)"
]
:fini4g -> [
"fin d'alerte vigicuite écarlate, passage en rouge (<4g/l)"
]
:lowering -> [
"attention ça baisse!",
"tu vas quand même pas en rester là ?",
"IL FAUT CONTINUER À BOIRE !",
"t'abandonnes déjà ?",
"!santai ?",
"faut pas en rester là",
"il faut se resservir",
"coucou faut reboire",
"encore un petit verre ?",
"abwaaaaaaaaaaaaarrrrrrrrrrrrrr",
"taux d'alcoolémie en chute ! agissez avant qu'il soit trop tard!",
"ÇA BAISSE !!"
]
:stopconduire -> [
"0.5g! bientot le gramme?",
"tu peux plus prendre la route... mais... tu peux prendre la route du gramme! !santai !",
"fini la conduite!",
"0.5! continues faut pas en rester là!",
"beau début, continues !",
"ça monte! 0.5g/l!"
]
:conduire -> [
"tu peux conduire, ou recommencer à boire! niveau critique!",
"!santai ?",
"tu peux reprendre la route, ou reprendre la route du gramme..",
"attention, niveau critique!",
"il faut boire !!",
"trop de sang dans ton alcool, c'est mauvais pour la santé",
"faut pas en rester là !",
]
:sober -> [
"sobre…",
"/!\\ alerte sobriété /!\\",
"... sobre?!?!",
"sobre :(",
"attention, t'es sobre :/",
"danger, alcoolémie à 0.0 !",
"sobre! c'était bien on recommence quand ?",
"sobre ? Faut recommencer...",
"T'es sobre. Ne te laisses pas abattre- ton caviste peut aider.",
"Vous êtes sobre ? Ceci n'est pas une fatalité - resservez vous vite !"
]
_ -> nil
end
message = case message do
m when is_binary(m) -> m
m when is_list(m) -> m |> Enum.shuffle() |> Enum.random()
nil -> nil
end
if message do
#IO.puts("#{acct}: #{message}")
account = IRC.Account.get(acct)
for {net, chan} <- IRC.Membership.notify_channels(account) do
user = IRC.UserTrack.find_by_account(net, account)
nick = if(user, do: user.nick, else: account.name)
IRC.Connection.broadcast_message(net, chan, "#{nick}: #{message}")
end
end
end
timer()
#IO.puts "tick stats ok"
{:noreply, {stats,now,dets,ets}}
end
def handle_info(_, state) do
{:noreply, state}
end
defp now() do
DateTime.utc_now()
|> Timex.Timezone.convert("Europe/Paris")
end
defp get_stats() do
- Enum.into(Nola.IRC.AlcoologPlugin.get_all_stats(), %{})
+ Enum.into(Nola.Plugins.Alcoolog.get_all_stats(), %{})
end
defp timer() do
Process.send_after(self(), :stats, :timer.seconds(@seconds))
end
end
diff --git a/lib/nola_plugins/base_plugin.ex b/lib/nola_plugins/base.ex
similarity index 83%
rename from lib/nola_plugins/base_plugin.ex
rename to lib/nola_plugins/base.ex
index a2b9ffb..b7b5e16 100644
--- a/lib/nola_plugins/base_plugin.ex
+++ b/lib/nola_plugins/base.ex
@@ -1,131 +1,132 @@
-defmodule Nola.IRC.BasePlugin do
+defmodule Nola.Plugins.Base do
def irc_doc, do: nil
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:version", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:help", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:liquidrender", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:plugin", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:plugins", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, "plugins", msg = %{trigger: %{type: :bang, args: []}}}, _) do
- enabled_string = IRC.Plugin.enabled()
+ enabled_string = Nola.Plugins.enabled()
|> Enum.map(fn(mod) ->
mod
|> Macro.underscore()
|> String.split("/", parts: :infinity)
|> List.last()
- |> String.replace("_plugin", "")
|> Enum.sort()
end)
|> Enum.join(", ")
msg.replyfun.("Enabled plugins: #{enabled_string}")
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :query, args: [plugin]}} = m}, _) do
- module = Module.concat([Nola.IRC, Macro.camelize(plugin<>"_plugin")])
+ module = Module.concat([Nola.Plugins, Macro.camelize(plugin)])
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module)
do
m.replyfun.("loaded, active: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
nil ->
- msg = case IRC.Plugin.get(module) do
+ 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.IRC, Macro.camelize(plugin<>"_plugin")])
+ module = Module.concat([Nola.Plugins, Macro.camelize(plugin)])
with true <- Code.ensure_loaded?(module),
- IRC.Plugin.switch(module, true),
- {:ok, pid} <- IRC.Plugin.start(module)
+ Nola.Plugins.switch(module, true),
+ {: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.IRC, Macro.camelize(plugin<>"_plugin")])
+ 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} <- IRC.Plugin.start(module)
+ {: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.IRC, Macro.camelize(plugin<>"_plugin")])
+ 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
IRC.Plugin.switch(module, false)
m.replyfun.("stopped: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
nil -> m.replyfun.("not active")
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "liquidrender", m = %{trigger: %{args: args}}}, _) do
template = Enum.join(args, " ")
m.replyfun.(Tmpl.render(template, m))
{:noreply, nil}
end
def handle_info({:irc, :trigger, "help", m = %{trigger: %{type: :bang}}}, _) do
url = NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel))
m.replyfun.("-> #{url}")
{:noreply, nil}
end
def handle_info({:irc, :trigger, "version", message = %{trigger: %{type: :bang}}}, _) do
{:ok, vsn} = :application.get_key(:nola, :vsn)
ver = List.to_string(vsn)
url = NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :index)
elixir_ver = Application.started_applications() |> List.keyfind(:elixir, 0) |> elem(2) |> to_string()
otp_ver = :erlang.system_info(:system_version) |> to_string() |> String.trim()
system = :erlang.system_info(:system_architecture) |> to_string()
+ brand = Nola.brand(:name)
+ owner = "#{Nola.brand(:owner)} <#{Nola.brand(:owner_email)}>"
message.replyfun.([
- <<"🤖 I am a robot running", 2, "beautte, version #{ver}", 2, " — source: #{Nola.source_url()}">>,
+ <<"🤖 I am a robot running", 2, "#{brand}, version #{ver}", 2, " — source: #{Nola.source_url()}">>,
"🦾 Elixir #{elixir_ver} #{otp_ver} on #{system}",
- "👷‍♀️ Owner: href <href@random.sh>",
+ "👷‍♀️ Owner: h#{owner}",
"🌍 Web interface: #{url}"
])
{:noreply, nil}
end
def handle_info(msg, _) do
{:noreply, nil}
end
end
diff --git a/lib/nola_plugins/bourosama_plugin.ex b/lib/nola_plugins/boursorama.ex
similarity index 97%
rename from lib/nola_plugins/bourosama_plugin.ex
rename to lib/nola_plugins/boursorama.ex
index dd05144..f872584 100644
--- a/lib/nola_plugins/bourosama_plugin.ex
+++ b/lib/nola_plugins/boursorama.ex
@@ -1,58 +1,58 @@
-defmodule Nola.IRC.BoursoramaPlugin do
+defmodule Nola.Plugins.Boursorama do
def irc_doc() do
"""
# bourses
Un peu comme [finance](#finance), mais en un peu mieux, et un peu moins bien.
Source: [boursorama.com](https://boursorama.com)
* **!caca40** affiche l'état du cac40
"""
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@cac40_url "https://www.boursorama.com/bourse/actions/palmares/france/?france_filter%5Bmarket%5D=1rPCAC&france_filter%5Bsector%5D=&france_filter%5Bvariation%5D=50002&france_filter%5Bperiod%5D=1&france_filter%5Bfilter%5D="
def init(_) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:cac40", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:caca40", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, cac, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) when cac in ["cac40", "caca40"] do
case HTTPoison.get(@cac40_url, [], []) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
html = Floki.parse(body)
board = Floki.find(body, "div.c-tradingboard")
cac40 = Floki.find(board, ".c-tradingboard__main > .c-tradingboard__infos")
instrument = Floki.find(cac40, ".c-instrument")
last = Floki.find(instrument, "span[data-ist-last]")
|> Floki.text()
|> String.replace(" ", "")
variation = Floki.find(instrument, "span[data-ist-variation]")
|> Floki.text()
sign = case variation do
"-"<>_ -> "▼"
"+" -> "▲"
_ -> ""
end
m.replyfun.("caca40: #{sign} #{variation} #{last}")
{:error, %HTTPoison.Response{status_code: code}} ->
m.replyfun.("caca40: erreur http #{code}")
_ ->
m.replyfun.("caca40: erreur http")
end
end
end
diff --git a/lib/nola_plugins/buffer_plugin.ex b/lib/nola_plugins/buffer.ex
similarity index 97%
rename from lib/nola_plugins/buffer_plugin.ex
rename to lib/nola_plugins/buffer.ex
index eece34e..67aea35 100644
--- a/lib/nola_plugins/buffer_plugin.ex
+++ b/lib/nola_plugins/buffer.ex
@@ -1,44 +1,44 @@
-defmodule Nola.IRC.BufferPlugin do
+defmodule Nola.Plugins.Buffer do
@table __MODULE__.ETS
def irc_doc, do: nil
def table(), do: @table
def select_buffer(network, channel, limit \\ 50) do
import Ex2ms
spec = fun do {{n, c, _}, m} when n == ^network and (c == ^channel or is_nil(c)) -> m end
:ets.select(@table, spec, limit)
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
for e <- ~w(messages triggers events outputs) do
{:ok, _} = Registry.register(IRC.PubSub, e, plugin: __MODULE__)
end
{:ok, :ets.new(@table, [:named_table, :ordered_set, :protected])}
end
def handle_info({:irc, :trigger, _, message}, ets), do: handle_message(message, ets)
def handle_info({:irc, :text, message}, ets), do: handle_message(message, ets)
def handle_info({:irc, :event, event}, ets), do: handle_message(event, ets)
defp handle_message(message = %{network: network}, ets) do
key = {network, Map.get(message, :channel), ts(message.at)}
:ets.insert(ets, {key, message})
{:noreply, ets}
end
defp ts(nil), do: ts(NaiveDateTime.utc_now())
defp ts(naive = %NaiveDateTime{}) do
ts = naive
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_unix()
-ts
end
end
diff --git a/lib/nola_plugins/calc_plugin.ex b/lib/nola_plugins/calc.ex
similarity index 96%
rename from lib/nola_plugins/calc_plugin.ex
rename to lib/nola_plugins/calc.ex
index 264370c..91fbbae 100644
--- a/lib/nola_plugins/calc_plugin.ex
+++ b/lib/nola_plugins/calc.ex
@@ -1,37 +1,37 @@
-defmodule Nola.IRC.CalcPlugin do
+defmodule Nola.Plugins.Calc do
@moduledoc """
# calc
* **!calc `<expression>`**: évalue l'expression mathématique `<expression>`.
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:calc", [plugin: __MODULE__])
{:ok, nil}
end
def handle_info({:irc, :trigger, "calc", message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: expr_list}}}, state) do
expr = Enum.join(expr_list, " ")
result = try do
case Abacus.eval(expr) do
{:ok, result} -> result
error -> inspect(error)
end
rescue
error -> if(error[:message], do: "#{error.message}", else: "erreur")
end
message.replyfun.("#{message.sender.nick}: #{expr} = #{result}")
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
end
diff --git a/lib/nola_plugins/coronavirus_plugin.ex b/lib/nola_plugins/coronavirus.ex
similarity index 99%
rename from lib/nola_plugins/coronavirus_plugin.ex
rename to lib/nola_plugins/coronavirus.ex
index d04d8f9..624bae7 100644
--- a/lib/nola_plugins/coronavirus_plugin.ex
+++ b/lib/nola_plugins/coronavirus.ex
@@ -1,172 +1,172 @@
-defmodule Nola.IRC.CoronavirusPlugin do
+defmodule Nola.Plugins.Coronavirus do
require Logger
NimbleCSV.define(CovidCsv, separator: ",", escape: "\"")
@moduledoc """
# Corona Virus
Données de [Johns Hopkins University](https://github.com/CSSEGISandData/COVID-19) et mises à jour a peu près tous les jours.
* `!coronavirus [France | Country]`: :-)
* `!coronavirus`: top 10 confirmés et non guéris
* `!coronavirus confirmés`: top 10 confirmés
* `!coronavirus morts`: top 10 morts
* `!coronavirus soignés`: top 10 soignés
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:coronavirus", [plugin: __MODULE__])
{:ok, nil, {:continue, :init}}
:ignore
end
def handle_continue(:init, _) do
date = Date.add(Date.utc_today(), -2)
{data, _} = fetch_data(%{}, date)
{data, next} = fetch_data(data)
:timer.send_after(next, :update)
{:noreply, %{data: data}}
end
def handle_info(:update, state) do
{data, next} = fetch_data(state.data)
:timer.send_after(next, :update)
{:noreply, %{data: data}}
end
def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :bang, args: args}}}, state) when args in [
[], ["morts"], ["confirmés"], ["soignés"], ["malades"], ["n"], ["nmorts"], ["nsoignés"], ["nconfirmés"]] do
{field, name} = case args do
["confirmés"] -> {:confirmed, "confirmés"}
["morts"] -> {:deaths, "morts"}
["soignés"] -> {:recovered, "soignés"}
["nmorts"] -> {:new_deaths, "nouveaux morts"}
["nconfirmés"] -> {:new_confirmed, "nouveaux confirmés"}
["n"] -> {:new_current, "nouveaux malades"}
["nsoignés"] -> {:new_recovered, "nouveaux soignés"}
_ -> {:current, "malades"}
end
IO.puts("FIELD #{inspect field}")
field_evol = String.to_atom("new_#{field}")
sorted = state.data
|> Enum.filter(fn({_, %{region: region}}) -> region == true end)
|> Enum.map(fn({location, data}) -> {location, Map.get(data, field, 0), Map.get(data, field_evol, 0)} end)
|> Enum.sort_by(fn({_,count,_}) -> count end, &>=/2)
|> Enum.take(10)
|> Enum.with_index()
|> Enum.map(fn({{location, count, evol}, index}) ->
ev = if String.starts_with?(name, "nouveaux") do
""
else
" (#{Util.plusminus(evol)})"
end
"##{index+1}: #{location} #{count}#{ev}"
end)
|> Enum.intersperse(" - ")
|> Enum.join()
m.replyfun.("CORONAVIRUS TOP10 #{name}: " <> sorted)
{:noreply, state}
end
def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :bang, args: location}}}, state) do
location = Enum.join(location, " ") |> String.downcase()
if data = Map.get(state.data, location) do
m.replyfun.("coronavirus: #{location}: "
<> "#{data.current} malades (#{Util.plusminus(data.new_current)}), "
<> "#{data.confirmed} confirmés (#{Util.plusminus(data.new_confirmed)}), "
<> "#{data.deaths} morts (#{Util.plusminus(data.new_deaths)}), "
<> "#{data.recovered} soignés (#{Util.plusminus(data.new_recovered)}) (@ #{data.update})")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :query, args: location}}}, state) do
m.replyfun.("https://github.com/CSSEGISandData/COVID-19")
{:noreply, state}
end
# 1. Try to fetch data for today
# 2. Fetch yesterday if no results
defp fetch_data(current_data, date \\ nil) do
now = Date.utc_today()
url = fn(date) ->
"https://github.com/CSSEGISandData/COVID-19/raw/master/csse_covid_19_data/csse_covid_19_daily_reports/#{date}.csv"
end
request_date = date || now
Logger.debug("Coronavirus check date: #{inspect request_date}")
{:ok, date_s} = Timex.format({request_date.year, request_date.month, request_date.day}, "%m-%d-%Y", :strftime)
cur_url = url.(date_s)
Logger.debug "Fetching URL #{cur_url}"
case HTTPoison.get(cur_url, [], follow_redirect: true) do
{:ok, %HTTPoison.Response{status_code: 200, body: csv}} ->
# Parse CSV update data
data = csv
|> CovidCsv.parse_string()
|> Enum.reduce(%{}, fn(line, acc) ->
case line do
# FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Deaths,Recovered,Active,Combined_Key
#0FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Deaths,Recovered,Active,Combined_Key,Incidence_Rate,Case-Fatality_Ratio
[_, _, state, region, update, _lat, _lng, confirmed, deaths, recovered, _active, _combined_key, _incidence_rate, _fatality_ratio] ->
state = String.downcase(state)
region = String.downcase(region)
confirmed = String.to_integer(confirmed)
deaths = String.to_integer(deaths)
recovered = String.to_integer(recovered)
current = (confirmed - recovered) - deaths
entry = %{update: update, confirmed: confirmed, deaths: deaths, recovered: recovered, current: current, region: region}
region_entry = Map.get(acc, region, %{update: nil, confirmed: 0, deaths: 0, recovered: 0, current: 0})
region_entry = %{
update: region_entry.update || update,
confirmed: region_entry.confirmed + confirmed,
deaths: region_entry.deaths + deaths,
current: region_entry.current + current,
recovered: region_entry.recovered + recovered,
region: true
}
changes = if old = Map.get(current_data, region) do
%{
new_confirmed: region_entry.confirmed - old.confirmed,
new_current: region_entry.current - old.current,
new_deaths: region_entry.deaths - old.deaths,
new_recovered: region_entry.recovered - old.recovered,
}
else
%{new_confirmed: 0, new_current: 0, new_deaths: 0, new_recovered: 0}
end
region_entry = Map.merge(region_entry, changes)
acc = Map.put(acc, region, region_entry)
acc = if state && state != "" do
Map.put(acc, state, entry)
else
acc
end
other ->
Logger.info("Coronavirus line failed: #{inspect line}")
acc
end
end)
Logger.info "Updated coronavirus database"
{data, :timer.minutes(60)}
{:ok, %HTTPoison.Response{status_code: 404}} ->
Logger.debug "Corona 404 #{cur_url}"
date = Date.add(date || now, -1)
fetch_data(current_data, date)
other ->
Logger.error "Coronavirus: Update failed #{inspect other}"
{current_data, :timer.minutes(5)}
end
end
end
diff --git a/lib/nola_plugins/correction_plugin.ex b/lib/nola_plugins/correction.ex
similarity index 97%
rename from lib/nola_plugins/correction_plugin.ex
rename to lib/nola_plugins/correction.ex
index 5f9b278..03968c8 100644
--- a/lib/nola_plugins/correction_plugin.ex
+++ b/lib/nola_plugins/correction.ex
@@ -1,59 +1,59 @@
-defmodule Nola.IRC.CorrectionPlugin do
+defmodule Nola.Plugins.Correction do
@moduledoc """
# correction
* `s/pattern/replace` replace `pattern` by `replace` in the last matching message
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "messages", [plugin: __MODULE__])
{:ok, _} = Registry.register(IRC.PubSub, "triggers", [plugin: __MODULE__])
{:ok, %{}}
end
# Trigger fallback
def handle_info({:irc, :trigger, _, m = %IRC.Message{}}, state) do
{:noreply, correction(m, state)}
end
def handle_info({:irc, :text, m = %IRC.Message{}}, state) do
{:noreply, correction(m, state)}
end
def correction(m, state) do
history = Map.get(state, key(m), [])
if String.starts_with?(m.text, "s/") do
case String.split(m.text, "/") do
["s", match, replace | _] ->
case Regex.compile(match) do
{:ok, reg} ->
repl = Enum.find(history, fn(m) -> Regex.match?(reg, m.text) end)
if repl do
new_text = String.replace(repl.text, reg, replace)
m.replyfun.("correction: <#{repl.sender.nick}> #{new_text}")
end
_ ->
m.replyfun.("correction: invalid regex")
end
_ -> m.replyfun.("correction: invalid regex format")
end
state
else
history = if length(history) > 100 do
{_, history} = List.pop_at(history, 99)
[m | history]
else
[m | history]
end
Map.put(state, key(m), history)
end
end
defp key(%{network: net, channel: chan}), do: "#{net}/#{chan}"
end
diff --git a/lib/nola_plugins/dice_plugin.ex b/lib/nola_plugins/dice.ex
similarity index 98%
rename from lib/nola_plugins/dice_plugin.ex
rename to lib/nola_plugins/dice.ex
index b5e7649..2c1515b 100644
--- a/lib/nola_plugins/dice_plugin.ex
+++ b/lib/nola_plugins/dice.ex
@@ -1,66 +1,66 @@
-defmodule Nola.IRC.DicePlugin do
+defmodule Nola.Plugins.Dice do
require Logger
@moduledoc """
# dice
* **!dice `[1 | lancés]` `[6 | faces]`**: lance une ou plusieurs fois un dé de 6 ou autre faces
"""
@default_faces 6
@default_rolls 1
@max_rolls 50
def short_irc_doc, do: "!dice (jeter un dé)"
defstruct client: nil, dets: nil
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:dice", [plugin: __MODULE__])
{:ok, %__MODULE__{}}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: args}}}, state) do
to_integer = fn(string, default) ->
case Integer.parse(string) do
{int, _} -> int
_ -> default
end
end
{rolls, faces} = case args do
[] -> {@default_rolls, @default_faces}
[faces, rolls] -> {to_integer.(rolls, @default_rolls), to_integer.(faces, @default_faces)}
[rolls] -> {to_integer.(rolls, @default_rolls), @default_faces}
end
roll(state, message, faces, rolls)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
defp roll(state, message, faces, 1) when faces > 0 do
random = :crypto.rand_uniform(1, faces+1)
message.replyfun.("#{message.sender.nick} dice: #{random}")
end
defp roll(state, message, faces, rolls) when faces > 0 and rolls > 0 and rolls <= @max_rolls do
{results, acc} = Enum.map_reduce(Range.new(1, rolls), 0, fn(i, acc) ->
random = :crypto.rand_uniform(1, faces+1)
{random, acc + random}
end)
results = Enum.join(results, "; ")
message.replyfun.("#{message.sender.nick} dice: [#{acc}] #{results}")
end
defp roll(_, _, _, _, _), do: nil
end
diff --git a/lib/nola_plugins/finance_plugin.ex b/lib/nola_plugins/finance.ex
similarity index 99%
rename from lib/nola_plugins/finance_plugin.ex
rename to lib/nola_plugins/finance.ex
index 16d06ee..d6f890a 100644
--- a/lib/nola_plugins/finance_plugin.ex
+++ b/lib/nola_plugins/finance.ex
@@ -1,190 +1,190 @@
-defmodule Nola.IRC.FinancePlugin do
+defmodule Nola.Plugins.Finance do
require Logger
@moduledoc """
# finance
Données de [alphavantage.co](https://alphavantage.co).
## forex / monnaies / crypto-monnaies
* **`!forex <MONNAIE1> [MONNAIE2]`**: taux de change entre deux monnaies.
* **`!forex <MONTANT> <MONNAIE1> <MONNAIE2>`**: converti `montant` entre deux monnaies
* **`?currency <recherche>`**: recherche une monnaie
Utiliser le symbole des monnaies (EUR, USD, ...).
## bourses
* **`!stocks <SYMBOLE>`**
* **`?stocks <recherche>`** cherche un symbole
Pour les symboles non-US, ajouter le suffixe (RNO Paris: RNO.PAR).
"""
@currency_list "http://www.alphavantage.co/physical_currency_list/"
@crypto_list "http://www.alphavantage.co/digital_currency_list/"
HTTPoison.start()
load_currency = fn(url) ->
resp = HTTPoison.get!(url)
resp.body
|> String.strip()
|> String.split("\n")
|> Enum.drop(1)
|> Enum.map(fn(line) ->
[symbol, name] = line
|> String.strip()
|> String.split(",", parts: 2)
{symbol, name}
end)
|> Enum.into(Map.new)
end
fiat = load_currency.(@currency_list)
crypto = load_currency.(@crypto_list)
@currencies Map.merge(fiat, crypto)
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:forex", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:currency", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:stocks", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, "stocks", message = %{trigger: %{type: :query, args: args = search}}}, state) do
search = Enum.join(search, "%20")
url = "https://www.alphavantage.co/query?function=SYMBOL_SEARCH&keywords=#{search}&apikey=#{api_key()}"
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: data}} ->
data = Poison.decode!(data)
if error = Map.get(data, "Error Message") do
Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
message.replyfun.("stocks: requête invalide")
else
items = for item <- Map.get(data, "bestMatches") do
symbol = Map.get(item, "1. symbol")
name = Map.get(item, "2. name")
type = Map.get(item, "3. type")
region = Map.get(item, "4. region")
currency = Map.get(item, "8. currency")
"#{symbol}: #{name} (#{region}; #{currency}; #{type})"
end
|> Enum.join(", ")
items = if items == "" do
"no results!"
else
items
end
message.replyfun.(items)
end
{:ok, resp = %HTTPoison.Response{status_code: code}} ->
Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
message.replyfun.("forex: erreur (api #{code})")
{:error, %HTTPoison.Error{reason: error}} ->
Logger.error "AlphaVantage HTTP error: #{inspect error}"
message.replyfun.("forex: erreur (http #{inspect error})")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "stocks", message = %{trigger: %{type: :bang, args: args = [symbol]}}}, state) do
url = "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=#{symbol}&apikey=#{api_key()}"
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: data}} ->
data = Poison.decode!(data)
if error = Map.get(data, "Error Message") do
Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
message.replyfun.("stocks: requête invalide")
else
data = Map.get(data, "Global Quote")
open = Map.get(data, "02. open")
high = Map.get(data, "03. high")
low = Map.get(data, "04. low")
price = Map.get(data, "05. price")
volume = Map.get(data, "06. volume")
prev_close = Map.get(data, "08. previous close")
change = Map.get(data, "09. change")
change_pct = Map.get(data, "10. change percent")
msg = "#{symbol}: #{price} #{change} [#{change_pct}] (high: #{high}, low: #{low}, open: #{open}, prev close: #{prev_close}) (volume: #{volume})"
message.replyfun.(msg)
end
{:ok, resp = %HTTPoison.Response{status_code: code}} ->
Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
message.replyfun.("stocks: erreur (api #{code})")
{:error, %HTTPoison.Error{reason: error}} ->
Logger.error "AlphaVantage HTTP error: #{inspect error}"
message.replyfun.("stocks: erreur (http #{inspect error})")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "forex", message = %{trigger: %{type: :bang, args: args = [_ | _]}}}, state) do
{amount, from, to} = case args do
[amount, from, to] ->
{amount, _} = Float.parse(amount)
{amount, from, to}
[from, to] ->
{1, from, to}
[from] ->
{1, from, "EUR"}
end
url = "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=#{from}&to_currency=#{to}&apikey=#{api_key()}"
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: data}} ->
data = Poison.decode!(data)
if error = Map.get(data, "Error Message") do
Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
message.replyfun.("forex: requête invalide")
else
data = Map.get(data, "Realtime Currency Exchange Rate")
from_name = Map.get(data, "2. From_Currency Name")
to_name = Map.get(data, "4. To_Currency Name")
rate = Map.get(data, "5. Exchange Rate")
{rate, _} = Float.parse(rate)
value = amount*rate
message.replyfun.("#{amount} #{from} (#{from_name}) -> #{value} #{to} (#{to_name}) (#{rate})")
end
{:ok, resp = %HTTPoison.Response{status_code: code}} ->
Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
message.replyfun.("forex: erreur (api #{code})")
{:error, %HTTPoison.Error{reason: error}} ->
Logger.error "AlphaVantage HTTP error: #{inspect error}"
message.replyfun.("forex: erreur (http #{inspect error})")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "currency", message = %{trigger: %{type: :query, args: args = search}}}, state) do
search = Enum.join(search, " ")
results = Enum.filter(@currencies, fn({symbol, name}) ->
String.contains?(String.downcase(name), String.downcase(search)) || String.contains?(String.downcase(symbol), String.downcase(search))
end)
|> Enum.map(fn({symbol, name}) ->
"#{symbol}: #{name}"
end)
|> Enum.join(", ")
if results == "" do
message.replyfun.("no results!")
else
message.replyfun.(results)
end
{:noreply, state}
end
defp api_key() do
Application.get_env(:nola, :alphavantage, [])
|> Keyword.get(:api_key, "demo")
end
end
diff --git a/lib/nola_plugins/gpt_plugin.ex b/lib/nola_plugins/gpt.ex
similarity index 98%
rename from lib/nola_plugins/gpt_plugin.ex
rename to lib/nola_plugins/gpt.ex
index 2c8f182..3d2a66d 100644
--- a/lib/nola_plugins/gpt_plugin.ex
+++ b/lib/nola_plugins/gpt.ex
@@ -1,259 +1,259 @@
-defmodule Nola.IRC.GptPlugin do
+defmodule Nola.Plugins.Gpt do
require Logger
- import Irc.Plugin.TempRef
+ 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(IRC.PubSub, "trigger:#{@trigger}", regopts)
{:ok, %__MODULE__{temprefs: new_temp_refs()}}
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [prompt | args]}}}, state) do
case Couch.get(@couch_db, prompt) do
{:ok, prompt} -> {:noreply, prompt(m, prompt, Enum.join(args, " "), state)}
{:error, :not_found} ->
m.replyfun.("gpt: prompt '#{prompt}' does not exists")
{:noreply, state}
error ->
Logger.info("gpt: prompt load error: #{inspect error}")
m.replyfun.("gpt: database error")
{:noreply, state}
end
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do
case Couch.get(@couch_db, "_all_docs") do
{:ok, %{"rows" => []}} -> m.replyfun.("gpt: no prompts available")
{:ok, %{"rows" => prompts}} ->
prompts = prompts |> Enum.map(fn(prompt) -> Map.get(prompt, "id") end) |> Enum.join(", ")
m.replyfun.("gpt: prompts: #{prompts}")
error ->
Logger.info("gpt: prompt load error: #{inspect error}")
m.replyfun.("gpt: database error")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :plus, args: [ref_or_id | args]}}}, state) do
id = lookup_temp_ref(ref_or_id, state.temprefs, ref_or_id)
case Couch.get(@couch_run_db, id) do
{:ok, run} ->
Logger.debug("+gpt run: #{inspect run}")
{:noreply, continue_prompt(m, run, Enum.join(args, " "), state)}
{:error, :not_found} ->
m.replyfun.("gpt: ref or id not found or expired: #{inspect ref_or_id} (if using short ref, try using full id)")
{:noreply, state}
error ->
Logger.info("+gpt: run load error: #{inspect error}")
m.replyfun.("gpt: database error")
{:noreply, state}
end
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: ["offensive" | text]}}}, state) do
text = Enum.join(text, " ")
{moderate?, moderation} = moderation(text, m.account.id)
reply = cond do
moderate? -> "⚠️ #{Enum.join(moderation, ", ")}"
!moderate? && moderation -> "👍"
!moderate? -> "☠️ error"
end
m.replyfun.(reply)
{:noreply, state}
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: ["show", ref_or_id]}}}, state) do
id = lookup_temp_ref(ref_or_id, state.temprefs, ref_or_id)
url = if m.channel do
NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :result, m.network, NolaWeb.format_chan(m.channel), id)
else
NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :result, id)
end
m.replyfun.("→ #{url}")
{:noreply, state}
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: [prompt]}}}, state) do
url = if m.channel do
NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :prompt, m.network, NolaWeb.format_chan(m.channel), prompt)
else
NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :prompt, prompt)
end
m.replyfun.("→ #{url}")
{:noreply, state}
end
def handle_info(info, state) do
Logger.debug("gpt: unhandled info: #{inspect info}")
{:noreply, state}
end
defp continue_prompt(msg, run, content, state) do
prompt_id = Map.get(run, "prompt_id")
prompt_rev = Map.get(run, "prompt_rev")
original_prompt = case Couch.get(@couch_db, prompt_id, rev: prompt_rev) do
{:ok, prompt} -> prompt
_ -> nil
end
if original_prompt do
continue_prompt = %{"_id" => prompt_id,
"_rev" => prompt_rev,
"type" => Map.get(original_prompt, "type"),
"parent_run_id" => Map.get(run, "_id"),
"openai_params" => Map.get(run, "request") |> Map.delete("prompt")}
continue_prompt = if prompt_string = Map.get(original_prompt, "continue_prompt") do
full_text = get_in(run, ~w(request prompt)) <> "\n" <> Map.get(run, "response")
continue_prompt
|> Map.put("prompt", prompt_string)
|> Map.put("prompt_format", "liquid")
|> Map.put("prompt_liquid_variables", %{"previous" => full_text})
else
prompt_content_tag = if content != "", do: " {{content}}", else: ""
string = get_in(run, ~w(request prompt)) <> "\n" <> Map.get(run, "response") <> prompt_content_tag
continue_prompt
|> Map.put("prompt", string)
|> Map.put("prompt_format", "liquid")
end
prompt(msg, continue_prompt, content, state)
else
msg.replyfun.("gpt: cannot continue this prompt: original prompt not found #{prompt_id}@v#{prompt_rev}")
state
end
end
defp prompt(msg, prompt = %{"type" => "completions", "prompt" => prompt_template}, content, state) do
- Logger.debug("gpt_plugin:prompt/4 #{inspect prompt}")
+ 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/nola_irc/plugin/temp_ref.ex b/lib/nola_plugins/helpers/temp_ref.ex
similarity index 98%
rename from lib/nola_irc/plugin/temp_ref.ex
rename to lib/nola_plugins/helpers/temp_ref.ex
index 923fa1a..160169d 100644
--- a/lib/nola_irc/plugin/temp_ref.ex
+++ b/lib/nola_plugins/helpers/temp_ref.ex
@@ -1,95 +1,95 @@
-defmodule Irc.Plugin.TempRef do
+defmodule Nola.Plugins.TempRefHelper do
@moduledoc """
This module allows to easily implement local temporary simple references for easy access from IRC.
For example, your plugin output could be acted on, and instead of giving the burden for the user to
write or copy that uuid, you could give them a small alphanumeric reference to use instead.
You can configure how many and for how long the references are kept.
## Usage
`import Irc.Plugin.TempRef`
```elixir
defmodule Irc.MyPlugin do
defstruct [:temprefs]
def init(_) do
# …
{:ok, %__MODULE__{temprefs: new_temp_refs()}
end
end
```
"""
defstruct [:refs, :max, :expire, :build_fun, :build_increase_fun, :build_options]
defmodule SimpleAlphaNumericBuilder do
def build(options) 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
end
def new_temp_refs(options \\ []) do
%__MODULE__{
refs: Keyword.get(options, :init_refs, []),
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])
}
end
def janitor_refs(state = %__MODULE__{}) do
if length(state.refs) > state.max do
%__MODULE__{refs: state.refs |> Enum.reverse() |> tl() |> Enum.reverse()}
else
state
end
end
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]}}
else
{nil, state}
end
end
def lookup_temp_ref(key, state, default \\ nil) do
case List.keyfind(state.refs, key, 0) do
{_, _, data} -> data
_ -> default
end
end
defp new_nonexisting_key(state, i) when i > 50 do
nil
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
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/nola_plugins/kick_roulette_plugin.ex b/lib/nola_plugins/kick_roulette.ex
similarity index 94%
rename from lib/nola_plugins/kick_roulette_plugin.ex
rename to lib/nola_plugins/kick_roulette.ex
index 55b7da4..f6d2ba2 100644
--- a/lib/nola_plugins/kick_roulette_plugin.ex
+++ b/lib/nola_plugins/kick_roulette.ex
@@ -1,32 +1,32 @@
-defmodule Nola.IRC.KickRoulettePlugin do
+defmodule Nola.Plugins.KickRoulette do
@moduledoc """
# kick roulette
* **!kick**, tentez votre chance…
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:kick", [plugin: __MODULE__])
{:ok, nil}
end
def handle_info({:irc, :trigger, "kick", message = %{trigger: %{type: :bang, args: []}}}, _) do
if 5 == :crypto.rand_uniform(1, 6) do
spawn(fn() ->
:timer.sleep(:crypto.rand_uniform(200, 10_000))
message.replyfun.({:kick, message.sender.nick, "perdu"})
end)
end
{:noreply, nil}
end
def handle_info(msg, _) do
{:noreply, nil}
end
end
diff --git a/lib/nola_plugins/last_fm_plugin.ex b/lib/nola_plugins/last_fm.ex
similarity index 99%
rename from lib/nola_plugins/last_fm_plugin.ex
rename to lib/nola_plugins/last_fm.ex
index 03df675..1a9b7dd 100644
--- a/lib/nola_plugins/last_fm_plugin.ex
+++ b/lib/nola_plugins/last_fm.ex
@@ -1,187 +1,187 @@
-defmodule Nola.IRC.LastFmPlugin do
+defmodule Nola.Plugins.LastFm do
require Logger
@moduledoc """
# last.fm
* **!lastfm|np `[nick|username]`**
* **.lastfm|np**
* **+lastfm, -lastfm `<username last.fm>; ?lastfm`** Configurer un nom d'utilisateur last.fm
"""
@single_trigger ~w(lastfm np)
@pubsub_topics ~w(trigger:lastfm trigger:np)
defstruct dets: nil
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [type: __MODULE__]
for t <- @pubsub_topics, do: {:ok, _} = Registry.register(IRC.PubSub, t, type: __MODULE__)
dets_filename = (Nola.data_path() <> "/" <> "lastfm.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [])
{:ok, %__MODULE__{dets: dets}}
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :plus, args: [username]}}}, state) do
username = String.strip(username)
:ok = :dets.insert(state.dets, {message.account.id, username})
message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm configuré: \"#{username}\".")
{:noreply, state}
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :minus, args: []}}}, state) do
text = case :dets.lookup(state.dets, message.account.id) do
[{_nick, _username}] ->
:dets.delete(state.dets, message.account.id)
message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm enlevé.")
_ -> nil
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :query, args: []}}}, state) do
text = case :dets.lookup(state.dets, message.account.id) do
[{_nick, username}] ->
message.replyfun.("#{message.sender.nick}: #{username}.")
_ -> nil
end
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: []}}}, state) do
irc_now_playing(message.account.id, message, state)
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: [nick_or_user]}}}, state) do
irc_now_playing(nick_or_user, message, state)
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :dot}}}, state) do
members = IRC.Membership.members(message.network, message.channel)
foldfun = fn({nick, user}, acc) -> [{nick,user}|acc] end
usernames = :dets.foldl(foldfun, [], state.dets)
|> Enum.uniq()
|> Enum.filter(fn({acct,_}) -> Enum.member?(members, acct) end)
|> Enum.map(fn({_, u}) -> u end)
for u <- usernames, do: irc_now_playing(u, message, state)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
def terminate(_reason, state) do
if state.dets do
:dets.sync(state.dets)
:dets.close(state.dets)
end
:ok
end
defp irc_now_playing(nick_or_user, message, state) do
nick_or_user = String.strip(nick_or_user)
id_or_user = if account = IRC.Account.get(nick_or_user) || IRC.Account.find_always_by_nick(message.network, message.channel, nick_or_user) do
account.id
else
nick_or_user
end
username = case :dets.lookup(state.dets, id_or_user) do
[{_, username}] -> username
_ -> id_or_user
end
case now_playing(username) do
{:error, text} when is_binary(text) ->
message.replyfun.(text)
{:ok, map} when is_map(map) ->
track = fetch_track(username, map)
text = format_now_playing(map, track)
user = if account = IRC.Account.get(id_or_user) do
user = IRC.UserTrack.find_by_account(message.network, account)
if(user, do: user.nick, else: account.name)
else
username
end
if user && text do
message.replyfun.("#{user} #{text}")
else
message.replyfun.("#{username}: pas de résultat")
end
other ->
message.replyfun.("erreur :(")
end
end
defp now_playing(user) do
api = Application.get_env(:nola, :lastfm)[:api_key]
url = "http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&format=json&limit=1&extended=1" <> "&api_key=" <> api <> "&user="<> user
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} -> Jason.decode(body)
{:ok, %HTTPoison.Response{status_code: 404}} -> {:error, "last.fm: utilisateur \"#{user}\" inexistant"}
{:ok, %HTTPoison.Response{status_code: code}} -> {:error, "last.fm: erreur #{to_string(code)}"}
error ->
Logger.error "Lastfm http error: #{inspect error}"
:error
end
end
defp fetch_track(user, %{"recenttracks" => %{"track" => [ t = %{"name" => name, "artist" => %{"name" => artist}} | _]}}) do
api = Application.get_env(:nola, :lastfm)[:api_key]
url = "http://ws.audioscrobbler.com/2.0/?method=track.getInfo&format=json" <> "&api_key=" <> api <> "&username="<> user <> "&artist="<>URI.encode(artist)<>"&track="<>URI.encode(name)
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
case Jason.decode(body) do
{:ok, body} -> body["track"] || %{}
_ -> %{}
end
error ->
Logger.error "Lastfm http error: #{inspect error}"
:error
end
end
defp format_now_playing(%{"recenttracks" => %{"track" => [track = %{"@attr" => %{"nowplaying" => "true"}} | _]}}, et) do
format_track(true, track, et)
end
defp format_now_playing(%{"recenttracks" => %{"track" => [track | _]}}, et) do
format_track(false, track, et)
end
defp format_now_playing(%{"error" => err, "message" => message}, _) do
"last.fm error #{err}: #{message}"
end
defp format_now_playing(miss) do
nil
end
defp format_track(np, track, extended) do
artist = track["artist"]["name"]
album = if track["album"]["#text"], do: " (" <> track["album"]["#text"] <> ")", else: ""
name = track["name"] <> album
action = if np, do: "écoute ", else: "a écouté"
love = if track["loved"] != "0", do: "❤️"
count = if x = extended["userplaycount"], do: "x#{x} #{love}"
tags = (get_in(extended, ["toptags", "tag"]) || [])
|> Enum.map(fn(tag) -> tag["name"] end)
|> Enum.filter(& &1)
|> Enum.join(", ")
[action, artist, name, count, tags, track["url"]]
|> Enum.filter(& &1)
|> Enum.map(&String.trim(&1))
|> Enum.join(" - ")
end
end
diff --git a/lib/nola_plugins/link_plugin.ex b/lib/nola_plugins/link.ex
similarity index 96%
rename from lib/nola_plugins/link_plugin.ex
rename to lib/nola_plugins/link.ex
index dee78e8..381bdab 100644
--- a/lib/nola_plugins/link_plugin.ex
+++ b/lib/nola_plugins/link.ex
@@ -1,271 +1,271 @@
-defmodule Nola.IRC.LinkPlugin do
+defmodule Nola.Plugins.Link do
@moduledoc """
# Link Previewer
An extensible link previewer for IRC.
To extend the supported sites, create a new handler implementing the callbacks.
- See `link_plugin/` directory for examples. The first in list handler that returns true to the `match/2` callback will be used,
+ See `link/` directory. The first in list handler that returns true to the `match/2` callback will be used,
and if the handler returns `:error` or crashes, will fallback to the default preview.
Unsupported websites will use the default link preview method, which is for html document the title, otherwise it'll use
the mimetype and size.
## Configuration:
```
- config :nola, Nola.IRC.LinkPlugin,
+ config :nola, Nola.Plugins.Link,
handlers: [
- Nola.IRC.LinkPlugin.Youtube: [
+ Nola.Plugins.Link.Youtube: [
invidious: true
],
- Nola.IRC.LinkPlugin.Twitter: [],
- Nola.IRC.LinkPlugin.Imgur: [],
+ Nola.Plugins.Link.Twitter: [],
+ Nola.Plugins.Link.Imgur: [],
]
```
"""
@ircdoc """
# Link preview
Previews links (just post a link!).
Announces real URL after redirections and provides extended support for YouTube, Twitter and Imgur.
"""
def short_irc_doc, do: false
def irc_doc, do: @ircdoc
require Logger
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@callback match(uri :: URI.t, options :: Keyword.t) :: {true, params :: Map.t} | false
@callback expand(uri :: URI.t, params :: Map.t, options :: Keyword.t) :: {:ok, lines :: [] | String.t} | :error
@callback post_match(uri :: URI.t, content_type :: binary, headers :: [], opts :: Keyword.t) :: {:body | :file, params :: Map.t} | false
@callback post_expand(uri :: URI.t, body :: binary() | Path.t, params :: Map.t, options :: Keyword.t) :: {:ok, lines :: [] | String.t} | :error
@optional_callbacks [expand: 3, post_expand: 4]
defstruct [:client]
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "messages", [plugin: __MODULE__])
#{:ok, _} = Registry.register(IRC.PubSub, "messages:telegram", [plugin: __MODULE__])
Logger.info("Link handler started")
{:ok, %__MODULE__{}}
end
def handle_info({:irc, :text, message = %{text: text}}, state) do
String.split(text)
|> Enum.map(fn(word) ->
if String.starts_with?(word, "http://") || String.starts_with?(word, "https://") do
uri = URI.parse(word)
if uri.scheme && uri.host do
spawn(fn() ->
:timer.kill_after(:timer.seconds(30))
case expand_link([uri]) do
{:ok, uris, text} ->
text = case uris do
[uri] -> text
[luri | _] ->
if luri.host == uri.host && luri.path == luri.path do
text
else
["-> #{URI.to_string(luri)}", text]
end
end
if is_list(text) do
for line <- text, do: message.replyfun.(line)
else
message.replyfun.(text)
end
_ -> nil
end
end)
end
end
end)
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
def terminate(_reason, state) do
:ok
end
# 1. Match the first valid handler
# 2. Try to run the handler
# 3. If :error or crash, default link.
# If :skip, nothing
# 4. ?
# Over five redirections: cancel.
def expand_link(acc = [_, _, _, _, _ | _]) do
{:ok, acc, "link redirects more than five times"}
end
def expand_link(acc=[uri | _]) do
Logger.debug("link: expanding: #{inspect uri}")
handlers = Keyword.get(Application.get_env(:nola, __MODULE__, [handlers: []]), :handlers)
handler = Enum.reduce_while(handlers, nil, fn({module, opts}, acc) ->
Logger.debug("link: attempt expanding: #{inspect module} for #{inspect uri}")
module = Module.concat([module])
case module.match(uri, opts) do
{true, params} -> {:halt, {module, params, opts}}
false -> {:cont, acc}
end
end)
run_expand(acc, handler)
end
def run_expand(acc, nil) do
expand_default(acc)
end
def run_expand(acc=[uri|_], {module, params, opts}) do
Logger.debug("link: expanding #{inspect uri} with #{inspect module}")
case module.expand(uri, params, opts) do
{:ok, data} -> {:ok, acc, data}
:error -> expand_default(acc)
:skip -> nil
end
rescue
e ->
Logger.error("link: rescued #{inspect uri} with #{inspect module}: #{inspect e}")
Logger.error(Exception.format(:error, e, __STACKTRACE__))
expand_default(acc)
catch
e, b ->
Logger.error("link: catched #{inspect uri} with #{inspect module}: #{inspect {e, b}}")
expand_default(acc)
end
defp get(url, headers \\ [], options \\ []) do
get_req(url, :hackney.get(url, headers, <<>>, options))
end
defp get_req(_, {:error, reason}) do
{:error, reason}
end
defp get_req(url, {:ok, 200, headers, client}) do
headers = Enum.reduce(headers, %{}, fn({key, value}, acc) ->
Map.put(acc, String.downcase(key), value)
end)
content_type = Map.get(headers, "content-type", "application/octect-stream")
length = Map.get(headers, "content-length", "0")
{length, _} = Integer.parse(length)
handlers = Keyword.get(Application.get_env(:nola, __MODULE__, [handlers: []]), :handlers)
handler = Enum.reduce_while(handlers, false, fn({module, opts}, acc) ->
module = Module.concat([module])
try do
case module.post_match(url, content_type, headers, opts) do
{mode, params} when mode in [:body, :file] -> {:halt, {module, params, opts, mode}}
false -> {:cont, acc}
end
rescue
e ->
Logger.error(inspect(e))
{:cont, false}
catch
e, b ->
Logger.error(inspect({b}))
{:cont, false}
end
end)
cond do
handler != false and length <= 30_000_000 ->
case get_body(url, 30_000_000, client, handler, <<>>) do
{:ok, _} = ok -> ok
:error ->
{:ok, "file: #{content_type}, size: #{human_size(length)}"}
end
#String.starts_with?(content_type, "text/html") && length <= 30_000_000 ->
# get_body(url, 30_000_000, client, <<>>)
true ->
:hackney.close(client)
{:ok, "file: #{content_type}, size: #{human_size(length)}"}
end
end
defp get_req(_, {:ok, redirect, headers, client}) when redirect in 300..399 do
headers = Enum.reduce(headers, %{}, fn({key, value}, acc) ->
Map.put(acc, String.downcase(key), value)
end)
location = Map.get(headers, "location")
:hackney.close(client)
{:redirect, location}
end
defp get_req(_, {:ok, status, headers, client}) do
:hackney.close(client)
{:error, status, headers}
end
defp get_body(url, len, client, {handler, params, opts, mode} = h, acc) when len >= byte_size(acc) do
case :hackney.stream_body(client) do
{:ok, data} ->
get_body(url, len, client, h, << acc::binary, data::binary >>)
:done ->
body = case mode do
:body -> acc
:file ->
{:ok, tmpfile} = Plug.Upload.random_file("linkplugin")
File.write!(tmpfile, acc)
tmpfile
end
handler.post_expand(url, body, params, opts)
{:error, reason} ->
{:ok, "failed to fetch body: #{inspect reason}"}
end
end
defp get_body(_, len, client, h, _acc) do
:hackney.close(client)
IO.inspect(h)
{:ok, "Error: file over 30"}
end
def expand_default(acc = [uri = %URI{scheme: scheme} | _]) when scheme in ["http", "https"] do
Logger.debug("link: expanding #{uri} with default")
headers = [{"user-agent", "DmzBot (like TwitterBot)"}]
options = [follow_redirect: false, max_body_length: 30_000_000]
case get(URI.to_string(uri), headers, options) do
{:ok, text} ->
{:ok, acc, text}
{:redirect, link} ->
new_uri = URI.parse(link)
#new_uri = %URI{new_uri | scheme: scheme, authority: uri.authority, host: uri.host, port: uri.port}
expand_link([new_uri | acc])
{:error, status, _headers} ->
text = Plug.Conn.Status.reason_phrase(status)
{:ok, acc, "Error: HTTP #{text} (#{status})"}
{:error, {:tls_alert, {:handshake_failure, err}}} ->
{:ok, acc, "TLS Error: #{to_string(err)}"}
{:error, reason} ->
{:ok, acc, "Error: #{to_string(reason)}"}
end
end
# Unsupported scheme, came from a redirect.
def expand_default(acc = [uri | _]) do
{:ok, [uri], "-> #{URI.to_string(uri)}"}
end
defp human_size(bytes) do
bytes
|> FileSize.new(:b)
|> FileSize.scale()
|> FileSize.format()
end
end
diff --git a/lib/nola_plugins/link_plugin/github.ex b/lib/nola_plugins/link/github.ex
similarity index 95%
rename from lib/nola_plugins/link_plugin/github.ex
rename to lib/nola_plugins/link/github.ex
index 93e0892..0069a40 100644
--- a/lib/nola_plugins/link_plugin/github.ex
+++ b/lib/nola_plugins/link/github.ex
@@ -1,49 +1,49 @@
-defmodule Nola.IRC.LinkPlugin.Github do
- @behaviour Nola.IRC.LinkPlugin
+defmodule Nola.Plugins.Link.Github do
+ @behaviour Nola.Plugins.Link
@impl true
def match(uri = %URI{host: "github.com", path: path}, _) do
case String.split(path, "/") do
["", user, repo] ->
{true, %{user: user, repo: repo, path: "#{user}/#{repo}"}}
_ ->
false
end
end
def match(_, _), do: false
@impl true
def post_match(_, _, _, _), do: false
@impl true
def expand(_uri, %{user: user, repo: repo}, _opts) do
case HTTPoison.get("https://api.github.com/repos/#{user}/#{repo}") do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, json} = Jason.decode(body)
src = json["source"]["full_name"]
disabled = if(json["disabled"], do: " (disabled)", else: "")
archived = if(json["archived"], do: " (archived)", else: "")
fork = if src && src != json["full_name"] do
" (⑂ #{json["source"]["full_name"]})"
else
""
end
start = "#{json["full_name"]}#{disabled}#{archived}#{fork} - #{json["description"]}"
tags = for(t <- json["topics"]||[], do: "##{t}") |> Enum.intersperse(", ") |> Enum.join("")
lang = if(json["language"], do: "#{json["language"]} - ", else: "")
issues = if(json["open_issues_count"], do: "#{json["open_issues_count"]} issues - ", else: "")
last_push = if at = json["pushed_at"] do
{:ok, date, _} = DateTime.from_iso8601(at)
" - last pushed #{DateTime.to_string(date)}"
else
""
end
network = "#{lang}#{issues}#{json["stargazers_count"]} stars - #{json["subscribers_count"]} watchers - #{json["forks_count"]} forks#{last_push}"
{:ok, [start, tags, network]}
other ->
:error
end
end
end
diff --git a/lib/nola_plugins/link_plugin/html.ex b/lib/nola_plugins/link/html.ex
similarity index 97%
rename from lib/nola_plugins/link_plugin/html.ex
rename to lib/nola_plugins/link/html.ex
index 56a8ceb..9b44319 100644
--- a/lib/nola_plugins/link_plugin/html.ex
+++ b/lib/nola_plugins/link/html.ex
@@ -1,106 +1,106 @@
-defmodule Nola.IRC.LinkPlugin.HTML do
- @behaviour Nola.IRC.LinkPlugin
+defmodule Nola.Plugins.Link.HTML do
+ @behaviour Nola.Plugins.Link
@impl true
def match(_, _), do: false
@impl true
def post_match(_url, "text/html"<>_, _header, _opts) do
{:body, nil}
end
def post_match(_, _, _, _), do: false
@impl true
def post_expand(url, body, _params, _opts) do
html = Floki.parse(body)
title = collect_title(html)
opengraph = collect_open_graph(html)
itemprops = collect_itemprops(html)
text = if Map.has_key?(opengraph, "title") && Map.has_key?(opengraph, "description") do
sitename = if sn = Map.get(opengraph, "site_name") do
"#{sn}"
else
""
end
paywall? = if Map.get(opengraph, "article:content_tier", Map.get(itemprops, "article:content_tier", "free")) == "free" do
""
else
"[paywall] "
end
section = if section = Map.get(opengraph, "article:section", Map.get(itemprops, "article:section", nil)) do
": #{section}"
else
""
end
date = case DateTime.from_iso8601(Map.get(opengraph, "article:published_time", Map.get(itemprops, "article:published_time", ""))) do
{:ok, date, _} ->
"#{Timex.format!(date, "%d/%m/%y", :strftime)}. "
_ ->
""
end
uri = URI.parse(url)
prefix = "#{paywall?}#{Map.get(opengraph, "site_name", uri.host)}#{section}"
prefix = unless prefix == "" do
"#{prefix} — "
else
""
end
[clean_text("#{prefix}#{Map.get(opengraph, "title")}")] ++ IRC.splitlong(clean_text("#{date}#{Map.get(opengraph, "description")}"))
else
clean_text(title)
end
{:ok, text}
end
defp collect_title(html) do
case Floki.find(html, "title") do
[{"title", [], [title]} | _] ->
String.trim(title)
_ ->
nil
end
end
defp collect_open_graph(html) do
Enum.reduce(Floki.find(html, "head meta"), %{}, fn(tag, acc) ->
case tag do
{"meta", values, []} ->
name = List.keyfind(values, "property", 0, {nil, nil}) |> elem(1)
content = List.keyfind(values, "content", 0, {nil, nil}) |> elem(1)
case name do
"og:" <> key ->
Map.put(acc, key, content)
"article:"<>_ ->
Map.put(acc, name, content)
_other -> acc
end
_other -> acc
end
end)
end
defp collect_itemprops(html) do
Enum.reduce(Floki.find(html, "[itemprop]"), %{}, fn(tag, acc) ->
case tag do
{"meta", values, []} ->
name = List.keyfind(values, "itemprop", 0, {nil, nil}) |> elem(1)
content = List.keyfind(values, "content", 0, {nil, nil}) |> elem(1)
case name do
"article:" <> key ->
Map.put(acc, name, content)
_other -> acc
end
_other -> acc
end
end)
end
defp clean_text(text) do
text
|> String.replace("\n", " ")
|> HtmlEntities.decode()
end
end
diff --git a/lib/nola_plugins/link_plugin/imgur.ex b/lib/nola_plugins/link/imgur.ex
similarity index 97%
rename from lib/nola_plugins/link_plugin/imgur.ex
rename to lib/nola_plugins/link/imgur.ex
index 5d74956..9fe9354 100644
--- a/lib/nola_plugins/link_plugin/imgur.ex
+++ b/lib/nola_plugins/link/imgur.ex
@@ -1,96 +1,96 @@
-defmodule Nola.IRC.LinkPlugin.Imgur do
- @behaviour Nola.IRC.LinkPlugin
+defmodule Nola.Plugins.Link.Imgur do
+ @behaviour Nola.Plugins.Link
@moduledoc """
# Imgur link preview
No options.
Needs to have a Imgur API key configured:
```
config :nola, :imgur,
client_id: "xxxxxxxx",
client_secret: "xxxxxxxxxxxxxxxxxxxx"
```
"""
@impl true
def match(uri = %URI{host: "imgur.io"}, arg) do
match(%URI{uri | host: "imgur.com"}, arg)
end
def match(uri = %URI{host: "i.imgur.io"}, arg) do
match(%URI{uri | host: "i.imgur.com"}, arg)
end
def match(uri = %URI{host: "imgur.com", path: "/a/"<>album_id}, _) do
{true, %{album_id: album_id}}
end
def match(uri = %URI{host: "imgur.com", path: "/gallery/"<>album_id}, _) do
{true, %{album_id: album_id}}
end
def match(uri = %URI{host: "i.imgur.com", path: "/"<>image}, _) do
[hash, _] = String.split(image, ".", parts: 2)
{true, %{image_id: hash}}
end
def match(_, _), do: false
@impl true
def post_match(_, _, _, _), do: false
def expand(_uri, %{album_id: album_id}, opts) do
expand_imgur_album(album_id, opts)
end
def expand(_uri, %{image_id: image_id}, opts) do
expand_imgur_image(image_id, opts)
end
def expand_imgur_image(image_id, opts) do
client_id = Keyword.get(Application.get_env(:nola, :imgur, []), :client_id, "42")
headers = [{"Authorization", "Client-ID #{client_id}"}]
options = []
case HTTPoison.get("https://api.imgur.com/3/image/#{image_id}", headers, options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, json} = Jason.decode(body)
data = json["data"]
title = String.slice(data["title"] || data["description"], 0, 180)
nsfw = if data["nsfw"], do: "(NSFW) - ", else: " "
height = Map.get(data, "height")
width = Map.get(data, "width")
size = Map.get(data, "size")
{:ok, "image, #{width}x#{height}, #{size} bytes #{nsfw}#{title}"}
other ->
:error
end
end
def expand_imgur_album(album_id, opts) do
client_id = Keyword.get(Application.get_env(:nola, :imgur, []), :client_id, "42")
headers = [{"Authorization", "Client-ID #{client_id}"}]
options = []
case HTTPoison.get("https://api.imgur.com/3/album/#{album_id}", headers, options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, json} = Jason.decode(body)
data = json["data"]
title = data["title"]
nsfw = data["nsfw"]
nsfw = if nsfw, do: "(NSFW) - ", else: ""
if data["images_count"] == 1 do
[image] = data["images"]
title = if title || data["title"] do
title = [title, data["title"]] |> Enum.filter(fn(x) -> x end) |> Enum.uniq() |> Enum.join(" — ")
"#{title} — "
else
""
end
{:ok, "#{nsfw}#{title}#{image["link"]}"}
else
title = if title, do: title, else: "Untitled album"
{:ok, "#{nsfw}#{title} - #{data["images_count"]} images"}
end
other ->
:error
end
end
end
diff --git a/lib/nola_plugins/link_plugin/pdf.ex b/lib/nola_plugins/link/pdf.ex
similarity index 92%
rename from lib/nola_plugins/link_plugin/pdf.ex
rename to lib/nola_plugins/link/pdf.ex
index 5f72ef5..e91dcc2 100644
--- a/lib/nola_plugins/link_plugin/pdf.ex
+++ b/lib/nola_plugins/link/pdf.ex
@@ -1,39 +1,39 @@
-defmodule Nola.IRC.LinkPlugin.PDF do
+defmodule Nola.Plugins.Link.PDF do
require Logger
- @behaviour Nola.IRC.LinkPlugin
+ @behaviour Nola.Plugins.Link
@impl true
def match(_, _), do: false
@impl true
def post_match(_url, "application/pdf"<>_, _header, _opts) do
{:file, nil}
end
def post_match(_, _, _, _), do: false
@impl true
def post_expand(url, file, _, _) do
case System.cmd("pdftitle", ["-p", file]) do
{text, 0} ->
text = text
|> String.trim()
if text == "" do
:error
else
basename = Path.basename(url, ".pdf")
text = "[#{basename}] " <> text
|> String.split("\n")
{:ok, text}
end
{_, 127} ->
Logger.error("dependency `pdftitle` is missing, please install it: `pip3 install pdftitle`.")
:error
{error, code} ->
Logger.warn("command `pdftitle` exited with status code #{code}:\n#{inspect error}")
:error
end
end
end
diff --git a/lib/nola_plugins/link_plugin/redacted.ex b/lib/nola_plugins/link/redacted.ex
similarity index 81%
rename from lib/nola_plugins/link_plugin/redacted.ex
rename to lib/nola_plugins/link/redacted.ex
index 7a6229d..a7cfe74 100644
--- a/lib/nola_plugins/link_plugin/redacted.ex
+++ b/lib/nola_plugins/link/redacted.ex
@@ -1,18 +1,18 @@
-defmodule Nola.IRC.LinkPlugin.Redacted do
- @behaviour Nola.IRC.LinkPlugin
+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
%{"id" => id} = URI.decode_query(id)
{true, %{torrent: id}}
end
def match(_, _), do: false
@impl true
def post_match(_, _, _, _), do: false
def expand(_uri, %{torrent: id}, _opts) do
end
end
diff --git a/lib/nola_plugins/link_plugin/reddit.ex b/lib/nola_plugins/link/reddit.ex
similarity index 98%
rename from lib/nola_plugins/link_plugin/reddit.ex
rename to lib/nola_plugins/link/reddit.ex
index 79102e0..016e025 100644
--- a/lib/nola_plugins/link_plugin/reddit.ex
+++ b/lib/nola_plugins/link/reddit.ex
@@ -1,119 +1,119 @@
-defmodule Nola.IRC.LinkPlugin.Reddit do
- @behaviour Nola.IRC.LinkPlugin
+defmodule Nola.Plugins.Link.Reddit do
+ @behaviour Nola.Plugins.Link
@impl true
def match(uri = %URI{host: "reddit.com", path: path}, _) do
case String.split(path, "/") do
["", "r", sub, "comments", post_id, _slug] ->
{true, %{mode: :post, path: path, sub: sub, post_id: post_id}}
["", "r", sub, "comments", post_id, _slug, ""] ->
{true, %{mode: :post, path: path, sub: sub, post_id: post_id}}
["", "r", sub, ""] ->
{true, %{mode: :sub, path: path, sub: sub}}
["", "r", sub] ->
{true, %{mode: :sub, path: path, sub: sub}}
# ["", "u", user] ->
# {true, %{mode: :user, path: path, user: user}}
_ ->
false
end
end
def match(uri = %URI{host: host, path: path}, opts) do
if String.ends_with?(host, ".reddit.com") do
match(%URI{uri | host: "reddit.com"}, opts)
else
false
end
end
@impl true
def post_match(_, _, _, _), do: false
@impl true
def expand(_, %{mode: :sub, sub: sub}, _opts) do
url = "https://api.reddit.com/r/#{sub}/about"
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
sr = Jason.decode!(body)
|> Map.get("data")
|> IO.inspect(limit: :infinity)
description = Map.get(sr, "public_description")||Map.get(sr, "description", "")
|> String.split("\n")
|> List.first()
name = if title = Map.get(sr, "title") do
Map.get(sr, "display_name_prefixed") <> ": " <> title
else
Map.get(sr, "display_name_prefixed")
end
nsfw = if Map.get(sr, "over18") do
"[NSFW] "
else
""
end
quarantine = if Map.get(sr, "quarantine") do
"[Quarantined] "
else
""
end
count = "#{Map.get(sr, "subscribers")} subscribers, #{Map.get(sr, "active_user_count")} active"
preview = "#{quarantine}#{nsfw}#{name} — #{description} (#{count})"
{:ok, preview}
_ ->
:error
end
end
def expand(_uri, %{mode: :post, path: path, sub: sub, post_id: post_id}, _opts) do
case HTTPoison.get("https://api.reddit.com#{path}?sr_detail=true") do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
json = Jason.decode!(body)
op = List.first(json)
|> Map.get("data")
|> Map.get("children")
|> List.first()
|> Map.get("data")
|> IO.inspect(limit: :infinity)
sr = get_in(op, ["sr_detail", "display_name_prefixed"])
{self?, url} = if Map.get(op, "selftext") == "" do
{false, Map.get(op, "url")}
else
{true, nil}
end
self_str = if(self?, do: "text", else: url)
up = Map.get(op, "ups")
down = Map.get(op, "downs")
comments = Map.get(op, "num_comments")
nsfw = if Map.get(op, "over_18") do
"[NSFW] "
else
""
end
state = cond do
Map.get(op, "hidden") -> "hidden"
Map.get(op, "archived") -> "archived"
Map.get(op, "locked") -> "locked"
Map.get(op, "quarantine") -> "quarantined"
Map.get(op, "removed_by") || Map.get(op, "removed_by_category") -> "removed"
Map.get(op, "banned_by") -> "banned"
Map.get(op, "pinned") -> "pinned"
Map.get(op, "stickied") -> "stickied"
true -> nil
end
flair = if flair = Map.get(op, "link_flair_text") do
"[#{flair}] "
else
""
end
title = "#{nsfw}#{sr}: #{flair}#{Map.get(op, "title")}"
state_str = if(state, do: "#{state}, ")
content = "by u/#{Map.get(op, "author")} - #{state_str}#{up} up, #{down} down, #{comments} comments - #{self_str}"
{:ok, [title, content]}
err ->
:error
end
end
end
diff --git a/lib/nola_plugins/link_plugin/twitter.ex b/lib/nola_plugins/link/twitter.ex
similarity index 98%
rename from lib/nola_plugins/link_plugin/twitter.ex
rename to lib/nola_plugins/link/twitter.ex
index 640b193..e7f3e63 100644
--- a/lib/nola_plugins/link_plugin/twitter.ex
+++ b/lib/nola_plugins/link/twitter.ex
@@ -1,158 +1,158 @@
-defmodule Nola.IRC.LinkPlugin.Twitter do
- @behaviour Nola.IRC.LinkPlugin
+defmodule Nola.Plugins.Link.Twitter do
+ @behaviour Nola.Plugins.Link
@moduledoc """
# Twitter Link Preview
Configuration:
needs an API key and auth tokens:
```
config :extwitter, :oauth, [
consumer_key: "zzzzz",
consumer_secret: "xxxxxxx",
access_token: "yyyyyy",
access_token_secret: "ssshhhhhh"
]
```
options:
* `expand_quoted`: Add the quoted tweet instead of its URL. Default: true.
"""
def match(uri = %URI{host: twitter, path: path}, _opts) when twitter in ["twitter.com", "m.twitter.com", "mobile.twitter.com"] do
case String.split(path, "/", parts: 4) do
["", _username, "status", status_id] ->
{status_id, _} = Integer.parse(status_id)
{true, %{status_id: status_id}}
_ -> false
end
end
def match(_, _), do: false
@impl true
def post_match(_, _, _, _), do: false
def expand(_uri, %{status_id: status_id}, opts) do
expand_tweet(ExTwitter.show(status_id, tweet_mode: "extended"), opts)
end
defp expand_tweet(nil, _opts) do
:error
end
defp link_tweet(tweet_or_screen_id_tuple, opts, force_twitter_com \\ false)
defp link_tweet({screen_name, id}, opts, force_twitter_com) do
path = "/#{screen_name}/status/#{id}"
nitter = Keyword.get(opts, :nitter)
host = if !force_twitter_com && nitter, do: nitter, else: "twitter.com"
"https://#{host}/#{screen_name}/status/#{id}"
end
defp link_tweet(tweet, opts, force_twitter_com) do
link_tweet({tweet.user.screen_name, tweet.id}, opts, force_twitter_com)
end
defp expand_tweet(tweet, opts) do
head = format_tweet_header(tweet, opts)
# Format tweet text
text = expand_twitter_text(tweet, opts)
text = if tweet.quoted_status do
quote_url = link_tweet(tweet.quoted_status, opts, true)
String.replace(text, quote_url, "")
else
text
end
text = IRC.splitlong(text)
reply_to = if tweet.in_reply_to_status_id do
reply_url = link_tweet({tweet.in_reply_to_screen_name, tweet.in_reply_to_status_id}, opts)
text = if tweet.in_reply_to_screen_name == tweet.user.screen_name, do: "continued from", else: "replying to"
<<3, 15, " ↪ ", text::binary, " ", reply_url::binary, 3>>
end
quoted = if tweet.quoted_status do
full_text = tweet.quoted_status
|> expand_twitter_text(opts)
|> IRC.splitlong_with_prefix(">")
head = format_tweet_header(tweet.quoted_status, opts, details: false, prefix: "↓ quoting")
[head | full_text]
else
[]
end
#<<2, "#{tweet.user.name} (@#{tweet.user.screen_name})", 2, " ", 3, 61, "#{foot} #{nitter_link}", 3>>, reply_to] ++ text ++ quoted
text = [head, reply_to | text] ++ quoted
|> Enum.filter(& &1)
{:ok, text}
end
defp expand_twitter_text(tweet, _opts) do
text = Enum.reduce(tweet.entities.urls, tweet.full_text, fn(entity, text) ->
String.replace(text, entity.url, entity.expanded_url)
end)
extended = tweet.extended_entities || %{media: []}
text = Enum.reduce(extended.media, text, fn(entity, text) ->
url = Enum.filter(extended.media, fn(e) -> entity.url == e.url end)
|> Enum.map(fn(e) ->
cond do
e.type == "video" -> e.expanded_url
true -> e.media_url_https
end
end)
|> Enum.join(" ")
String.replace(text, entity.url, url)
end)
|> HtmlEntities.decode()
end
defp format_tweet_header(tweet, opts, format_opts \\ []) do
prefix = Keyword.get(format_opts, :prefix, nil)
details = Keyword.get(format_opts, :details, true)
padded_prefix = if prefix, do: "#{prefix} ", else: ""
author = <<padded_prefix::binary, 2, "#{tweet.user.name} (@#{tweet.user.screen_name})", 2>>
link = link_tweet(tweet, opts)
{:ok, at} = Timex.parse(tweet.created_at, "%a %b %e %H:%M:%S %z %Y", :strftime)
{:ok, formatted_time} = Timex.format(at, "{relative}", :relative)
nsfw = if tweet.possibly_sensitive, do: <<3, 52, "NSFW", 3>>
rts = if tweet.retweet_count && tweet.retweet_count > 0, do: "#{tweet.retweet_count} RT"
likes = if tweet.favorite_count && tweet.favorite_count > 0, do: "#{tweet.favorite_count} ❤︎"
qrts = if tweet.quote_count && tweet.quote_count > 0, do: "#{tweet.quote_count} QRT"
replies = if tweet.reply_count && tweet.reply_count > 0, do: "#{tweet.reply_count} Reps"
dmcad = if tweet.withheld_copyright, do: <<3, 52, "DMCA", 3>>
withheld_local = if tweet.withheld_in_countries && length(tweet.withheld_in_countries) > 0 do
"Withheld in #{length(tweet.withheld_in_countries)} countries"
end
verified = if tweet.user.verified, do: <<3, 51, "✔", 3>>
meta = if details do
[verified, nsfw, formatted_time, dmcad, withheld_local, rts, qrts, likes, replies]
else
[verified, nsfw, formatted_time, dmcad, withheld_local]
end
meta = meta
|> Enum.filter(& &1)
|> Enum.join(" - ")
meta = <<3, 15, meta::binary, " → #{link}", 3>>
<<author::binary, " — ", meta::binary>>
end
end
diff --git a/lib/nola_plugins/link_plugin/youtube.ex b/lib/nola_plugins/link/youtube.ex
similarity index 96%
rename from lib/nola_plugins/link_plugin/youtube.ex
rename to lib/nola_plugins/link/youtube.ex
index f7c7541..1b14221 100644
--- a/lib/nola_plugins/link_plugin/youtube.ex
+++ b/lib/nola_plugins/link/youtube.ex
@@ -1,72 +1,72 @@
-defmodule Nola.IRC.LinkPlugin.YouTube do
- @behaviour Nola.IRC.LinkPlugin
+defmodule Nola.Plugins.Link.YouTube do
+ @behaviour Nola.Plugins.Link
@moduledoc """
# YouTube link preview
needs an API key:
```
config :nola, :youtube,
api_key: "xxxxxxxxxxxxx"
```
options:
* `invidious`: Add a link to invidious.
"""
@impl true
def match(uri = %URI{host: yt, path: "/watch", query: "v="<>video_id}, _opts) when yt in ["youtube.com", "www.youtube.com"] do
{true, %{video_id: video_id}}
end
def match(%URI{host: "youtu.be", path: "/"<>video_id}, _opts) do
{true, %{video_id: video_id}}
end
def match(_, _), do: false
@impl true
def post_match(_, _, _, _), do: false
@impl true
def expand(uri, %{video_id: video_id}, opts) do
key = Application.get_env(:nola, :youtube)[:api_key]
params = %{
"part" => "snippet,contentDetails,statistics",
"id" => video_id,
"key" => key
}
headers = []
options = [params: params]
case HTTPoison.get("https://www.googleapis.com/youtube/v3/videos", [], options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
case Jason.decode(body) do
{:ok, json} ->
item = List.first(json["items"])
if item do
snippet = item["snippet"]
duration = item["contentDetails"]["duration"] |> String.replace("PT", "") |> String.downcase
date = snippet["publishedAt"]
|> DateTime.from_iso8601()
|> elem(1)
|> Timex.format("{relative}", :relative)
|> elem(1)
line = if host = Keyword.get(opts, :invidious) do
["-> https://#{host}/watch?v=#{video_id}"]
else
[]
end
{:ok, line ++ ["#{snippet["title"]}", "— #{duration} — uploaded by #{snippet["channelTitle"]} — #{date}"
<> " — #{item["statistics"]["viewCount"]} views, #{item["statistics"]["likeCount"]} likes"]}
else
:error
end
_ -> :error
end
end
end
end
diff --git a/lib/nola_plugins/logger_plugin.ex b/lib/nola_plugins/logger.ex
similarity index 94%
rename from lib/nola_plugins/logger_plugin.ex
rename to lib/nola_plugins/logger.ex
index b13f33a..69cac66 100644
--- a/lib/nola_plugins/logger_plugin.ex
+++ b/lib/nola_plugins/logger.ex
@@ -1,70 +1,71 @@
-defmodule Nola.IRC.LoggerPlugin do
+defmodule Nola.Plugins.Logger do
require Logger
@couch_db "bot-logs"
def irc_doc(), do: nil
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "triggers", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages", regopts)
+ {:ok, _} = Registry.register(IRC.PubSub, "messages:telegram", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "irc:outputs", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages:private", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, _, m}, state) do
{:noreply, log(m, state)}
end
def handle_info({:irc, :text, m}, state) do
{:noreply, log(m, state)}
end
def handle_info(info, state) do
Logger.debug("logger_plugin: unhandled info: #{info}")
{:noreply, state}
end
def log(entry, state) do
case Couch.post(@couch_db, format_to_db(entry)) do
{:ok, id, _rev} ->
Logger.debug("logger_plugin: saved: #{inspect id}")
state
error ->
Logger.error("logger_plugin: save failed: #{inspect error}")
end
rescue
e ->
Logger.error("logger_plugin: rescued processing for #{inspect entry}: #{inspect e}")
Logger.error(Exception.format(:error, e, __STACKTRACE__))
state
catch
e, b ->
Logger.error("logger_plugin: catched processing for #{inspect entry}: #{inspect e}")
Logger.error(Exception.format(e, b, __STACKTRACE__))
state
end
def format_to_db(msg = %IRC.Message{id: id}) do
msg
|> Poison.encode!()
|> Map.drop("id")
%{"_id" => id || FlakeId.get(),
"type" => "irc.message/v1",
"object" => msg}
end
def format_to_db(anything) do
%{"_id" => FlakeId.get(),
"type" => "object",
"object" => anything}
end
end
diff --git a/lib/nola_plugins/outline_plugin.ex b/lib/nola_plugins/outline.ex
similarity index 98%
rename from lib/nola_plugins/outline_plugin.ex
rename to lib/nola_plugins/outline.ex
index 820500e..f0ccfd2 100644
--- a/lib/nola_plugins/outline_plugin.ex
+++ b/lib/nola_plugins/outline.ex
@@ -1,108 +1,108 @@
-defmodule Nola.IRC.OutlinePlugin do
+defmodule Nola.Plugins.Outline do
@moduledoc """
# outline auto-link
Envoie un lien vers Outline quand un lien est envoyé.
* **!outline `<url>`** crée un lien outline pour `<url>`.
* **+outline `<host>`** active outline pour `<host>`.
* **-outline `<host>`** désactive outline pour `<host>`.
"""
def short_irc_doc, do: false
def irc_doc, do: @moduledoc
require Logger
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
defstruct [:file, :hosts]
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:outline", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages", regopts)
file = Path.join(Nola.data_path, "/outline.txt")
hosts = case File.read(file) do
{:error, :enoent} ->
[]
{:ok, lines} ->
String.split(lines, "\n", trim: true)
end
{:ok, %__MODULE__{file: file, hosts: hosts}}
end
def handle_info({:irc, :trigger, "outline", message = %IRC.Message{trigger: %IRC.Trigger{type: :plus, args: [host]}}}, state) do
state = %{state | hosts: [host | state.hosts]}
save(state)
message.replyfun.("ok")
{:noreply, state}
end
def handle_info({:irc, :trigger, "outline", message = %IRC.Message{trigger: %IRC.Trigger{type: :minus, args: [host]}}}, state) do
state = %{state | hosts: List.delete(state.hosts, host)}
save(state)
message.replyfun.("ok")
{:noreply, state}
end
def handle_info({:irc, :trigger, "outline", message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [url]}}}, state) do
line = "-> #{outline(url)}"
message.replyfun.(line)
end
def handle_info({:irc, :text, message = %IRC.Message{text: text}}, state) do
String.split(text)
|> Enum.map(fn(word) ->
if String.starts_with?(word, "http://") || String.starts_with?(word, "https://") do
uri = URI.parse(word)
if uri.scheme && uri.host do
if Enum.any?(state.hosts, fn(host) -> String.ends_with?(uri.host, host) end) do
outline_url = outline(word)
line = "-> #{outline_url}"
message.replyfun.(line)
end
end
end
end)
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
def save(state = %{file: file, hosts: hosts}) do
string = Enum.join(hosts, "\n")
File.write(file, string)
end
def outline(url) do
unexpanded = "https://outline.com/#{url}"
headers = [
{"User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:77.0) Gecko/20100101 Firefox/77.0"},
{"Accept", "*/*"},
{"Accept-Language", "en-US,en;q=0.5"},
{"Origin", "https://outline.com"},
{"DNT", "1"},
{"Referer", unexpanded},
{"Pragma", "no-cache"},
{"Cache-Control", "no-cache"}
]
params = %{"source_url" => url}
case HTTPoison.get("https://api.outline.com/v3/parse_article", headers, params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: json}} ->
body = Poison.decode!(json)
if Map.get(body, "success") do
code = get_in(body, ["data", "short_code"])
"https://outline.com/#{code}"
else
unexpanded
end
error ->
Logger.info("outline.com error: #{inspect error}")
unexpanded
end
end
end
diff --git a/lib/nola_plugins/preums_plugin.ex b/lib/nola_plugins/preums.ex
similarity index 98%
rename from lib/nola_plugins/preums_plugin.ex
rename to lib/nola_plugins/preums.ex
index f250e85..80c5af0 100644
--- a/lib/nola_plugins/preums_plugin.ex
+++ b/lib/nola_plugins/preums.ex
@@ -1,276 +1,276 @@
-defmodule Nola.IRC.PreumsPlugin do
+defmodule Nola.Plugins.Preums do
@moduledoc """
# preums !!!
* `!preums`: affiche le preums du jour
* `.preums`: stats des preums
"""
# WIP Scores
# L'idée c'est de donner un score pour mettre un peu de challenge en pénalisant les preums faciles.
#
# Un preums ne vaut pas 1 point, mais plutôt 0.10 ou 0.05, et on arrondi au plus proche. C'est un jeu sur le long
# terme. Un gros bonus pourrait apporter beaucoup de points.
#
# Il faudrait ces données:
# - moyenne des preums
# - activité récente du channel et par nb actifs d'utilisateurs
# (aggréger memberships+usertrack last_active ?)
# (faire des stats d'activité habituelle (un peu a la pisg) ?)
# - preums consécutifs
#
# Malus:
# - est proche de la moyenne en faible activité
# - trop consécutif de l'utilisateur sauf si activité
#
# Bonus:
# - plus le preums est éloigné de la moyenne
# - après 18h double
# - plus l'activité est élévée, exponentiel selon la moyenne
# - derns entre 4 et 6 (pourrait être adapté selon les stats d'activité)
#
# WIP Badges:
# - derns
# - streaks
# - faciles
# - ?
require Logger
@perfects [~r/preum(s|)/i]
# dets {{chan, day = {yyyy, mm, dd}}, nick, now, perfect?, text}
def all(dets) do
:dets.foldl(fn(i, acc) -> [i|acc] end, [], dets)
end
def all(dets, channel) do
fun = fn({{chan, date}, account_id, time, perfect, text}, acc) ->
if channel == chan do
[%{date: date, account_id: account_id, time: time, perfect: perfect, text: text} | acc]
else
acc
end
end
:dets.foldl(fun, [], dets)
end
def topnicks(dets, channel, options \\ []) do
sort_elem = case Keyword.get(options, :sort_by, :score) do
:score -> 1
:count -> 0
end
fun = fn(x = {{chan, date}, account_id, time, perfect, text}, acc) ->
if (channel == nil and chan) or (channel == chan) do
{count, points} = Map.get(acc, account_id, {0, 0})
score = score(chan, account_id, time, perfect, text)
Map.put(acc, account_id, {count + 1, points + score})
else
acc
end
end
:dets.foldl(fun, %{}, dets)
|> Enum.sort_by(fn({_account_id, value}) -> elem(value, sort_elem) end, &>=/2)
end
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def dets do
(Nola.data_path() <> "/preums.dets") |> String.to_charlist()
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "account", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "triggers", regopts)
{:ok, dets} = :dets.open_file(dets(), [{:repair, :force}])
Util.ets_mutate_select_each(:dets, dets, [{:"$1", [], [:"$1"]}], fn(table, obj) ->
{key, nick, now, perfect, text} = obj
case key do
{{net, {bork,chan}}, date} ->
:dets.delete(table, key)
nick = if IRC.Account.get(nick) do
nick
else
if acct = IRC.Account.find_always_by_nick(net, nil, nick) do
acct.id
else
nick
end
end
:dets.insert(table, { { {net,chan}, date }, nick, now, perfect, text})
{{_net, nil}, _} ->
:dets.delete(table, key)
{{net, chan}, date} ->
if !IRC.Account.get(nick) do
if acct = IRC.Account.find_always_by_nick(net, chan, nick) do
:dets.delete(table, key)
:dets.insert(table, { { {net,chan}, date }, acct.id, now, perfect, text})
end
end
_ ->
Logger.debug("DID NOT FIX: #{inspect key}")
end
end)
{:ok, %{dets: dets}}
end
# Latest
def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) do
channelkey = {m.network, m.channel}
state = handle_preums(m, state)
tz = timezone(channelkey)
{:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
date = {now.year, now.month, now.day}
key = {channelkey, date}
chan_cache = Map.get(state, channelkey, %{})
item = if i = Map.get(chan_cache, date) do
i
else
case :dets.lookup(state.dets, key) do
[item = {^key, _account_id, _now, _perfect, _text}] -> item
_ -> nil
end
end
if item do
{_, account_id, date, _perfect, text} = item
h = "#{date.hour}:#{date.minute}:#{date.second}"
account = IRC.Account.get(account_id)
user = IRC.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
m.replyfun.("preums: #{nick} à #{h}: “#{text}”")
end
{:noreply, state}
end
# Stats
def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :dot}}}, state) do
channel = {m.network, m.channel}
state = handle_preums(m, state)
top = topnicks(state.dets, channel, sort_by: :score)
|> Enum.map(fn({account_id, {count, score}}) ->
account = IRC.Account.get(account_id)
user = IRC.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
"#{nick}: #{score} (#{count})"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
msg = unless top == "" do
"top preums: #{top}"
else
"vous êtes tous nuls"
end
m.replyfun.(msg)
{:noreply, state}
end
# Help
def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :query}}}, state) do
state = handle_preums(m, state)
msg = "!preums - preums du jour, .preums top preumseurs"
m.replymsg.(msg)
{:noreply, state}
end
# Trigger fallback
def handle_info({:irc, :trigger, _, m = %IRC.Message{}}, state) do
state = handle_preums(m, state)
{:noreply, state}
end
# Message fallback
def handle_info({:irc, :text, m = %IRC.Message{}}, state) do
{:noreply, handle_preums(m, state)}
end
# Account
def handle_info({:account_change, old_id, new_id}, state) do
spec = [{{:_, :"$1", :_, :_, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
rename_object_owner(table, obj, new_id)
end)
{:noreply, state}
end
# Account: move from nick to account id
# FIXME: Doesn't seem to work.
def handle_info({:accounts, accounts}, state) do
for x={:account, _net, _chan, _nick, _account_id} <- accounts do
handle_info(x, state)
end
{:noreply, state}
end
def handle_info({:account, _net, _chan, nick, account_id}, state) do
nick = String.downcase(nick)
spec = [{{:_, :"$1", :_, :_, :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
Logger.debug("account:: merging #{nick} -> #{account_id}")
rename_object_owner(table, obj, account_id)
end)
{:noreply, state}
end
def handle_info(_, dets) do
{:noreply, dets}
end
defp rename_object_owner(table, object = {key, _, now, perfect, time}, new_id) do
:dets.delete_object(table, key)
:dets.insert(table, {key, new_id, now, perfect, time})
end
defp timezone(channel) do
- env = Application.get_env(:nola, Nola.IRC.PreumsPlugin, [])
+ env = Application.get_env(:nola, Nola.Plugins.Preums, [])
channels = Keyword.get(env, :channels, %{})
channel_settings = Map.get(channels, channel, [])
default = Keyword.get(env, :default_tz, "Europe/Paris")
Keyword.get(channel_settings, :tz, default) || default
end
defp handle_preums(%IRC.Message{channel: nil}, state) do
state
end
defp handle_preums(m = %IRC.Message{text: text, sender: sender}, state) do
channel = {m.network, m.channel}
tz = timezone(channel)
{:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
date = {now.year, now.month, now.day}
key = {channel, date}
chan_cache = Map.get(state, channel, %{})
unless i = Map.get(chan_cache, date) do
case :dets.lookup(state.dets, key) do
[item = {^key, _nick, _now, _perfect, _text}] ->
# Preums lost, but wasn't cached
Map.put(state, channel, %{date => item})
[] ->
# Preums won!
perfect? = Enum.any?(@perfects, fn(perfect) -> Regex.match?(perfect, text) end)
item = {key, m.account.id, now, perfect?, text}
:dets.insert(state.dets, item)
:dets.sync(state.dets)
Map.put(state, channel, %{date => item})
{:error, _} = error ->
Logger.error("#{__MODULE__} dets lookup failed: #{inspect error}")
state
end
else
state
end
end
def score(_chan, _account, _time, _perfect, _text) do
1
end
end
diff --git a/lib/nola_plugins/quatre_cent_vingt_plugin.ex b/lib/nola_plugins/quatre_cent_vingt.ex
similarity index 99%
rename from lib/nola_plugins/quatre_cent_vingt_plugin.ex
rename to lib/nola_plugins/quatre_cent_vingt.ex
index 8953ea3..3331625 100644
--- a/lib/nola_plugins/quatre_cent_vingt_plugin.ex
+++ b/lib/nola_plugins/quatre_cent_vingt.ex
@@ -1,149 +1,149 @@
-defmodule Nola.IRC.QuatreCentVingtPlugin do
+defmodule Nola.Plugins.QuatreCentVingt do
require Logger
@moduledoc """
# 420
* **!420**: recorde un nouveau 420.
* **!420*x**: recorde un nouveau 420*x (*2 = 840, ...) (à vous de faire la multiplication).
* **!420 pseudo**: stats du pseudo.
"""
@achievements %{
1 => ["[le premier… il faut bien commencer un jour]"],
10 => ["T'en es seulement à 10 ? ╭∩╮(Ο_Ο)╭∩╮"],
42 => ["Bravo, et est-ce que autant de pétards t'on aidés à trouver la Réponse ? ٩(- ̮̮̃-̃)۶ [42]"],
100 => ["°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ 100 °º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸"],
115 => [" ۜ\(סּںסּَ` )/ۜ 115!!"]
}
@emojis [
"\\o/",
"~o~",
"~~o∞~~",
"*\\o/*",
"**\\o/**",
"*ô*",
]
@coeffs Range.new(1, 100)
def irc_doc, do: @moduledoc
def start_link, do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
def init(_) do
for coeff <- @coeffs do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:#{420*coeff}", [plugin: __MODULE__])
end
{:ok, _} = Registry.register(IRC.PubSub, "account", [plugin: __MODULE__])
dets_filename = (Nola.data_path() <> "/420.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag},{:repair,:force}])
{:ok, dets}
:ignore
end
for coeff <- @coeffs do
qvc = to_string(420 * coeff)
def handle_info({:irc, :trigger, unquote(qvc), m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :bang}}}, dets) do
{count, last} = get_statistics_for_nick(dets, m.account.id)
count = count + unquote(coeff)
text = achievement_text(count)
now = DateTime.to_unix(DateTime.utc_now())-1 # this is ugly
for i <- Range.new(1, unquote(coeff)) do
:ok = :dets.insert(dets, {m.account.id, now+i})
end
last_s = if last do
last_s = format_relative_timestamp(last)
" (le dernier était #{last_s})"
else
""
end
m.replyfun.("#{m.sender.nick} 420 +#{unquote(coeff)} #{text}#{last_s}")
{:noreply, dets}
end
end
def handle_info({:irc, :trigger, "420", m = %IRC.Message{trigger: %IRC.Trigger{args: [nick], type: :bang}}}, dets) do
account = IRC.Account.find_by_nick(m.network, nick)
if account do
text = case get_statistics_for_nick(dets, m.account.id) do
{0, _} -> "#{nick} n'a jamais !420 ... honte à lui."
{count, last} ->
last_s = format_relative_timestamp(last)
"#{nick} 420: total #{count}, le dernier #{last_s}"
end
m.replyfun.(text)
else
m.replyfun.("je connais pas de #{nick}")
end
{:noreply, dets}
end
# Account
def handle_info({:account_change, old_id, new_id}, dets) do
spec = [{{:"$1", :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, dets, spec, fn(table, obj) ->
rename_object_owner(table, obj, new_id)
end)
{:noreply, dets}
end
# Account: move from nick to account id
def handle_info({:accounts, accounts}, dets) do
for x={:account, _net, _chan, _nick, _account_id} <- accounts do
handle_info(x, dets)
end
{:noreply, dets}
end
def handle_info({:account, _net, _chan, nick, account_id}, dets) do
nick = String.downcase(nick)
spec = [{{:"$1", :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, dets, spec, fn(table, obj) ->
Logger.debug("account:: merging #{nick} -> #{account_id}")
rename_object_owner(table, obj, account_id)
end)
{:noreply, dets}
end
def handle_info(_, dets) do
{:noreply, dets}
end
defp rename_object_owner(table, object = {_, at}, account_id) do
:dets.delete_object(table, object)
:dets.insert(table, {account_id, at})
end
defp format_relative_timestamp(timestamp) do
alias Timex.Format.DateTime.Formatters
alias Timex.Timezone
date = timestamp
|> DateTime.from_unix!
|> Timezone.convert("Europe/Paris")
{:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
{:ok, detail} = Formatters.Default.lformat(date, " ({h24}:{m})", "fr")
relative <> detail
end
defp get_statistics_for_nick(dets, acct) do
qvc = :dets.lookup(dets, acct) |> Enum.sort
count = Enum.reduce(qvc, 0, fn(_, acc) -> acc + 1 end)
{_, last} = List.last(qvc) || {nil, nil}
{count, last}
end
@achievements_keys Map.keys(@achievements)
defp achievement_text(count) when count in @achievements_keys do
Enum.random(Map.get(@achievements, count))
end
defp achievement_text(count) do
emoji = Enum.random(@emojis)
"#{emoji} [#{count}]"
end
end
diff --git a/lib/nola_plugins/radio_france_plugin.ex b/lib/nola_plugins/radio_france.ex
similarity index 99%
rename from lib/nola_plugins/radio_france_plugin.ex
rename to lib/nola_plugins/radio_france.ex
index c2e966f..2096de8 100644
--- a/lib/nola_plugins/radio_france_plugin.ex
+++ b/lib/nola_plugins/radio_france.ex
@@ -1,133 +1,133 @@
-defmodule Nola.IRC.RadioFrancePlugin do
+defmodule Nola.Plugins.RadioFrance do
require Logger
def irc_doc() do
"""
# radio france
Qu'est ce qu'on écoute sur radio france ?
* **!radiofrance `[station]`, !rf `[station]`**
* **!fip, !inter, !info, !bleu, !culture, !musique, !fip `[sous-station]`, !bleu `[région]`**
"""
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@trigger "radiofrance"
@shortcuts ~w(fip inter info bleu culture musique)
def init(_) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:radiofrance", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:rf", regopts)
for s <- @shortcuts do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:#{s}", regopts)
end
{:ok, nil}
end
def handle_info({:irc, :trigger, "rf", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) do
handle_info({:irc, :trigger, "radiofrance", m}, state)
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do
m.replyfun.("radiofrance: précisez la station!")
{:noreply, state}
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do
now(args_to_station(args), m)
{:noreply, state}
end
def handle_info({:irc, :trigger, trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) when trigger in @shortcuts do
now(args_to_station([trigger | args]), m)
{:noreply, state}
end
defp args_to_station(args) do
args
|> Enum.map(&unalias/1)
|> Enum.map(&String.downcase/1)
|> Enum.join("_")
end
def handle_info(info, state) do
Logger.debug("unhandled info: #{inspect info}")
{:noreply, state}
end
defp now(station, m) when is_binary(station) do
case HTTPoison.get(np_url(station), [], []) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
json = Poison.decode!(body)
song? = !!get_in(json, ["now", "song"])
station = reformat_station_name(get_in(json, ["now", "stationName"]))
now_title = get_in(json, ["now", "firstLine", "title"])
now_subtitle = get_in(json, ["now", "secondLine", "title"])
next_title = get_in(json, ["next", "firstLine", "title"])
next_subtitle = get_in(json, ["next", "secondLine", "title"])
next_song? = !!get_in(json, ["next", "song"])
next_at = get_in(json, ["next", "startTime"])
now = format_title(song?, now_title, now_subtitle)
prefix = if song?, do: "🎶", else: "🎤"
m.replyfun.("#{prefix} #{station}: #{now}")
next = format_title(song?, next_title, next_subtitle)
if next do
next_prefix = if next_at do
next_date = DateTime.from_unix!(next_at)
in_seconds = DateTime.diff(next_date, DateTime.utc_now())
in_minutes = ceil(in_seconds / 60)
if in_minutes >= 5 do
if next_song?, do: "#{in_minutes}m 🔜", else: "dans #{in_minutes} minutes:"
else
if next_song?, do: "🔜", else: "suivi de:"
end
else
if next_song?, do: "🔜", else: "à suivre:"
end
m.replyfun.("#{next_prefix} #{next}")
end
{:error, %HTTPoison.Response{status_code: 404}} ->
m.replyfun.("radiofrance: la radio \"#{station}\" n'existe pas")
{:error, %HTTPoison.Response{status_code: code}} ->
m.replyfun.("radiofrance: erreur http #{code}")
_ ->
m.replyfun.("radiofrance: ça n'a pas marché, rip")
end
end
defp np_url(station), do: "https://www.radiofrance.fr/api/v2.0/stations/#{station}/live"
defp unalias("inter"), do: "franceinter"
defp unalias("info"), do: "franceinfo"
defp unalias("bleu"), do: "francebleu"
defp unalias("culture"), do: "franceculture"
defp unalias("musique"), do: "francemusique"
defp unalias(station), do: station
defp format_title(_, nil, nil) do
nil
end
defp format_title(true, title, artist) do
[artist, title] |> Enum.filter(& &1) |> Enum.join(" - ")
end
defp format_title(false, show, section) do
[show, section] |> Enum.filter(& &1) |> Enum.join(": ")
end
defp reformat_station_name(station) do
station
|> String.replace("france", "france ")
|> String.replace("_", " ")
end
end
diff --git a/lib/nola_plugins/say_plugin.ex b/lib/nola_plugins/say.ex
similarity index 98%
rename from lib/nola_plugins/say_plugin.ex
rename to lib/nola_plugins/say.ex
index 915b0f6..91ffacc 100644
--- a/lib/nola_plugins/say_plugin.ex
+++ b/lib/nola_plugins/say.ex
@@ -1,73 +1,73 @@
-defmodule Nola.IRC.SayPlugin do
+defmodule Nola.Plugins.Say do
def irc_doc do
"""
# say
Say something...
* **!say `<channel>` `<text>`** say something on `channel`
* **!asay `<channel>` `<text>`** same but anonymously
You must be a member of the channel.
"""
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [type: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:say", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:asay", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages:private", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, "say", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do
text = Enum.join(text, " ")
say_for(m.account, target, text, true)
{:noreply, state}
end
def handle_info({:irc, :trigger, "asay", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do
text = Enum.join(text, " ")
say_for(m.account, target, text, false)
{:noreply, state}
end
def handle_info({:irc, :text, m = %{text: "say "<>rest}}, state) do
case String.split(rest, " ", parts: 2) do
[target, text] -> say_for(m.account, target, text, true)
_ -> nil
end
{:noreply, state}
end
def handle_info({:irc, :text, m = %{text: "asay "<>rest}}, state) do
case String.split(rest, " ", parts: 2) do
[target, text] -> say_for(m.account, target, text, false)
_ -> nil
end
{:noreply, state}
end
def handle_info(_, state) do
{:noreply, state}
end
defp say_for(account, target, text, with_nick?) do
for {net, chan} <- IRC.Membership.of_account(account) do
chan2 = String.replace(chan, "#", "")
if (target == "#{net}/#{chan}" || target == "#{net}/#{chan2}" || target == chan || target == chan2) do
if with_nick? do
IRC.send_message_as(account, net, chan, text)
else
IRC.Connection.broadcast_message(net, chan, text)
end
end
end
end
end
diff --git a/lib/nola_plugins/script_plugin.ex b/lib/nola_plugins/script.ex
similarity index 96%
rename from lib/nola_plugins/script_plugin.ex
rename to lib/nola_plugins/script.ex
index 94d4edf..d383abb 100644
--- a/lib/nola_plugins/script_plugin.ex
+++ b/lib/nola_plugins/script.ex
@@ -1,42 +1,42 @@
-defmodule Nola.IRC.ScriptPlugin do
+defmodule Nola.Plugins.Script do
require Logger
@moduledoc """
Allows to run outside scripts. Scripts are expected to be long running and receive/send data as JSON over stdin/stdout.
"""
@ircdoc """
# script
Allows to run an outside script.
* **+script `<name>` `[command]`** défini/lance un script
* **-script `<name>`** arrête un script
* **-script del `<name>`** supprime un script
"""
def irc_doc, do: @ircdoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:script", [plugin: __MODULE__])
dets_filename = (Nola.data_path() <> "/" <> "scripts.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [])
{:ok, %{dets: dets}}
end
def handle_info({:irc, :trigger, "script", m = %{trigger: %{type: :plus, args: [name | args]}}}, state) do
end
def handle_info({:irc, :trigger, "script", m = %{trigger: %{type: :minus, args: args}}}, state) do
case args do
["del", name] -> :ok #prout
[name] -> :ok#stop
end
end
end
diff --git a/lib/nola_plugins/seen_plugin.ex b/lib/nola_plugins/seen.ex
similarity index 98%
rename from lib/nola_plugins/seen_plugin.ex
rename to lib/nola_plugins/seen.ex
index 2a4d0dd..65af0c1 100644
--- a/lib/nola_plugins/seen_plugin.ex
+++ b/lib/nola_plugins/seen.ex
@@ -1,59 +1,59 @@
-defmodule Nola.IRC.SeenPlugin do
+defmodule Nola.Plugins.Seen do
@moduledoc """
# seen
* **!seen `<nick>`**
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "triggers", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages", regopts)
dets_filename = (Nola.data_path() <> "/seen.dets") |> String.to_charlist()
{:ok, dets} = :dets.open_file(dets_filename, [])
{:ok, %{dets: dets}}
end
def handle_info({:irc, :trigger, "seen", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [nick]}}}, state) do
witness(m, state)
m.replyfun.(last_seen(m.channel, nick, state))
{:noreply, state}
end
def handle_info({:irc, :trigger, _, m}, state) do
witness(m, state)
{:noreply, state}
end
def handle_info({:irc, :text, m}, state) do
witness(m, state)
{:noreply, state}
end
defp witness(%IRC.Message{channel: channel, text: text, sender: %{nick: nick}}, %{dets: dets}) do
:dets.insert(dets, {{channel, nick}, DateTime.utc_now(), text})
:ok
end
defp last_seen(channel, nick, %{dets: dets}) do
case :dets.lookup(dets, {channel, nick}) do
[{_, date, text}] ->
diff = round(DateTime.diff(DateTime.utc_now(), date)/60)
cond do
diff >= 30 ->
duration = Timex.Duration.from_minutes(diff)
format = Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
"#{nick} a parlé pour la dernière fois il y a #{format}: “#{text}”"
true -> "#{nick} est là..."
end
[] ->
"je ne connais pas de #{nick}"
end
end
end
diff --git a/lib/nola_plugins/sms_plugin.ex b/lib/nola_plugins/sms.ex
similarity index 99%
rename from lib/nola_plugins/sms_plugin.ex
rename to lib/nola_plugins/sms.ex
index d8f7387..ad16f78 100644
--- a/lib/nola_plugins/sms_plugin.ex
+++ b/lib/nola_plugins/sms.ex
@@ -1,165 +1,165 @@
-defmodule Nola.IRC.SmsPlugin do
+defmodule Nola.Plugins.Sms do
@moduledoc """
## sms
* **!sms `<nick>` `<message>`** envoie un SMS.
"""
def short_irc_doc, do: false
def irc_doc, do: @moduledoc
require Logger
def incoming(from, "enable "<>key) do
key = String.trim(key)
account = IRC.Account.find_meta_account("sms-validation-code", String.downcase(key))
if account do
net = IRC.Account.get_meta(account, "sms-validation-target")
IRC.Account.put_meta(account, "sms-number", from)
IRC.Account.delete_meta(account, "sms-validation-code")
IRC.Account.delete_meta(account, "sms-validation-number")
IRC.Account.delete_meta(account, "sms-validation-target")
IRC.Connection.broadcast_message(net, account, "SMS Number #{from} added!")
send_sms(from, "Yay! Number linked to account #{account.name}")
end
end
def incoming(from, message) do
account = IRC.Account.find_meta_account("sms-number", from)
if account do
reply_fun = fn(text) ->
send_sms(from, text)
end
trigger_text = if Enum.any?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(message, trigger) end) do
message
else
"!"<>message
end
message = %IRC.Message{
id: FlakeId.get(),
transport: :sms,
network: "sms",
channel: nil,
text: message,
account: account,
sender: %ExIRC.SenderInfo{nick: account.name},
replyfun: reply_fun,
trigger: IRC.Connection.extract_trigger(trigger_text)
}
Logger.debug("converted sms to message: #{inspect message}")
IRC.Connection.publish(message, ["messages:sms"])
message
end
end
def my_number() do
Keyword.get(Application.get_env(:nola, :sms, []), :number, "+33000000000")
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def path() do
account = Keyword.get(Application.get_env(:nola, :sms), :account)
"https://eu.api.ovh.com/1.0/sms/#{account}"
end
def path(rest) do
Path.join(path(), rest)
end
def send_sms(number, text) do
url = path("/virtualNumbers/#{my_number()}/jobs")
body = %{
"message" => text,
"receivers" => [number],
#"senderForResponse" => true,
#"noStopClause" => true,
"charset" => "UTF-8",
"coding" => "8bit"
} |> Poison.encode!()
headers = [{"content-type", "application/json"}] ++ sign("POST", url, body)
options = []
case HTTPoison.post(url, body, headers, options) do
{:ok, %HTTPoison.Response{status_code: 200}} -> :ok
{:ok, %HTTPoison.Response{status_code: code} = resp} ->
Logger.error("SMS Error: #{inspect resp}")
{:error, code}
{:error, error} -> {:error, error}
end
end
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:sms", [plugin: __MODULE__])
:ok = register_ovh_callback()
{:ok, %{}}
:ignore
end
def handle_info({:irc, :trigger, "sms", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [nick | text]}}}, state) do
with \
{:tree, false} <- {:tree, m.sender.nick == "Tree"},
{_, %IRC.Account{} = account} <- {:account, IRC.Account.find_always_by_nick(m.network, m.channel, nick)},
{_, number} when not is_nil(number) <- {:number, IRC.Account.get_meta(account, "sms-number")}
do
text = Enum.join(text, " ")
sender = if m.channel do
"#{m.channel} <#{m.sender.nick}> "
else
"<#{m.sender.nick}> "
end
case send_sms(number, sender<>text) do
:ok -> m.replyfun.("sent!")
{:error, error} -> m.replyfun.("not sent, error: #{inspect error}")
end
else
{:tree, _} -> m.replyfun.("Tree: va en enfer")
{:account, _} -> m.replyfun.("#{nick} not known")
{:number, _} -> m.replyfun.("#{nick} have not enabled sms")
end
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
defp register_ovh_callback() do
url = path()
body = %{
"callBack" =>NolaWeb.Router.Helpers.sms_url(NolaWeb.Endpoint, :ovh_callback),
"smsResponse" => %{
"cgiUrl" => NolaWeb.Router.Helpers.sms_url(NolaWeb.Endpoint, :ovh_callback),
"responseType" => "cgi"
}
} |> Poison.encode!()
headers = [{"content-type", "application/json"}] ++ sign("PUT", url, body)
options = []
case HTTPoison.put(url, body, headers, options) do
{:ok, %HTTPoison.Response{status_code: 200}} ->
:ok
error -> error
end
end
defp sign(method, url, body) do
ts = DateTime.utc_now() |> DateTime.to_unix()
as = env(:app_secret)
ck = env(:consumer_key)
sign = Enum.join([as, ck, String.upcase(method), url, body, ts], "+")
sign_hex = :crypto.hash(:sha, sign) |> Base.encode16(case: :lower)
headers = [{"X-OVH-Application", env(:app_key)}, {"X-OVH-Timestamp", ts},
{"X-OVH-Signature", "$1$"<>sign_hex}, {"X-Ovh-Consumer", ck}]
end
def parse_number(num) do
{:error, :todo}
end
defp env() do
Application.get_env(:nola, :sms)
end
defp env(key) do
Keyword.get(env(), key)
end
end
diff --git a/lib/nola_plugins/tell_plugin.ex b/lib/nola_plugins/tell.ex
similarity index 99%
rename from lib/nola_plugins/tell_plugin.ex
rename to lib/nola_plugins/tell.ex
index ecc98df..8978c82 100644
--- a/lib/nola_plugins/tell_plugin.ex
+++ b/lib/nola_plugins/tell.ex
@@ -1,106 +1,106 @@
-defmodule Nola.IRC.TellPlugin do
+defmodule Nola.Plugins.Tell do
use GenServer
@moduledoc """
# Tell
* **!tell `<nick>` `<message>`**: tell `message` to `nick` when they reconnect.
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def dets do
(Nola.data_path() <> "/tell.dets") |> String.to_charlist()
end
def tell(m, target, message) do
GenServer.cast(__MODULE__, {:tell, m, target, message})
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "account", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:tell", regopts)
{:ok, dets} = :dets.open_file(dets(), [type: :bag])
{:ok, %{dets: dets}}
end
def handle_cast({:tell, m, target, message}, state) do
do_tell(state, m, target, message)
{:noreply, state}
end
def handle_info({:irc, :trigger, "tell", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [target | message]}}}, state) do
do_tell(state, m, target, message)
{:noreply, state}
end
def handle_info({:account, network, channel, nick, account_id}, state) do
messages = :dets.lookup(state.dets, {network, channel, account_id})
if messages != [] do
strs = Enum.map(messages, fn({_, from, message, at}) ->
account = IRC.Account.get(from)
user = IRC.UserTrack.find_by_account(network, account)
fromnick = if user, do: user.nick, else: account.name
"#{nick}: <#{fromnick}> #{message}"
end)
Enum.each(strs, fn(s) -> IRC.Connection.broadcast_message(network, channel, s) end)
:dets.delete(state.dets, {network, channel, account_id})
end
{:noreply, state}
end
def handle_info({:account_change, old_id, new_id}, state) do
#:ets.fun2ms(fn({ {_net, _chan, target_id}, from_id, _, _} = obj) when (target_id == old_id) or (from_id == old_id) -> obj end)
spec = [{{{:"$1", :"$2", :"$3"}, :"$4", :_, :_}, [{:orelse, {:==, :"$3", {:const, old_id}}, {:==, :"$4", {:const, old_id}}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
case obj do
{ {net, chan, ^old_id}, from_id, message, at } = obj ->
:dets.delete(obj)
:dets.insert(table, {{net, chan, new_id}, from_id, message, at})
{key, ^old_id, message, at} = obj ->
:dets.delete(table, obj)
:dets.insert(table, {key, new_id, message, at})
_ -> :ok
end
end)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
def terminate(_, state) do
:dets.close(state.dets)
:ok
end
defp do_tell(state, m, nick_target, message) do
target = IRC.Account.find_always_by_nick(m.network, m.channel, nick_target)
message = Enum.join(message, " ")
with \
{:target, %IRC.Account{} = target} <- {:target, target},
{:same, false} <- {:same, target.id == m.account.id},
target_user = IRC.UserTrack.find_by_account(m.network, target),
target_nick = if(target_user, do: target_user.nick, else: target.name),
present? = if(target_user, do: Map.has_key?(target_user.last_active, m.channel)),
{:absent, true, _} <- {:absent, !present?, target_nick},
{:message, message} <- {:message, message}
do
obj = { {m.network, m.channel, target.id}, m.account.id, message, NaiveDateTime.utc_now()}
:dets.insert(state.dets, obj)
m.replyfun.("will tell to #{target_nick}")
else
{:same, _} -> m.replyfun.("are you so stupid that you need a bot to tell yourself things ?")
{:target, _} -> m.replyfun.("#{nick_target} unknown")
{:absent, _, nick} -> m.replyfun.("#{nick} is here, tell yourself!")
{:message, _} -> m.replyfun.("can't tell without a message")
end
end
end
diff --git a/lib/nola_plugins/txt_plugin.ex b/lib/nola_plugins/txt.ex
similarity index 99%
rename from lib/nola_plugins/txt_plugin.ex
rename to lib/nola_plugins/txt.ex
index cab912a..f6e71a3 100644
--- a/lib/nola_plugins/txt_plugin.ex
+++ b/lib/nola_plugins/txt.ex
@@ -1,556 +1,556 @@
-defmodule Nola.IRC.TxtPlugin do
+defmodule Nola.Plugins.Txt do
alias IRC.UserTrack
require Logger
@moduledoc """
# [txt]({{context_path}}/txt)
* **.txt**: liste des fichiers et statistiques.
Les fichiers avec une `*` sont vérrouillés.
[Voir sur le web]({{context_path}}/txt).
* **!txt**: lis aléatoirement une ligne dans tous les fichiers.
* **!txt `<recherche>`**: recherche une ligne dans tous les fichiers.
* **~txt**: essaie de générer une phrase (markov).
* **~txt `<début>`**: essaie de générer une phrase commencant par `<debut>`.
* **!`FICHIER`**: lis aléatoirement une ligne du fichier `FICHIER`.
* **!`FICHIER` `<chiffre>`**: lis la ligne `<chiffre>` du fichier `FICHIER`.
* **!`FICHIER` `<recherche>`**: recherche une ligne contenant `<recherche>` dans `FICHIER`.
* **+txt `<file`>**: crée le fichier `<file>`.
* **+`FICHIER` `<texte>`**: ajoute une ligne `<texte>` dans le fichier `FICHIER`.
* **-`FICHIER` `<chiffre>`**: supprime la ligne `<chiffre>` du fichier `FICHIER`.
* **-txtrw, +txtrw**. op seulement. active/désactive le mode lecture seule.
* **+txtlock `<fichier>`, -txtlock `<fichier>`**. op seulement. active/désactive le verrouillage d'un fichier.
Insérez `\\\\` pour faire un saut de ligne.
"""
def short_irc_doc, do: "!txt https://sys.115ans.net/irc/txt "
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
defstruct triggers: %{}, rw: true, locks: nil, markov_handler: nil, markov: nil
def random(file) do
GenServer.call(__MODULE__, {:random, file})
end
def reply_random(message, file) do
if line = random(file) do
line
|> format_line(nil, message)
|> message.replyfun.()
line
end
end
def init([]) do
dets_locks_filename = (Nola.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist
{:ok, locks} = :dets.open_file(dets_locks_filename, [])
- markov_handler = Keyword.get(Application.get_env(:nola, __MODULE__, []), :markov_handler, Nola.IRC.TxtPlugin.Markov.Native)
+ markov_handler = Keyword.get(Application.get_env(:nola, __MODULE__, []), :markov_handler, Nola.Plugins.Txt.Markov.Native)
{:ok, markov} = markov_handler.start_link()
{:ok, _} = Registry.register(IRC.PubSub, "triggers", [plugin: __MODULE__])
{:ok, %__MODULE__{locks: locks, markov_handler: markov_handler, markov: markov, triggers: load()}}
end
def handle_info({:received, "!reload", _, chan}, state) do
{:noreply, %__MODULE__{state | triggers: load()}}
end
#
# ADMIN: RW/RO
#
def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :plus}}}, state = %{rw: false}) do
if channel && UserTrack.operator?(msg.network, channel, msg.sender.nick) do
msg.replyfun.("txt: écriture réactivée")
{:noreply, %__MODULE__{state | rw: true}}
else
{:noreply, state}
end
end
def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :minus}}}, state = %{rw: true}) do
if channel && UserTrack.operator?(msg.network, channel, msg.sender.nick) do
msg.replyfun.("txt: écriture désactivée")
{:noreply, %__MODULE__{state | rw: false}}
else
{:noreply, state}
end
end
#
# ADMIN: LOCKS
#
def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- UserTrack.operator?(msg.network, msg.channel, msg.sender.nick)
do
:dets.insert(state.locks, {trigger})
msg.replyfun.("txt: #{trigger} verrouillé")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :minus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- UserTrack.operator?(msg.network, msg.channel, msg.sender.nick),
true <- :dets.member(state.locks, trigger)
do
:dets.delete(state.locks, trigger)
msg.replyfun.("txt: #{trigger} déverrouillé")
end
{:noreply, state}
end
#
# FILE LIST
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :dot}}}, state) do
map = Enum.map(state.triggers, fn({key, data}) ->
ignore? = String.contains?(key, ".")
locked? = case :dets.lookup(state.locks, key) do
[{trigger}] -> "*"
_ -> ""
end
unless ignore?, do: "#{key}: #{to_string(Enum.count(data))}#{locked?}"
end)
|> Enum.filter(& &1)
total = Enum.reduce(state.triggers, 0, fn({_, data}, acc) ->
acc + Enum.count(data)
end)
detail = Enum.join(map, ", ")
total = ". total: #{Enum.count(state.triggers)} fichiers, #{to_string(total)} lignes. Détail: https://sys.115ans.net/irc/txt"
ro = if !state.rw, do: " (lecture seule activée)", else: ""
(detail<>total<>ro)
|> msg.replyfun.()
{:noreply, state}
end
#
# GLOBAL: RANDOM
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: []}}}, state) do
result = Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
Enum.reduce(data, acc, fn({l, _}, acc) ->
[{trigger, l} | acc]
end)
end)
|> Enum.shuffle()
if !Enum.empty?(result) do
{source, line} = Enum.random(result)
msg.replyfun.(format_line(line, "#{source}: ", msg))
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: args}}}, state) do
grep = Enum.join(args, " ")
|> String.downcase
|> :unicode.characters_to_nfd_binary()
result = with_stateful_results(msg, {:bang,"txt",msg.network,msg.channel,grep}, fn() ->
Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
if !String.contains?(trigger, ".") do
Enum.reduce(data, acc, fn({l, _}, acc) ->
[{trigger, l} | acc]
end)
else
acc
end
end)
|> Enum.filter(fn({_, line}) ->
line
|> String.downcase()
|> :unicode.characters_to_nfd_binary()
|> String.contains?(grep)
end)
|> Enum.shuffle()
end)
if result do
{source, line} = result
msg.replyfun.(["#{source}: " | line])
end
{:noreply, state}
end
def with_stateful_results(msg, key, initfun) do
me = self()
scope = {msg.network, msg.channel || msg.sender.nick}
key = {__MODULE__, me, scope, key}
with_stateful_results(key, initfun)
end
def with_stateful_results(key, initfun) do
pid = case :global.whereis_name(key) do
:undefined ->
start_stateful_results(key, initfun.())
pid -> pid
end
if pid, do: wait_stateful_results(key, initfun, pid)
end
def start_stateful_results(key, []) do
nil
end
def start_stateful_results(key, list) do
me = self()
{pid, _} = spawn_monitor(fn() ->
Process.monitor(me)
stateful_results(me, list)
end)
:yes = :global.register_name(key, pid)
pid
end
def wait_stateful_results(key, initfun, pid) do
send(pid, :get)
receive do
{:stateful_results, line} ->
line
{:DOWN, _ref, :process, ^pid, reason} ->
with_stateful_results(key, initfun)
after
5000 ->
nil
end
end
defp stateful_results(owner, []) do
send(owner, :empty)
:ok
end
@stateful_results_expire :timer.minutes(30)
defp stateful_results(owner, [line | rest] = acc) do
receive do
:get ->
send(owner, {:stateful_results, line})
stateful_results(owner, rest)
{:DOWN, _ref, :process, ^owner, _} ->
:ok
after
@stateful_results_expire -> :ok
end
end
#
# GLOBAL: MARKOV
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :tilde, args: []}}}, state) do
case state.markov_handler.sentence(state.markov) do
{:ok, line} ->
msg.replyfun.(line)
error ->
Logger.error "Txt Markov error: "<>inspect error
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :tilde, args: complete}}}, state) do
complete = Enum.join(complete, " ")
case state.markov_handler.complete_sentence(complete, state.markov) do
{:ok, line} ->
msg.replyfun.(line)
error ->
Logger.error "Txt Markov error: "<>inspect error
end
{:noreply, state}
end
#
# TXT CREATE
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- can_write?(state, msg, trigger),
:ok <- create_file(trigger)
do
msg.replyfun.("#{trigger}.txt créé. Ajouter: `+#{trigger} …` ; Lire: `!#{trigger}`")
{:noreply, %__MODULE__{state | triggers: load()}}
else
_ -> {:noreply, state}
end
end
#
# TXT: RANDOM
#
def handle_info({:irc, :trigger, trigger, m = %{trigger: %{type: :query, args: opts}}}, state) do
{trigger, _} = clean_trigger(trigger)
if Map.get(state.triggers, trigger) do
url = if m.channel do
NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :txt, m.network, NolaWeb.format_chan(m.channel), trigger)
else
NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :txt, trigger)
end
m.replyfun.("-> #{url}")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :bang, args: opts}}}, state) do
{trigger, _} = clean_trigger(trigger)
line = get_random(msg, state.triggers, trigger, String.trim(Enum.join(opts, " ")))
if line do
msg.replyfun.(format_line(line, nil, msg))
end
{:noreply, state}
end
#
# TXT: ADD
#
def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :plus, args: content}}}, state) do
with \
true <- can_write?(state, msg, trigger),
{:ok, idx} <- add(state.triggers, msg.text)
do
msg.replyfun.("#{msg.sender.nick}: ajouté à #{trigger}. (#{idx})")
{:noreply, %__MODULE__{state | triggers: load()}}
else
{:error, {:jaro, string, idx}} ->
msg.replyfun.("#{msg.sender.nick}: doublon #{trigger}##{idx}: #{string}")
error ->
Logger.debug("txt add failed: #{inspect error}")
{:noreply, state}
end
end
#
# TXT: DELETE
#
def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :minus, args: [id]}}}, state) do
with \
true <- can_write?(state, msg, trigger),
data <- Map.get(state.triggers, trigger),
{id, ""} <- Integer.parse(id),
{text, _id} <- Enum.find(data, fn({_, idx}) -> id-1 == idx end)
do
data = data |> Enum.into(Map.new)
data = Map.delete(data, text)
msg.replyfun.("#{msg.sender.nick}: #{trigger}.txt##{id} supprimée: #{text}")
dump(trigger, data)
{:noreply, %__MODULE__{state | triggers: load()}}
else
_ ->
{:noreply, state}
end
end
def handle_info(:reload_markov, state=%__MODULE__{triggers: triggers, markov: markov}) do
state.markov_handler.reload(state.triggers, state.markov)
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
def handle_call({:random, file}, _from, state) do
random = get_random(nil, state.triggers, file, [])
{:reply, random, state}
end
def terminate(_reason, state) do
if state.locks do
:dets.sync(state.locks)
:dets.close(state.locks)
end
:ok
end
# Load/Reloads text files from disk
defp load() do
triggers = Path.wildcard(directory() <> "/*.txt")
|> Enum.reduce(%{}, fn(path, m) ->
file = Path.basename(path)
key = String.replace(file, ".txt", "")
data = directory() <> file
|> File.read!
|> String.split("\n")
|> Enum.reject(fn(line) ->
cond do
line == "" -> true
!line -> true
true -> false
end
end)
|> Enum.with_index
Map.put(m, key, data)
end)
|> Enum.sort
|> Enum.into(Map.new)
send(self(), :reload_markov)
triggers
end
defp dump(trigger, data) do
data = data
|> Enum.sort_by(fn({_, idx}) -> idx end)
|> Enum.map(fn({text, _}) -> text end)
|> Enum.join("\n")
File.write!(directory() <> "/" <> trigger <> ".txt", data<>"\n", [])
end
defp get_random(msg, triggers, trigger, []) do
if data = Map.get(triggers, trigger) do
{data, _idx} = Enum.random(data)
data
else
nil
end
end
defp get_random(msg, triggers, trigger, opt) do
arg = case Integer.parse(opt) do
{pos, ""} -> {:index, pos}
{_pos, _some_string} -> {:grep, opt}
_error -> {:grep, opt}
end
get_with_param(msg, triggers, trigger, arg)
end
defp get_with_param(msg, triggers, trigger, {:index, pos}) do
data = Map.get(triggers, trigger, %{})
case Enum.find(data, fn({_, index}) -> index+1 == pos end) do
{text, _} -> text
_ -> nil
end
end
defp get_with_param(msg, triggers, trigger, {:grep, query}) do
out = with_stateful_results(msg, {:grep, trigger, query}, fn() ->
data = Map.get(triggers, trigger, %{})
regex = Regex.compile!("#{query}", "i")
Enum.filter(data, fn({txt, _}) -> Regex.match?(regex, txt) end)
|> Enum.map(fn({txt, _}) -> txt end)
|> Enum.shuffle()
end)
if out, do: out
end
defp create_file(name) do
File.touch!(directory() <> "/" <> name <> ".txt")
:ok
end
defp add(triggers, trigger_and_content) do
case String.split(trigger_and_content, " ", parts: 2) do
[trigger, content] ->
{trigger, _} = clean_trigger(trigger)
if Map.has_key?(triggers, trigger) do
jaro = Enum.find(triggers[trigger], fn({string, idx}) -> String.jaro_distance(content, string) > 0.9 end)
if jaro do
{string, idx} = jaro
{:error, {:jaro, string, idx}}
else
File.write!(directory() <> "/" <> trigger <> ".txt", content<>"\n", [:append])
idx = Enum.count(triggers[trigger])+1
{:ok, idx}
end
else
{:error, :notxt}
end
_ -> {:error, :badarg}
end
end
# fixme: this is definitely the ugliest thing i've ever done
defp clean_trigger(trigger) do
[trigger | opts] = trigger
|> String.strip
|> String.split(" ", parts: 2)
trigger = trigger
|> String.downcase
|> :unicode.characters_to_nfd_binary()
|> String.replace(~r/[^a-z0-9._]/, "")
|> String.trim(".")
|> String.trim("_")
{trigger, opts}
end
def format_line(line, prefix, msg) do
prefix = unless(prefix, do: "", else: prefix)
prefix <> line
|> String.split("\\\\")
|> Enum.map(fn(line) ->
String.split(line, "\\\\\\\\")
end)
|> List.flatten()
|> Enum.map(fn(line) ->
String.trim(line)
|> Tmpl.render(msg)
end)
end
def directory() do
Application.get_env(:nola, :data_path) <> "/irc.txt/"
end
defp can_write?(%{rw: rw?, locks: locks}, msg = %{channel: nil, sender: sender}, trigger) do
admin? = IRC.admin?(sender)
locked? = case :dets.lookup(locks, trigger) do
[{trigger}] -> true
_ -> false
end
unlocked? = if rw? == false, do: false, else: !locked?
can? = unlocked? || admin?
if !can? do
reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé"
msg.replyfun.("#{sender.nick}: permission refusée (#{reason})")
end
can?
end
defp can_write?(state = %__MODULE__{rw: rw?, locks: locks}, msg = %{channel: channel, sender: sender}, trigger) do
admin? = IRC.admin?(sender)
operator? = IRC.UserTrack.operator?(msg.network, channel, sender.nick)
locked? = case :dets.lookup(locks, trigger) do
[{trigger}] -> true
_ -> false
end
unlocked? = if rw? == false, do: false, else: !locked?
can? = admin? || operator? || unlocked?
if !can? do
reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé"
msg.replyfun.("#{sender.nick}: permission refusée (#{reason})")
end
can?
end
end
diff --git a/lib/nola_plugins/txt_plugin/markov.ex b/lib/nola_plugins/txt/markov.ex
similarity index 87%
rename from lib/nola_plugins/txt_plugin/markov.ex
rename to lib/nola_plugins/txt/markov.ex
index 2e30dfa..b47666c 100644
--- a/lib/nola_plugins/txt_plugin/markov.ex
+++ b/lib/nola_plugins/txt/markov.ex
@@ -1,9 +1,9 @@
-defmodule Nola.IRC.TxtPlugin.Markov do
+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}
end
diff --git a/lib/nola_plugins/txt_plugin/markov_native.ex b/lib/nola_plugins/txt/markov_native.ex
similarity index 88%
rename from lib/nola_plugins/txt_plugin/markov_native.ex
rename to lib/nola_plugins/txt/markov_native.ex
index 4c403c2..aa6b454 100644
--- a/lib/nola_plugins/txt_plugin/markov_native.ex
+++ b/lib/nola_plugins/txt/markov_native.ex
@@ -1,33 +1,33 @@
-defmodule Nola.IRC.TxtPlugin.MarkovNative do
- @behaviour Nola.IRC.TxtPlugin.Markov
+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/nola_plugins/txt_plugin/markov_py_markovify.ex b/lib/nola_plugins/txt/markov_py_markovify.ex
similarity index 78%
rename from lib/nola_plugins/txt_plugin/markov_py_markovify.ex
rename to lib/nola_plugins/txt/markov_py_markovify.ex
index b610ea8..f79ed47 100644
--- a/lib/nola_plugins/txt_plugin/markov_py_markovify.ex
+++ b/lib/nola_plugins/txt/markov_py_markovify.ex
@@ -1,39 +1,39 @@
-defmodule Nola.IRC.TxtPlugin.MarkovPyMarkovify do
+defmodule Nola.Plugins.Txt.MarkovPyMarkovify do
def start_link() do
{:ok, nil}
end
def reload(_data, _markov) do
:ok
end
def sentence(_) do
{:ok, run()}
end
def complete_sentence(sentence, _) do
{:ok, run([sentence])}
end
defp run(args \\ []) do
{binary, script} = script()
- args = [script, Path.expand(Nola.IRC.TxtPlugin.directory()) | args]
+ args = [script, Path.expand(Nola.Plugins.Txt.directory()) | args]
IO.puts "Args #{inspect args}"
case MuonTrap.cmd(binary, args) do
{response, 0} -> response
{response, code} -> "error #{code}: #{response}"
end
end
defp script() do
default_script = to_string(:code.priv_dir(:nola)) <> "/irc/txt/markovify.py"
- env = Application.get_env(:nola, Nola.IRC.TxtPlugin, [])
+ 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/nola_plugins/untappd_plugin.ex b/lib/nola_plugins/untappd.ex
similarity index 98%
rename from lib/nola_plugins/untappd_plugin.ex
rename to lib/nola_plugins/untappd.ex
index 50b0c4d..cf5c694 100644
--- a/lib/nola_plugins/untappd_plugin.ex
+++ b/lib/nola_plugins/untappd.ex
@@ -1,66 +1,66 @@
-defmodule Nola.IRC.UntappdPlugin do
+defmodule Nola.Plugins.Untappd do
def irc_doc() do
"""
# [Untappd](https://untappd.com)
* `!beer <beer name>` Information about the first beer matching `<beer name>`
* `?beer <beer name>` List the 10 firsts beer matching `<beer name>`
_Note_: The best way to search is always "Brewery Name + Beer Name", such as "Dogfish 60 Minute".
Link your Untappd account to the bot (for automated checkins on [alcoolog](#alcoolog), ...) with the `enable-untappd` command, in private.
"""
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:beer", [plugin: __MODULE__])
{:ok, %{}}
end
def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %{type: :bang, args: args}}}, state) do
case Untappd.search_beer(Enum.join(args, " "), limit: 1) do
{:ok, %{"response" => %{"beers" => %{"count" => count, "items" => [result | _]}}}} ->
%{"beer" => beer, "brewery" => brewery} = result
description = Map.get(beer, "beer_description")
|> String.replace("\n", " ")
|> String.replace("\r", " ")
|> String.trim()
beer_s = "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")} - #{Map.get(beer, "beer_abv")}°"
city = get_in(brewery, ["location", "brewery_city"])
location = [Map.get(brewery, "brewery_type"), city, Map.get(brewery, "country_name")]
|> Enum.filter(fn(x) -> x end)
|> Enum.join(", ")
extra = "#{Map.get(beer, "beer_style")} - IBU: #{Map.get(beer, "beer_ibu")} - #{location}"
m.replyfun.([beer_s, extra, description])
err ->
m.replyfun.("Error")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %{type: :query, args: args}}}, state) do
case Untappd.search_beer(Enum.join(args, " ")) do
{:ok, %{"response" => %{"beers" => %{"count" => count, "items" => results}}}} ->
beers = for %{"beer" => beer, "brewery" => brewery} <- results do
"#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")} - #{Map.get(beer, "beer_abv")}°"
end
|> Enum.intersperse(", ")
|> Enum.join("")
m.replyfun.("#{count}. #{beers}")
err ->
m.replyfun.("Error")
end
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
end
diff --git a/lib/nola_plugins/user_mention_plugin.ex b/lib/nola_plugins/user_mention.ex
similarity index 90%
rename from lib/nola_plugins/user_mention_plugin.ex
rename to lib/nola_plugins/user_mention.ex
index eb230fd..c4dd719 100644
--- a/lib/nola_plugins/user_mention_plugin.ex
+++ b/lib/nola_plugins/user_mention.ex
@@ -1,52 +1,52 @@
-defmodule Nola.IRC.UserMentionPlugin do
+defmodule Nola.Plugins.UserMention do
@moduledoc """
# mention
* **@`<nick>` `<message>`**: notifie si possible le nick immédiatement via Telegram, SMS, ou équivalent à `!tell`.
"""
require Logger
def short_irc_doc, do: false
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "triggers", plugin: __MODULE__)
{:ok, nil}
end
def handle_info({:irc, :trigger, nick, message = %IRC.Message{sender: sender, account: account, network: network, channel: channel, trigger: %IRC.Trigger{type: :at, args: content}}}, state) do
nick = nick
|> String.trim(":")
|> String.trim(",")
target = IRC.Account.find_always_by_nick(network, channel, nick)
if target do
telegram = IRC.Account.get_meta(target, "telegram-id")
sms = IRC.Account.get_meta(target, "sms-number")
text = "#{channel} <#{sender.nick}> #{Enum.join(content, " ")}"
cond do
telegram ->
Nola.Telegram.send_message(telegram, "`#{channel}` <**#{sender.nick}**> #{Enum.join(content, " ")}")
sms ->
- case Nola.IRC.SmsPlugin.send_sms(sms, text) do
+ case Nola.Plugins.Sms.send_sms(sms, text) do
{:error, code} -> message.replyfun("#{sender.nick}: erreur #{code} (sms)")
end
true ->
- Nola.IRC.TellPlugin.tell(message, nick, content)
+ Nola.Plugins.Tell.tell(message, nick, content)
end
else
message.replyfun.("#{nick} m'est inconnu")
end
{:noreply, state}
end
def handle_info(_, state) do
{:noreply, state}
end
end
diff --git a/lib/nola_plugins/wikipedia_plugin.ex b/lib/nola_plugins/wikipedia.ex
similarity index 98%
rename from lib/nola_plugins/wikipedia_plugin.ex
rename to lib/nola_plugins/wikipedia.ex
index 3202e13..dcd1874 100644
--- a/lib/nola_plugins/wikipedia_plugin.ex
+++ b/lib/nola_plugins/wikipedia.ex
@@ -1,90 +1,90 @@
-defmodule Nola.IRC.WikipediaPlugin do
+defmodule Nola.Plugins.Wikipedia do
require Logger
@moduledoc """
# wikipédia
* **!wp `<recherche>`**: retourne le premier résultat de la `<recherche>` Wikipedia
* **!wp**: un article Wikipédia au hasard
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:wp", [plugin: __MODULE__])
{:ok, nil}
end
def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do
irc_random(message)
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do
irc_search(Enum.join(args, " "), message)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
defp irc_search("", message), do: irc_random(message)
defp irc_search(query, message) do
params = %{
"action" => "query",
"list" => "search",
"srsearch" => String.strip(query),
"srlimit" => 1,
}
case query_wikipedia(params) do
{:ok, %{"query" => %{"search" => [item | _]}}} ->
title = item["title"]
url = "https://fr.wikipedia.org/wiki/" <> String.replace(title, " ", "_")
msg = "Wikipédia: #{title} — #{url}"
message.replyfun.(msg)
_ ->
nil
end
end
defp irc_random(message) do
params = %{
"action" => "query",
"generator" => "random",
"grnnamespace" => 0,
"prop" => "info"
}
case query_wikipedia(params) do
{:ok, %{"query" => %{"pages" => map = %{}}}} ->
[{_, item}] = Map.to_list(map)
title = item["title"]
url = "https://fr.wikipedia.org/wiki/" <> String.replace(title, " ", "_")
msg = "Wikipédia: #{title} — #{url}"
message.replyfun.(msg)
_ ->
nil
end
end
defp query_wikipedia(params) do
url = "https://fr.wikipedia.org/w/api.php"
params = params
|> Map.put("format", "json")
|> Map.put("utf8", "")
case HTTPoison.get(url, [], params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} -> Jason.decode(body)
{:ok, %HTTPoison.Response{status_code: 400, body: body}} ->
Logger.error "Wikipedia HTTP 400: #{inspect body}"
{:error, "http 400"}
error ->
Logger.error "Wikipedia http error: #{inspect error}"
{:error, "http client error"}
end
end
end
diff --git a/lib/nola_plugins/wolfram_alpha_plugin.ex b/lib/nola_plugins/wolfram_alpha.ex
similarity index 97%
rename from lib/nola_plugins/wolfram_alpha_plugin.ex
rename to lib/nola_plugins/wolfram_alpha.ex
index 6ee06f0..c2f106e 100644
--- a/lib/nola_plugins/wolfram_alpha_plugin.ex
+++ b/lib/nola_plugins/wolfram_alpha.ex
@@ -1,47 +1,47 @@
-defmodule Nola.IRC.WolframAlphaPlugin do
+defmodule Nola.Plugins.WolframAlpha do
use GenServer
require Logger
@moduledoc """
# wolfram alpha
* **`!wa <requête>`** lance `<requête>` sur WolframAlpha
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:wa", [plugin: __MODULE__])
{:ok, nil}
end
def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: query}}}, state) do
query = Enum.join(query, " ")
params = %{
"appid" => Keyword.get(Application.get_env(:nola, :wolframalpha, []), :app_id, "NO_APP_ID"),
"units" => "metric",
"i" => query
}
url = "https://www.wolframalpha.com/input/?i=" <> URI.encode(query)
case HTTPoison.get("http://api.wolframalpha.com/v1/result", [], [params: params]) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
m.replyfun.(["#{query} -> #{body}", url])
{:ok, %HTTPoison.Response{status_code: code, body: body}} ->
error = case {code, body} do
{501, b} -> "input invalide: #{body}"
{code, error} -> "erreur #{code}: #{body || ""}"
end
m.replyfun.("wa: #{error}")
{:error, %HTTPoison.Error{reason: reason}} ->
m.replyfun.("wa: erreur http: #{to_string(reason)}")
_ ->
m.replyfun.("wa: erreur http")
end
{:noreply, state}
end
end
diff --git a/lib/nola_plugins/youtube_plugin.ex b/lib/nola_plugins/youtube.ex
similarity index 98%
rename from lib/nola_plugins/youtube_plugin.ex
rename to lib/nola_plugins/youtube.ex
index fb9bea2..c62f73c 100644
--- a/lib/nola_plugins/youtube_plugin.ex
+++ b/lib/nola_plugins/youtube.ex
@@ -1,104 +1,104 @@
-defmodule Nola.IRC.YouTubePlugin do
+defmodule Nola.Plugins.YouTube do
require Logger
@moduledoc """
# youtube
* **!yt `<recherche>`**, !youtube `<recherche>`: retourne le premier résultat de la `<recherche>` YouTube
"""
defstruct client: nil
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
for t <- ["trigger:yt", "trigger:youtube"], do: {:ok, _} = Registry.register(IRC.PubSub, t, [plugin: __MODULE__])
{:ok, %__MODULE__{}}
end
def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do
irc_search(Enum.join(args, " "), message)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
defp irc_search(query, message) do
case search(query) do
{:ok, %{"items" => [item | _]}} ->
url = "https://youtube.com/watch?v=" <> item["id"]
snippet = item["snippet"]
duration = item["contentDetails"]["duration"] |> String.replace("PT", "") |> String.downcase
date = snippet["publishedAt"]
|> DateTime.from_iso8601()
|> elem(1)
|> Timex.format("{relative}", :relative)
|> elem(1)
info_line = "— #{duration} — uploaded by #{snippet["channelTitle"]} — #{date}"
<> " — #{item["statistics"]["viewCount"]} views, #{item["statistics"]["likeCount"]} likes,"
<> " #{item["statistics"]["dislikeCount"]} dislikes"
message.replyfun.("#{snippet["title"]} — #{url}")
message.replyfun.(info_line)
{:error, error} ->
message.replyfun.("Erreur YouTube: "<>error)
_ ->
nil
end
end
defp search(query) do
query = query
|> String.strip
key = Application.get_env(:nola, :youtube)[:api_key]
params = %{
"key" => key,
"maxResults" => 1,
"part" => "id",
"safeSearch" => "none",
"type" => "video",
"q" => query,
}
url = "https://www.googleapis.com/youtube/v3/search"
case HTTPoison.get(url, [], params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, json} = Jason.decode(body)
item = List.first(json["items"])
if item do
video_id = item["id"]["videoId"]
params = %{
"part" => "snippet,contentDetails,statistics",
"id" => video_id,
"key" => key
}
headers = []
options = [params: params]
case HTTPoison.get("https://www.googleapis.com/youtube/v3/videos", [], options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
Jason.decode(body)
{:ok, %HTTPoison.Response{status_code: code, body: body}} ->
Logger.error "YouTube HTTP #{code}: #{inspect body}"
{:error, "http #{code}"}
error ->
Logger.error "YouTube http error: #{inspect error}"
:error
end
else
:error
end
{:ok, %HTTPoison.Response{status_code: code, body: body}} ->
Logger.error "YouTube HTTP #{code}: #{inspect body}"
{:error, "http #{code}"}
error ->
Logger.error "YouTube http error: #{inspect error}"
:error
end
end
end
diff --git a/lib/nola_web/controllers/alcoolog_controller.ex b/lib/nola_web/controllers/alcoolog_controller.ex
index 3081762..3c812c5 100644
--- a/lib/nola_web/controllers/alcoolog_controller.ex
+++ b/lib/nola_web/controllers/alcoolog_controller.ex
@@ -1,323 +1,323 @@
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]
def token(conn, %{"token" => token}) do
case Nola.Token.lookup(token) do
{:ok, {:alcoolog, :index, network, channel}} -> index(conn, nil, network, channel)
err ->
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
profile_account = IRC.Account.find_always_by_nick(network, nick, nick)
days = String.to_integer(Map.get(params, "days", "180"))
friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
if friend? do
- stats = Nola.IRC.AlcoologPlugin.get_full_statistics(profile_account.id)
- history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- Nola.IRC.AlcoologPlugin.nick_history(profile_account) 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()
conn
|> assign(:title, "alcoolog #{nick}")
|> render("user.html", network: network, profile: profile_account, days: days, nick: nick, history: history, stats: stats)
else
conn
|> put_status(404)
|> text("Page not found")
end
end
def nick_stats_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
profile_account = IRC.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
if friend? do
- stats = Nola.IRC.AlcoologPlugin.get_full_statistics(profile_account.id)
+ stats = Nola.Plugins.Alcoolog.get_full_statistics(profile_account.id)
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(stats))
else
conn
|> put_status(404)
|> json([])
end
end
def nick_gls_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
profile_account = IRC.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
count = String.to_integer(Map.get(params, "days", "180"))
if friend? do
- data = Nola.IRC.AlcoologPlugin.user_over_time_gl(profile_account, count)
+ 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))
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(filled))
else
conn
|> put_status(404)
|> json([])
end
end
def nick_volumes_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
profile_account = IRC.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
count = String.to_integer(Map.get(params, "days", "180"))
if friend? do
- data = Nola.IRC.AlcoologPlugin.user_over_time(profile_account, count)
+ 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))
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(filled))
else
conn
|> put_status(404)
|> json([])
end
end
def nick_log_json(conn = %{assigns: %{account: account}}, %{"network" => network, "nick" => nick}) do
profile_account = IRC.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
if friend? do
- history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- Nola.IRC.AlcoologPlugin.nick_history(profile_account) 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
last = List.last(history)
- {_, active} = Nola.IRC.AlcoologPlugin.user_stats(profile_account)
+ {_, active} = Nola.Plugins.Alcoolog.user_stats(profile_account)
last = %{last | active: active, at: DateTime.utc_now() |> DateTime.to_iso8601()}
history = history ++ [last]
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(history))
else
conn
|> put_status(404)
|> json([])
end
end
def nick_history_json(conn = %{assigns: %{account: account}}, %{"network" => network, "nick" => nick}) do
profile_account = IRC.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
if friend? do
- history = for {_, date, value} <- Nola.IRC.AlcoologAnnouncerPlugin.log(profile_account) do
+ history = for {_, date, value} <- Nola.Plugs.AlcoologAnnouncer.log(profile_account) do
%{date: DateTime.to_iso8601(date), value: value}
end
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(history))
else
conn
|> put_status(404)
|> json([])
end
end
def index(conn = %{assigns: %{account: account}}, %{"network" => network, "chan" => channel}) do
index(conn, account, network, NolaWeb.reformat_chan(channel))
end
def index(conn = %{assigns: %{account: account}}, _) do
index(conn, account, nil, nil)
end
#def index(conn, params) do
# network = Map.get(params, "network")
# chan = if c = Map.get(params, "chan") do
# NolaWeb.reformat_chan(c)
# end
# irc_conn = if network do
# IRC.Connection.get_network(network, chan)
# end
# bot = if(irc_conn, do: irc_conn.nick)#
#
# conn
# |> put_status(403)
# |> render("auth.html", network: network, channel: chan, irc_conn: conn, bot: bot)
#end
def index(conn, account, network, channel) do
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)
match = [
{{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_},
[
{:>, :"$1", {:const, before15}},
], [:"$_"]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
members = IRC.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.IRC.AlcoologPlugin.ETS, match)
+ 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.IRC.AlcoologPlugin.get_channel_statistics(account, network, channel)
+ 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)
# {date, single_peak}
#
conn
|> assign(:title, "alcoolog")
|> 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
count = 30
channel = NolaWeb.reformat_chan(channel)
members = IRC.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)
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.IRC.AlcoologPlugin.user_over_time_gl(account, count), gls, fn({date, gl}, 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)
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}))
end
def minisync(conn, %{"user_id" => user_id, "key" => key, "value" => value}) do
account = IRC.Account.get(user_id)
if account do
- ds = Nola.IRC.AlcoologPlugin.data_state()
- meta = Nola.IRC.AlcoologPlugin.get_user_meta(ds, account.id)
+ 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.IRC.AlcoologPlugin.put_user_meta(ds, account.id, new_meta)
+ Nola.Plugins.Alcoolog.put_user_meta(ds, account.id, new_meta)
_ ->
conn
|> put_status(:unprocessable_entity)
|> text("invalid value")
end
else
conn
|> put_status(:not_found)
|> text("not found")
end
end
end
diff --git a/lib/nola_web/controllers/gpt_controller.ex b/lib/nola_web/controllers/gpt_controller.ex
index 038b235..810a875 100644
--- a/lib/nola_web/controllers/gpt_controller.ex
+++ b/lib/nola_web/controllers/gpt_controller.ex
@@ -1,33 +1,33 @@
defmodule NolaWeb.GptController do
use NolaWeb, :controller
require Logger
plug NolaWeb.ContextPlug
def result(conn, params = %{"id" => result_id}) do
- case Nola.IRC.GptPlugin.get_result(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.IRC.GptPlugin.get_prompt(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/nola_web/controllers/irc_controller.ex b/lib/nola_web/controllers/irc_controller.ex
index c617e78..d6114e6 100644
--- a/lib/nola_web/controllers/irc_controller.ex
+++ b/lib/nola_web/controllers/irc_controller.ex
@@ -1,101 +1,101 @@
defmodule NolaWeb.IrcController do
use NolaWeb, :controller
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([IRC.Account.AccountPlugin] ++ IRC.Plugin.enabled()) do
+ commands = for mod <- Enum.uniq([Nola.Plugins.Account] ++ Nola.Plugins.enabled()) do
if is_atom(mod) do
- identifier = Module.split(mod) |> List.last |> String.replace("Plugin", "") |> Macro.underscore
+ identifier = Module.split(mod) |> List.last |> Macro.underscore
{identifier, mod.irc_doc()}
end
end
|> Enum.filter(& &1)
|> Enum.filter(fn({_, doc}) -> doc end)
members = cond do
network && channel -> Enum.map(IRC.UserTrack.channel(network, channel), fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple) end)
true ->
IRC.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)
else
conn
|> put_status(404)
|> text("Not found")
end
else
do_txt(conn, name)
end
end
def txt(conn, _), do: do_txt(conn, nil)
defp do_txt(conn, nil) do
- doc = Nola.IRC.TxtPlugin.irc_doc()
+ doc = Nola.IRC.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)
conn
|> assign(:title, "txt")
|> 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
if lines = Map.get(data, txt) do
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")
|> render("txt.html", name: txt, data: lines, doc: nil)
else
conn
|> put_status(404)
|> text("Not found")
end
end
defp data() do
dir = Application.get_env(:nola, :data_path) <> "/irc.txt/"
Path.wildcard(dir <> "/*.txt")
|> 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)
Map.put(m, key, data)
end)
|> Enum.sort
|> Enum.into(Map.new)
end
end
diff --git a/lib/nola_web/controllers/sms_controller.ex b/lib/nola_web/controllers/sms_controller.ex
index 575655c..0fffa23 100644
--- a/lib/nola_web/controllers/sms_controller.ex
+++ b/lib/nola_web/controllers/sms_controller.ex
@@ -1,10 +1,10 @@
defmodule NolaWeb.SmsController do
use NolaWeb, :controller
require Logger
def ovh_callback(conn, %{"senderid" => from, "message" => message}) do
- spawn(fn() -> Nola.IRC.SmsPlugin.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/nola_web/live/chat_live.ex b/lib/nola_web/live/chat_live.ex
index 276b362..678ea36 100644
--- a/lib/nola_web/live/chat_live.ex
+++ b/lib/nola_web/live/chat_live.ex
@@ -1,120 +1,120 @@
defmodule NolaWeb.ChatLive do
use Phoenix.LiveView
use Phoenix.HTML
require Logger
def mount(%{"network" => network, "chan" => chan}, %{"account" => account_id}, socket) do
chan = NolaWeb.reformat_chan(chan)
connection = IRC.Connection.get_network(network, chan)
account = IRC.Account.get(account_id)
membership = IRC.Membership.of_account(IRC.Account.get("DRgpD4fLf8PDJMLp8Dtb"))
if account && connection && Enum.member?(membership, {connection.network, chan}) do
{:ok, _} = Registry.register(IRC.PubSub, "#{connection.network}:events", plugin: __MODULE__)
for t <- ["messages", "triggers", "outputs", "events"] do
{:ok, _} = Registry.register(IRC.PubSub, "#{connection.network}/#{chan}:#{t}", plugin: __MODULE__)
end
IRC.PuppetConnection.start(account, connection)
users = IRC.UserTrack.channel(connection.network, chan)
|> Enum.map(fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple) end)
|> Enum.reduce(Map.new, fn(user = %{id: id}, acc) ->
Map.put(acc, id, user)
end)
- backlog = case Nola.IRC.BufferPlugin.select_buffer(connection.network, chan) do
+ backlog = case Nola.IRC.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
{:ok, redirect(socket, to: "/")}
end
end
def handle_event("send", %{"message" => %{"text" => text}}, socket) do
account = IRC.Account.get(socket.assigns.account_id)
IRC.send_message_as(account, socket.assigns.network, socket.assigns.channel, text, true)
{:noreply, assign(socket, :counter, socket.assigns.counter + 1)}
end
def handle_info({:irc, :event, event = %{type: :join, user_id: id}}, socket) do
if user = IRC.UserTrack.lookup(id) do
socket = socket
|> assign(:users, Map.put(socket.assigns.users, id, user))
|> append_to_backlog(event)
{:noreply, socket}
else
{:noreply, socket}
end
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)
{: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)
{: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)
{:noreply, socket}
end
def handle_info({:irc, :trigger, _, message}, socket) 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)
{:noreply, socket}
end
def handle_info(info, socket) do
Logger.debug("Unhandled info: #{inspect info}")
{:noreply, socket}
end
defp append_to_backlog(socket, line) do
{add, _} = reduce_contextual_event(line, {[], List.last(socket.assigns.backlog)})
assign(socket, :backlog, socket.assigns.backlog ++ add)
end
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
end
end

File Metadata

Mime Type
text/x-diff
Expires
Fri, Mar 14, 4:23 PM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
33602
Default Alt Text
(269 KB)

Event Timeline