summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorhref <href@random.sh>2020-07-07 21:39:10 +0200
committerhref <href@random.sh>2020-07-07 21:39:51 +0200
commitd6ee134a5957e299c3ad59011df320b3c41e6e61 (patch)
tree29567e6635466f8a3415a935b3cc8a777019f5bc /lib
parentbleh (diff)
pouet
Diffstat (limited to 'lib')
-rw-r--r--lib/irc.ex3
-rw-r--r--lib/irc/account.ex439
-rw-r--r--lib/irc/connection.ex462
-rw-r--r--lib/irc/connection_handler.ex36
-rw-r--r--lib/irc/conns.ex3
-rw-r--r--lib/irc/login_handler.ex27
-rw-r--r--lib/irc/membership.ex129
-rw-r--r--lib/irc/plugin_supervisor.ex91
-rw-r--r--lib/irc/pubsub_handler.ex135
-rw-r--r--lib/irc/user_track.ex177
-rw-r--r--lib/irc/user_track_handler.ex93
-rw-r--r--lib/lsg.ex4
-rw-r--r--lib/lsg/application.ex7
-rw-r--r--lib/lsg/auth_token.ex59
-rw-r--r--lib/lsg/telegram.ex254
-rw-r--r--lib/lsg_irc.ex34
-rw-r--r--lib/lsg_irc/alcolog_plugin.ex872
-rw-r--r--lib/lsg_irc/alcoolisme_plugin.ex198
-rw-r--r--lib/lsg_irc/alcoolog_announcer_plugin.ex65
-rw-r--r--lib/lsg_irc/base_plugin.ex79
-rw-r--r--lib/lsg_irc/calc_plugin.ex2
-rw-r--r--lib/lsg_irc/coronavirus_plugin.ex3
-rw-r--r--lib/lsg_irc/correction_plugin.ex21
-rw-r--r--lib/lsg_irc/last_fm_plugin.ex20
-rw-r--r--lib/lsg_irc/link_plugin.ex130
-rw-r--r--lib/lsg_irc/link_plugin/github.ex44
-rw-r--r--lib/lsg_irc/link_plugin/reddit_plugin.ex114
-rw-r--r--lib/lsg_irc/link_plugin/twitter.ex13
-rw-r--r--lib/lsg_irc/preums_plugin.ex150
-rw-r--r--lib/lsg_irc/quatre_cent_vingt_plugin.ex66
-rw-r--r--lib/lsg_irc/say_plugin.ex71
-rw-r--r--lib/lsg_irc/script_plugin.ex43
-rw-r--r--lib/lsg_irc/sms_plugin.ex163
-rw-r--r--lib/lsg_irc/txt_plugin.ex141
-rw-r--r--lib/lsg_irc/untappd_plugin.ex66
-rw-r--r--lib/lsg_web.ex16
-rw-r--r--lib/lsg_web/channels/user_socket.ex2
-rw-r--r--lib/lsg_web/context_plug.ex81
-rw-r--r--lib/lsg_web/controllers/alcoolog_controller.ex56
-rw-r--r--lib/lsg_web/controllers/irc_auth_sse_controller.ex66
-rw-r--r--lib/lsg_web/controllers/irc_controller.ex27
-rw-r--r--lib/lsg_web/controllers/network_controller.ex11
-rw-r--r--lib/lsg_web/controllers/page_controller.ex50
-rw-r--r--lib/lsg_web/controllers/sms_controller.ex10
-rw-r--r--lib/lsg_web/controllers/untappd_controller.ex18
-rw-r--r--lib/lsg_web/endpoint.ex2
-rw-r--r--lib/lsg_web/router.ex35
-rw-r--r--lib/lsg_web/templates/alcoolog/auth.html.eex39
-rw-r--r--lib/lsg_web/templates/alcoolog/index.html.eex61
-rw-r--r--lib/lsg_web/templates/irc/index.html.eex30
-rw-r--r--lib/lsg_web/templates/irc/txt.html.eex7
-rw-r--r--lib/lsg_web/templates/irc/txts.html.eex14
-rw-r--r--lib/lsg_web/templates/layout/app.html.eex16
-rw-r--r--lib/lsg_web/templates/network/index.html.eex1
-rw-r--r--lib/lsg_web/templates/page/user.html.eex38
-rw-r--r--lib/lsg_web/views/layout_view.ex66
-rw-r--r--lib/lsg_web/views/network_view.ex4
-rw-r--r--lib/tmpl.ex123
-rw-r--r--lib/untappd.ex94
-rw-r--r--lib/util.ex34
60 files changed, 4063 insertions, 1052 deletions
diff --git a/lib/irc.ex b/lib/irc.ex
index f989366..1e1fd50 100644
--- a/lib/irc.ex
+++ b/lib/irc.ex
@@ -2,6 +2,9 @@ defmodule IRC do
defmodule Message do
defstruct [:text,
+ {:transport, :irc},
+ :network,
+ :account,
:sender,
:channel,
:trigger,
diff --git a/lib/irc/account.ex b/lib/irc/account.ex
new file mode 100644
index 0000000..6f9eb05
--- /dev/null
+++ b/lib/irc/account.ex
@@ -0,0 +1,439 @@
+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?
+
+ defstruct [:id, :name, :token]
+ @type t :: %__MODULE__{id: String.t(), name: 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(LSG.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
+
+ defp 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
+ * **enable-sms** Link a SMS number
+ * **enable-telegram** Link a Telegram account
+ """
+
+ def irc_doc, do: @moduledoc
+ def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
+ def init(_) do
+ {:ok, _} = Registry.register(IRC.PubSub, "message: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 = LSG.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 = LSG.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 #{LSG.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 = LSG.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/irc/connection.ex b/lib/irc/connection.ex
new file mode 100644
index 0000000..d115d88
--- /dev/null
+++ b/lib/irc/connection.ex
@@ -0,0 +1,462 @@
+defmodule IRC.Connection do
+ require Logger
+ use Ecto.Schema
+
+ @moduledoc """
+ # IRC Connection
+
+ Provides a nicer abstraction over ExIRC's handlers.
+
+ ## Start connections
+
+ ```
+ IRC.Connection.start_link(host: "irc.random.sh", port: 6697, nick: "pouetbot", channels: ["#dev"])
+
+ ## PubSub topics
+
+ * `account` -- accounts change
+ * {:account_change, old_account_id, new_account_id} # Sent when account merged
+ * {:accounts, [{:account, network, channel, nick, account_id}] # Sent on bot join
+ * {:account, network, nick, account_id} # Sent on user join
+ * `message` -- aill messages (without triggers)
+ * `message:private` -- all messages without a channel
+ * `message:#CHANNEL` -- all messages within `#CHANNEL`
+ * `triggers` -- all triggers
+ * `trigger:TRIGGER` -- any message with a trigger `TRIGGER`
+
+ ## Replying to %IRC.Message{}
+
+ Each `IRC.Message` comes with a dedicated `replyfun`, to which you only have to pass either:
+
+ """
+ def irc_doc, do: nil
+
+ @min_backoff :timer.seconds(5)
+ @max_backoff :timer.seconds(2*60)
+
+ embedded_schema do
+ field :network, :string
+ field :host, :string
+ field :port, :integer
+ field :nick, :string
+ field :user, :string
+ field :name, :string
+ field :pass, :string
+ field :tls, :boolean, default: false
+ field :channels, {:array, :string}, default: []
+ end
+
+ defmodule Supervisor do
+ use DynamicSupervisor
+
+ def start_link() do
+ DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
+ end
+
+ def start_child(%IRC.Connection{} = conn) do
+ spec = %{id: conn.id, start: {IRC.Connection, :start_link, [conn]}, restart: :transient}
+ DynamicSupervisor.start_child(__MODULE__, spec)
+ end
+
+ @impl true
+ def init(_init_arg) do
+ DynamicSupervisor.init(
+ strategy: :one_for_one,
+ max_restarts: 10,
+ max_seconds: 1
+ )
+ end
+ end
+
+
+ def changeset(params) do
+ import Ecto.Changeset
+
+ %__MODULE__{id: EntropyString.large_id()}
+ |> cast(params, [:network, :host, :port, :nick, :user, :name, :pass, :channels, :tls])
+ |> validate_required([:host, :port, :nick, :user, :name])
+ |> apply_action(:insert)
+ end
+
+ def to_tuple(%__MODULE__{} = conn) do
+ {conn.id, conn.network, conn.host, conn.port, conn.nick, conn.user, conn.name, conn.pass, conn.tls, conn.channels, nil}
+ end
+
+ def from_tuple({id, network, host, port, nick, user, name, pass, tls, channels, _}) do
+ %__MODULE__{id: id, network: network, host: host, port: port, nick: nick, user: user, name: name, pass: pass, tls: tls, channels: channels}
+ end
+
+ ## -- MANAGER API
+
+ def setup() do
+ :dets.open_file(dets(), [])
+ end
+
+ def dets(), do: to_charlist(LSG.data_path("/connections.dets"))
+
+ def lookup(id) do
+ case :dets.lookup(dets(), id) do
+ [object | _] -> from_tuple(object)
+ _ -> nil
+ end
+ end
+
+ def connections() do
+ :dets.foldl(fn(object, acc) -> [from_tuple(object) | acc] end, [], dets())
+ end
+
+ def start_all() do
+ for conn <- connections(), do: {conn, IRC.Connection.Supervisor.start_child(conn)}
+ end
+
+ def get_network(network, channel \\ nil) do
+ spec = [{{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
+ [{:==, :"$1", {:const, network}}], [:"$_"]}]
+ results = Enum.map(:dets.select(dets(), spec), fn(object) -> from_tuple(object) end)
+ |> IO.inspect()
+ if channel do
+ Enum.find(results, fn(conn) -> Enum.member?(conn.channels, channel) end)
+ else
+ List.first(results)
+ end
+ end
+
+ def get_host_nick(host, port, nick) do
+ spec = [{{:_, :_, :"$1", :"$2", :"$3", :_, :_, :_, :_, :_, :_},
+ [{:andalso,
+ {:andalso, {:==, :"$1", {:const, host}}, {:==, :"$2", {:const, port}}},
+ {:==, :"$3", {:const, nick}}}],
+ [:"$_"]}
+ ]
+ case :dets.select(dets(), spec) do
+ [object] -> from_tuple(object)
+ [] -> nil
+ end
+ end
+
+ def delete_connection(%__MODULE__{id: id} = conn) do
+ :dets.delete(dets(), id)
+ stop_connection(conn)
+ :ok
+ end
+
+ def start_connection(%__MODULE__{} = conn) do
+ IRC.Connection.Supervisor.start_child(conn)
+ end
+
+ def stop_connection(%__MODULE__{id: id}) do
+ case :global.whereis_name(id) do
+ pid when is_pid(pid) ->
+ GenServer.stop(pid, :normal)
+ _ -> :error
+ end
+ end
+
+ def add_connection(opts) do
+ case changeset(opts) do
+ {:ok, conn} ->
+ if existing = get_host_nick(conn.host, conn.port, conn.nick) do
+ {:error, {:existing, conn}}
+ else
+ :dets.insert(dets(), to_tuple(conn))
+ IRC.Connection.Supervisor.start_child(conn)
+ end
+ error -> error
+ end
+ end
+
+ def start_link(conn) do
+ GenServer.start_link(__MODULE__, [conn], name: {:global, conn.id})
+ end
+
+ def broadcast_message(net, chan, message) do
+ dispatch("conn", {:broadcast, net, chan, message}, IRC.ConnectionPubSub)
+ end
+ def broadcast_message(list, message) when is_list(list) do
+ for {net, chan} <- list do
+ broadcast_message(net, chan, message)
+ end
+ end
+
+ def privmsg(channel, line) do
+ GenServer.cast(__MODULE__, {:privmsg, channel, line})
+ end
+
+ def init([conn]) do
+ backoff = :backoff.init(@min_backoff, @max_backoff)
+ |> :backoff.type(:jitter)
+ {:ok, %{client: nil, backoff: backoff, conn: conn}, {:continue, :connect}}
+ end
+
+ @triggers %{
+ "!" => :bang,
+ "+" => :plus,
+ "-" => :minus,
+ "?" => :query,
+ "." => :dot,
+ "~" => :tilde,
+ "++" => :plus_plus,
+ "--" => :minus_minus,
+ "!!" => :bang_bang,
+ "??" => :query_query,
+ }
+
+ def handle_continue(:connect, state) do
+ client_opts = []
+ |> Keyword.put(:network, state.conn.network)
+ {:ok, _} = Registry.register(IRC.ConnectionPubSub, "conn", [])
+ client = if state.client && Process.alive?(state.client) do
+ Logger.info("Reconnecting client")
+ state.client
+ else
+ Logger.info("Connecting")
+ {:ok, client} = ExIRC.Client.start_link(debug: false)
+ client
+ end
+ ExIRC.Client.add_handler(client, self())
+ if state.conn.tls do
+ ExIRC.Client.connect_ssl!(client, state.conn.host, state.conn.port)
+ else
+ ExIRC.Client.connect!(client, state.conn.host, state.conn.port)
+ end
+ {:noreply, %{state | client: client}}
+ end
+
+ def handle_info(:disconnected, state) do
+ {delay, backoff} = :backoff.fail(state.backoff)
+ Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect delay}ms")
+ Process.send_after(self(), :connect, delay)
+ {:noreply, %{state | backoff: backoff}}
+ end
+
+ def handle_info(:connect, state) do
+ {:noreply, state, {:continue, :connect}}
+ end
+
+ def handle_cast({:privmsg, channel, line}, state) do
+ irc_reply(state.client, {channel, nil}, line)
+ {:noreply, state}
+ end
+
+ # Connection successful
+ def handle_info({:connected, server, port}, state) do
+ Logger.info("#{inspect(self())} Connected to #{server}:#{port} #{inspect state}")
+ ExIRC.Client.logon(state.client, state.conn.pass || "", state.conn.nick, state.conn.user, state.conn.name)
+ {:noreply, state}
+ end
+
+ # Logon successful
+ def handle_info(:logged_in, state) do
+ Logger.info("#{inspect(self())} Logged in")
+ {_, backoff} = :backoff.succeed(state.backoff)
+ Enum.map(state.conn.channels, &ExIRC.Client.join(state.client, &1))
+ {:noreply, %{state | backoff: backoff}}
+ end
+
+ # Been kicked
+ def handle_info({:kicked, _sender, chan, _reason}, state) do
+ ExIRC.Client.join(state.client, chan)
+ {:noreply, state}
+ end
+
+ # Received something in a channel
+ def handle_info({:received, text, sender, chan}, state) do
+ reply_fun = fn(text) -> irc_reply(state.client, {chan, sender}, text) end
+ account = IRC.Account.lookup(sender)
+ message = %IRC.Message{text: text, network: network(state), account: account, sender: sender, channel: chan, replyfun: reply_fun, trigger: extract_trigger(text)}
+ message = case IRC.UserTrack.messaged(message) do
+ :ok -> message
+ {:ok, message} -> message
+ end
+ publish(message, ["message:#{chan}"])
+ {:noreply, state}
+ end
+
+ # Received a private message
+ def handle_info({:received, text, sender}, state) do
+ reply_fun = fn(text) -> irc_reply(state.client, {sender.nick, sender}, text) end
+ account = IRC.Account.lookup(sender)
+ message = %IRC.Message{text: text, network: network(state), account: account, sender: sender, replyfun: reply_fun, trigger: extract_trigger(text)}
+ message = case IRC.UserTrack.messaged(message) do
+ :ok -> message
+ {:ok, message} -> message
+ end
+ publish(message, ["message:private"])
+ {:noreply, state}
+ end
+
+ ## -- Broadcast
+ def handle_info({:broadcast, net, account = %IRC.Account{}, message}, state) do
+ if net == state.conn.network do
+ user = IRC.UserTrack.find_by_account(net, account)
+ if user do
+ irc_reply(state.client, {user.nick, nil}, message)
+ end
+ end
+ {:noreply, state}
+ end
+ def handle_info({:broadcast, net, chan, message}, state) do
+ if net == state.conn.network && Enum.member?(state.conn.channels, chan) do
+ irc_reply(state.client, {chan, nil}, message)
+ end
+ {:noreply, state}
+ end
+
+ ## -- UserTrack
+
+ def handle_info({:joined, channel}, state) do
+ ExIRC.Client.who(state.client, channel)
+ {:noreply, state}
+ end
+
+ def handle_info({:who, channel, whos}, state) do
+ accounts = Enum.map(whos, fn(who = %ExIRC.Who{nick: nick, operator?: operator}) ->
+ priv = if operator, do: [:operator], else: []
+ # Don't touch -- on WHO the bot joined, not the users.
+ IRC.UserTrack.joined(channel, who, priv, false)
+ account = IRC.Account.lookup(who)
+ if account do
+ {:account, who.network, channel, who.nick, account.id}
+ end
+ end)
+ |> Enum.filter(fn(x) -> x end)
+ dispatch("account", {:accounts, accounts})
+ {:noreply, state}
+ end
+
+ def handle_info({:quit, _reason, sender}, state) do
+ IRC.UserTrack.quitted(sender)
+ {:noreply, state}
+ end
+
+ def handle_info({:joined, channel, sender}, state) do
+ IRC.UserTrack.joined(channel, sender, [])
+ account = IRC.Account.lookup(sender)
+ if account do
+ dispatch("account", {:account, sender.network, channel, sender.nick, account.id})
+ end
+ {:noreply, state}
+ end
+
+ def handle_info({:kicked, nick, _by, channel, _reason}, state) do
+ IRC.UserTrack.parted(network(state), channel, nick)
+ {:noreply, state}
+ end
+
+ def handle_info({:parted, channel, %ExIRC.SenderInfo{nick: nick}}, state) do
+ IRC.UserTrack.parted(channel, nick)
+ {:noreply, state}
+ end
+
+ def handle_info({:mode, [channel, mode, nick]}, state) do
+ track_mode(network(state), channel, nick, mode)
+ {:noreply, state}
+ end
+
+ def handle_info({:nick_changed, old_nick, new_nick}, state) do
+ IRC.UserTrack.renamed(network(state), old_nick, new_nick)
+ {:noreply, state}
+ end
+
+ def handle_info(unhandled, client) do
+ IO.puts inspect(unhandled)
+ {:noreply, client}
+ end
+
+ def publish(pub), do: publish(pub, [])
+
+ def publish(m = %IRC.Message{trigger: nil}, keys) do
+ dispatch(["message"] ++ keys, {:irc, :text, m})
+ end
+
+ def publish(m = %IRC.Message{trigger: t = %IRC.Trigger{trigger: trigger}}, keys) do
+ dispatch(["triggers", "trigger:"<>trigger], {:irc, :trigger, trigger, m})
+ end
+
+ def dispatch(keys, content, sub \\ IRC.PubSub)
+
+ def dispatch(key, content, sub) when is_binary(key), do: dispatch([key], content, sub)
+ def dispatch(keys, content, sub) when is_list(keys) do
+ IO.puts "dispatching to #{inspect({sub,keys})} --> #{inspect content}"
+ for key <- keys do
+ spawn(fn() -> Registry.dispatch(sub, key, fn h ->
+ for {pid, _} <- h, do: send(pid, content)
+ end) end)
+ end
+ end
+
+ #
+ # Triggers
+ #
+
+ def triggers, do: @triggers
+
+ for {trigger, name} <- @triggers do
+ def extract_trigger(unquote(trigger)<>text) do
+ text = String.strip(text)
+ [trigger | args] = String.split(text, " ")
+ %IRC.Trigger{type: unquote(name), trigger: String.downcase(trigger), args: args}
+ end
+ end
+
+ def extract_trigger(_), do: nil
+
+ #
+ # IRC Replies
+ #
+
+ # irc_reply(ExIRC.Client pid, {channel or nick, ExIRC.Sender}, binary | replies
+ # replies :: {:kick, reason} | {:kick, nick, reason} | {:mode, mode, nick}
+ defp irc_reply(client, {target, _}, text) when is_binary(text) or is_list(text) do
+ lines = IRC.splitlong(text)
+ |> Enum.map(fn(x) -> if(is_list(x), do: x, else: String.split(x, "\n")) end)
+ |> List.flatten()
+ for line <- lines do
+ ExIRC.Client.msg(client, :privmsg, target, line)
+ end
+ end
+
+ defp irc_reply(client, {target, %{nick: nick}}, {:kick, reason}) do
+ ExIRC.Client.kick(client, target, nick, reason)
+ end
+
+ defp irc_reply(client, {target, _}, {:kick, nick, reason}) do
+ ExIRC.Client.kick(client, target, nick, reason)
+ end
+
+ defp irc_reply(client, {target, %{nick: nick}}, {:mode, mode}) do
+ ExIRC.Client.mode(client, target, mode, nick)
+ end
+
+ defp irc_reply(client, target, {:mode, mode, nick}) do
+ ExIRC.Client.mode(client, target, mode, nick)
+ end
+
+ defp irc_reply(client, target, {:channel_mode, mode}) do
+ ExIRC.Client.mode(client, target, mode)
+ end
+
+ defp track_mode(network, channel, nick, "+o") do
+ IRC.UserTrack.change_privileges(network, channel, nick, {[:operator], []})
+ :ok
+ end
+
+ defp track_mode(network, channel, nick, "-o") do
+ IRC.UserTrack.change_privileges(network, channel, nick, {[], [:operator]})
+ :ok
+ end
+
+ defp server(%{conn: %{host: host, port: port}}) do
+ host <> ":" <> to_string(port)
+ end
+
+ defp network(state = %{conn: %{network: network}}) do
+ if network do
+ network
+ else
+ # FIXME performance meheeeee
+ ExIRC.Client.state(state.client)[:network]
+ end
+ end
+
+end
diff --git a/lib/irc/connection_handler.ex b/lib/irc/connection_handler.ex
deleted file mode 100644
index 1c335f2..0000000
--- a/lib/irc/connection_handler.ex
+++ /dev/null
@@ -1,36 +0,0 @@
-defmodule IRC.ConnectionHandler do
- defmodule State do
- defstruct [:host, :port, :pass, :nick, :name, :user, :client]
- end
-
- def start_link(client) do
- irc = Application.get_env(:lsg, :irc)[:irc]
- host = irc[:host]
- port = irc[:port]
- nick = irc[:nick]
- user = irc[:user]
- name = irc[:name]
- GenServer.start_link(__MODULE__, [%State{client: client, host: host, port: port, nick: nick, user: user, name: name}])
- end
-
- def init([state]) do
- ExIRC.Client.add_handler state.client, self
- ExIRC.Client.connect! state.client, state.host, state.port
- {:ok, state}
- end
-
- def handle_info({:connected, server, port}, state) do
- debug "Connected to #{server}:#{port}"
- ExIRC.Client.logon state.client, state.pass, state.nick, state.user, state.name
- {:noreply, state}
- end
-
- # Catch-all for messages you don't care about
- def handle_info(msg, state) do
- {:noreply, state}
- end
-
- defp debug(msg) do
- IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset()
- end
-end
diff --git a/lib/irc/conns.ex b/lib/irc/conns.ex
new file mode 100644
index 0000000..b0e5d3c
--- /dev/null
+++ b/lib/irc/conns.ex
@@ -0,0 +1,3 @@
+defmodule IRC.Conns do
+
+end
diff --git a/lib/irc/login_handler.ex b/lib/irc/login_handler.ex
deleted file mode 100644
index 4b02dc2..0000000
--- a/lib/irc/login_handler.ex
+++ /dev/null
@@ -1,27 +0,0 @@
-defmodule IRC.LoginHandler do
- def start_link(client) do
- GenServer.start_link(__MODULE__, [client])
- end
-
- def init([client]) do
- ExIRC.Client.add_handler client, self
- conf = Application.get_env(:lsg, :irc, []) |> Keyword.get(:irc)
- channels = Keyword.get(conf, :channels, [])
- {:ok, {client, channels}}
- end
-
- def handle_info(:logged_in, state = {client, channels}) do
- debug "Logged in to server"
- channels |> Enum.map(&ExIRC.Client.join client, &1)
- {:noreply, state}
- end
-
- # Catch-all for messages you don't care about
- def handle_info(_msg, state) do
- {:noreply, state}
- end
-
- defp debug(msg) do
- IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset()
- end
-end
diff --git a/lib/irc/membership.ex b/lib/irc/membership.ex
new file mode 100644
index 0000000..74ed1b4
--- /dev/null
+++ b/lib/irc/membership.ex
@@ -0,0 +1,129 @@
+defmodule IRC.Membership do
+ @moduledoc """
+ Memberships (users in channels)
+ """
+
+ # Key: {account, net, channel}
+ # Format: {key, last_seen}
+
+ defp dets() do
+ to_charlist(LSG.data_path <> "/memberships.dets")
+ end
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [], [name: __MODULE__])
+ end
+
+ def init(_) do
+ dets = :dets.open_file(dets(), [])
+ {:ok, dets}
+ end
+
+ def of_account(%IRC.Account{id: id}) do
+ spec = [{{{:"$1", :"$2", :"$3"}, :_}, [{:==, :"$1", {:const, id}}], [{{:"$2", :"$3"}}]}]
+ :dets.select(dets(), spec)
+ end
+
+ def merge_account(old_id, new_id) do
+ #iex(37)> :ets.fun2ms(fn({{old_id, _, _}, _}=obj) when old_id == "42" -> obj end)
+ spec = [{{{:"$1", :_, :_}, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
+ Util.ets_mutate_select_each(:dets, dets(), spec, fn(table, obj = {{_old, net, chan}, ts}) ->
+ :dets.delete_object(table, obj)
+ :dets.insert(table, {{new_id, net, chan}, ts})
+ end)
+ end
+
+ def touch(%IRC.Account{id: id}, network, channel) do
+ :dets.insert(dets(), {{id, network, channel}, NaiveDateTime.utc_now()})
+ end
+ def touch(account_id, network, channel) do
+ if account = IRC.Account.get(account_id) do
+ touch(account, network, channel)
+ end
+ end
+
+ def notify_channels(account, minutes \\ 30, last_active \\ true) do
+ not_before = NaiveDateTime.add(NaiveDateTime.utc_now(), (minutes*-60), :second)
+ spec = [{{{:"$1", :_, :_}, :_}, [{:==, :"$1", {:const, account.id}}], [:"$_"]}]
+ memberships = :dets.select(dets(), spec)
+ |> Enum.sort_by(fn({_, ts}) -> ts end, {:desc, NaiveDateTime})
+ active_memberships = Enum.filter(memberships, fn({_, ts}) -> NaiveDateTime.compare(ts, not_before) == :gt end)
+ cond do
+ active_memberships == [] && last_active ->
+ case memberships do
+ [{{_, net, chan}, _}|_] -> [{net, chan}]
+ _ -> []
+ end
+ active_memberships == [] ->
+ []
+ true ->
+ Enum.map(active_memberships, fn({{_, net, chan}, _}) -> {net,chan} end)
+ end
+ end
+
+ def members_or_friends(account, _network, nil) do
+ friends(account)
+ end
+
+ def members_or_friends(_, network, channel) do
+ members(network, channel)
+ end
+
+ def expanded_members_or_friends(account, network, channel) do
+ expand(network, members_or_friends(account, network, channel))
+ end
+
+ def expanded_members(network, channel) do
+ expand(network, members(network, channel))
+ end
+
+ def members(network, channel) do
+ #iex(19)> :ets.fun2ms(fn({{id, net, chan}, ts}) when net == network and chan == channel and ts > min_seen -> id end)
+ limit = 0 # NaiveDateTime.add(NaiveDateTime.utc_now, 30*((24*-60)*60), :second)
+ spec = [
+ {{{:"$1", :"$2", :"$3"}, :"$4"},
+ [
+ {:andalso,
+ {:andalso, {:==, :"$2", {:const, network}}, {:==, :"$3", {:const, channel}}},
+ {:>, :"$4", {:const, limit}}}
+ ], [:"$1"]}
+ ]
+ :dets.select(dets(), spec)
+ end
+
+ def friends(account = %IRC.Account{id: id}) do
+ for({net, chan} <- of_account(account), do: members(net, chan))
+ |> List.flatten()
+ |> Enum.uniq()
+ end
+
+ def handle_info(_, dets) do
+ {:noreply, dets}
+ end
+
+ def handle_cast(_, dets) do
+ {:noreply, dets}
+ end
+
+ def handle_call(_, _, dets) do
+ {:noreply, dets}
+ end
+
+ def terminate(_, dets) do
+ :dets.sync(dets)
+ :dets.close(dets)
+ end
+
+ defp expand(network, list) do
+ for id <- list do
+ if account = IRC.Account.get(id) do
+ user = IRC.UserTrack.find_by_account(network, account)
+ nick = if(user, do: user.nick, else: account.name)
+ {account, user, nick}
+ end
+ end
+ |> Enum.filter(fn(x) -> x end)
+ end
+
+
+end
diff --git a/lib/irc/plugin_supervisor.ex b/lib/irc/plugin_supervisor.ex
new file mode 100644
index 0000000..ca092dc
--- /dev/null
+++ b/lib/irc/plugin_supervisor.ex
@@ -0,0 +1,91 @@
+defmodule IRC.Plugin do
+ require Logger
+
+ defmodule Supervisor do
+ use DynamicSupervisor
+
+ def start_link() do
+ DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
+ end
+
+ def start_child(module, opts \\ []) do
+ IO.inspect(module)
+ spec = %{id: {IRC.Plugin,module}, start: {IRC.Plugin, :start_link, [module, opts]}, name: module, restart: :transient}
+ DynamicSupervisor.start_child(__MODULE__, spec)
+ 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(LSG.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)}
+ 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)
+ 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)}")
+ :ignore
+ end
+ end
+
+end
+
diff --git a/lib/irc/pubsub_handler.ex b/lib/irc/pubsub_handler.ex
deleted file mode 100644
index 61ae55c..0000000
--- a/lib/irc/pubsub_handler.ex
+++ /dev/null
@@ -1,135 +0,0 @@
-defmodule IRC.PubSubHandler do
- @moduledoc """
- # IRC PubSub
-
- Provides a nicer abstraction over ExIRC's handlers.
-
- ## PubSub topics
-
- * `message` -- all messages (without triggers)
- * `message:private` -- all messages without a channel
- * `message:#CHANNEL` -- all messages within `#CHANNEL`
- * `triggers` -- all triggers
- * `trigger:TRIGGER` -- any message with a trigger `TRIGGER`
-
- ## Replying to %IRC.Message{}
-
- Each `IRC.Message` comes with a dedicated `replyfun`, to which you only have to pass either:
-
- """
- def irc_doc, do: nil
-
- def start_link(client) do
- GenServer.start_link(__MODULE__, [client], [name: __MODULE__])
- end
-
- def privmsg(channel, line) do
- GenServer.cast(__MODULE__, {:privmsg, channel, line})
- end
-
- def init([client]) do
- ExIRC.Client.add_handler(client, self())
- {:ok, client}
- end
-
- @triggers %{
- "!" => :bang,
- "+" => :plus,
- "-" => :minus,
- "?" => :query,
- "." => :dot,
- "~" => :tilde
- }
-
- def handle_cast({:privmsg, channel, line}, client) do
- irc_reply(client, {channel, nil}, line)
- {:noreply, client}
- end
-
- def handle_info({:received, text, sender, chan}, client) do
- reply_fun = fn(text) -> irc_reply(client, {chan, sender}, text) end
- message = %IRC.Message{text: text, sender: sender, channel: chan, replyfun: reply_fun, trigger: extract_trigger(text)}
- publish(message, ["message:#{chan}"])
- {:noreply, client}
- end
-
- def handle_info({:received, text, sender}, client) do
- reply_fun = fn(text) -> irc_reply(client, {sender.nick, sender}, text) end
- message = %IRC.Message{text: text, sender: sender, replyfun: reply_fun, trigger: extract_trigger(text)}
- publish(message, ["message:private"])
- {:noreply, client}
- end
-
- def handle_info(unhandled, client) do
- IO.puts inspect(unhandled)
- {:noreply, client}
- end
-
- defp publish(pub), do: publish(pub, [])
-
- defp publish(m = %IRC.Message{trigger: nil}, keys) do
- dispatch(["message"] ++ keys, {:irc, :text, m})
- end
-
- defp publish(m = %IRC.Message{trigger: t = %IRC.Trigger{trigger: trigger}}, keys) do
- dispatch(["triggers", "trigger:"<>trigger], {:irc, :trigger, trigger, m})
- end
-
- defp dispatch(key, content) when is_binary(key), do: dispatch([key], content)
- defp dispatch(keys, content) when is_list(keys) do
- IO.puts "dispatching to #{inspect(keys)} --> #{inspect content}"
- for key <- keys do
- spawn(fn() -> Registry.dispatch(IRC.PubSub, key, fn h ->
- for {pid, _} <- h, do: send(pid, content)
- end) end)
- end
- end
-
- #
- # Triggers
- #
-
-
- for {trigger, name} <- @triggers do
- defp extract_trigger(unquote(trigger)<>text) do
- text = String.strip(text)
- [trigger | args] = String.split(text, " ")
- %IRC.Trigger{type: unquote(name), trigger: trigger, args: args}
- end
- end
-
- defp extract_trigger(_), do: nil
-
- #
- # IRC Replies
- #
-
- # irc_reply(ExIRC.Client pid, {channel or nick, ExIRC.Sender}, binary | replies
- # replies :: {:kick, reason} | {:kick, nick, reason} | {:mode, mode, nick}
- defp irc_reply(client, {target, _}, text) when is_binary(text) or is_list(text) do
- for line <- IRC.splitlong(text) do
- ExIRC.Client.msg(client, :privmsg, target, line)
- end
- end
-
- defp irc_reply(client, {target, %{nick: nick}}, {:kick, reason}) do
- ExIRC.Client.kick(client, target, nick, reason)
- end
-
- defp irc_reply(client, {target, _}, {:kick, nick, reason}) do
- ExIRC.Client.kick(client, target, nick, reason)
- end
-
- defp irc_reply(client, {target, %{nick: nick}}, {:mode, mode}) do
- ExIRC.Client.mode(client, target, mode, nick)
- end
-
- defp irc_reply(client, target, {:mode, mode, nick}) do
- ExIRC.Client.mode(client, target, mode, nick)
- end
-
- defp irc_reply(client, target, {:channel_mode, mode}) do
- ExIRC.Client.mode(client, target, mode)
- end
-
-end
diff --git a/lib/irc/user_track.ex b/lib/irc/user_track.ex
index 02d5752..08b5160 100644
--- a/lib/irc/user_track.ex
+++ b/lib/irc/user_track.ex
@@ -4,7 +4,7 @@ defmodule IRC.UserTrack do
"""
@ets IRC.UserTrack.Storage
- # {uuid, nick, nicks, privilege_map}
+ # {uuid, network, nick, nicks, privilege_map}
# Privilege map:
# %{"#channel" => [:operator, :voice]
defmodule Storage do
@@ -49,24 +49,87 @@ defmodule IRC.UserTrack do
defmodule Id, do: use EntropyString
defmodule User do
- defstruct [:id, :nick, :nicks, :username, :host, :realname, :privileges]
+ defstruct [:id, :account, :network, :nick, {:nicks, []}, :username, :host, :realname, {:privileges, %{}}, {:last_active, %{}}]
def to_tuple(u = %__MODULE__{}) do
- {u.id || IRC.UserTrack.Id.large_id, u.nick, u.nicks || [], u.username, u.host, u.realname, u.privileges}
+ {u.id || IRC.UserTrack.Id.large_id, u.network, u.account, String.downcase(u.nick), u.nick, u.nicks || [], u.username, u.host, u.realname, u.privileges, u.last_active}
end
- def from_tuple({id, nick, nicks, username, host, realname, privs}) do
- %__MODULE__{id: id, nick: nick, nicks: nicks, username: username, realname: realname, privileges: privs}
+ #tuple size: 11
+ def from_tuple({id, network, account, _downcased_nick, nick, nicks, username, host, realname, privs, last_active}) do
+ struct = %__MODULE__{id: id, account: account, network: network, nick: nick, nicks: nicks, username: username, host: host, realname: realname, privileges: privs, last_active: last_active}
end
end
- def find_by_nick(nick) do
- case :ets.match(@ets, {:'$1', nick, :_, :_, :_, :_, :_}) do
- [[id]] -> lookup(id)
+ def find_by_account(%IRC.Account{id: id}) do
+ #iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
+ spec = [
+ {{:_, :_, :"$2", :_, :_, :_, :_, :_, :_, :_, :_},
+ [
+ {:==, :"$2", {:const, id}}
+ ], [:"$_"]}
+ ]
+ for obj <- :ets.select(@ets, spec), do: User.from_tuple(obj)
+ end
+
+ def find_by_account(network, nil) do
+ nil
+ end
+
+ def find_by_account(network, %IRC.Account{id: id}) do
+ #iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
+ spec = [
+ {{:_, :"$1", :"$2", :_, :_, :_, :_, :_, :_, :_, :_},
+ [
+ {:andalso, {:==, :"$1", {:const, network}},
+ {:==, :"$2", {:const, id}}}
+ ], [:"$_"]}
+ ]
+ case :ets.select(@ets, spec) do
+ results = [_r | _] ->
+ results
+ |> Enum.sort_by(fn({_, _, _, _, _, _, _, _, _, _, actives}) ->
+ Map.get(actives, nil)
+ end, {:desc, NaiveDateTime})
+ |> List.first
+ |> User.from_tuple()
_ -> nil
end
end
+
+ def merge_account(old_id, new_id) do
+ #iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
+ spec = [
+ {{:_, :_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_},
+ [
+ {:==, :"$1", {:const, old_id}}
+ ], [:"$_"]}
+ ]
+ Enum.each(:ets.select(@ets, spec), fn({id, net, _, downcased_nick, nick, nicks, username, host, realname, privs, active}) ->
+ Storage.op(fn(ets) ->
+ :ets.insert(@ets, {id, net, new_id, downcased_nick, nick, nicks, username, host, realname, privs, active})
+ end)
+ end)
+ end
+
+ def find_by_nick(%ExIRC.Who{network: network, nick: nick}) do
+ find_by_nick(network, nick)
+ end
+
+
+ def find_by_nick(%ExIRC.SenderInfo{network: network, nick: nick}) do
+ find_by_nick(network, nick)
+ end
+
+ def find_by_nick(network, nick) do
+ case :ets.match(@ets, {:"$1", network, :_, String.downcase(nick), :_, :_, :_, :_, :_, :_, :_}) do
+ [[id]] -> lookup(id)
+ _ ->
+ nil
+ end
+ end
+
def to_list, do: :ets.tab2list(@ets)
def lookup(id) do
@@ -76,8 +139,8 @@ defmodule IRC.UserTrack do
end
end
- def operator?(channel, nick) do
- if user = find_by_nick(nick) do
+ def operator?(network, channel, nick) do
+ if user = find_by_nick(network, nick) do
privs = Map.get(user.privileges, channel, [])
Enum.member?(privs, :admin) || Enum.member?(privs, :operator)
else
@@ -85,22 +148,34 @@ defmodule IRC.UserTrack do
end
end
- def channel(channel) do
- Enum.filter(to_list(), fn({_, nick, _, _, _, _, channels}) ->
+ def channel(network, channel) do
+ Enum.filter(to_list(), fn({_, network, _, _, _, _, _, _, _, channels, _}) ->
Map.get(channels, channel)
end)
end
+ # TODO
+ def connected(sender = %{nick: nick}) do
+ end
+
def joined(c, s), do: joined(c,s,[])
- def joined(channel, sender=%{nick: nick, user: uname, host: host}, privileges) do
+ def joined(channel, sender=%{nick: nick, user: uname, host: host}, privileges, touch \\ true) do
privileges = if IRC.admin?(sender) do
privileges ++ [:admin]
else privileges end
- user = if user = find_by_nick(nick) do
+ user = if user = find_by_nick(sender.network, nick) do
%User{user | username: uname, host: host, privileges: Map.put(user.privileges || %{}, channel, privileges)}
else
- %User{nick: nick, username: uname, host: host, privileges: %{channel => privileges}}
+ user = %User{network: sender.network, nick: nick, username: uname, host: host, privileges: %{channel => privileges}}
+
+ account = IRC.Account.lookup(user).id
+ user = %User{user | account: account}
+ end
+ user = touch_struct(user, channel)
+
+ if touch && user.account do
+ IRC.Membership.touch(user.account, sender.network, channel)
end
Storage.op(fn(ets) ->
@@ -108,27 +183,47 @@ defmodule IRC.UserTrack do
end)
end
- def joined(channel, nick, privileges) do
- user = if user = find_by_nick(nick) do
- %User{user | privileges: Map.put(user.privileges, channel, privileges)}
+ #def joined(network, channel, nick, privileges) do
+ # user = if user = find_by_nick(network, nick) do
+ # %User{user | privileges: Map.put(user.privileges, channel, privileges)}
+ # else
+ # %User{nick: nick, privileges: %{channel => privileges}}
+ # end
+ #
+ # Storage.op(fn(ets) ->
+ # :ets.insert(ets, User.to_tuple(user))
+ # end)
+ #end
+
+ def messaged(%IRC.Message{network: network, account: account, channel: chan, sender: %{nick: nick}} = m) do
+ {user, account} = if user = find_by_nick(network, nick) do
+ {touch_struct(user, chan), account || IRC.Account.lookup(user)}
else
- %User{nick: nick, privileges: %{channel => privileges}}
+ user = %User{network: network, nick: nick, privileges: %{}}
+ account = IRC.Account.lookup(user)
+ {%User{user | account: account.id}, account}
+ end
+ Storage.insert(User.to_tuple(user))
+ if chan, do: IRC.Membership.touch(account, network, chan)
+ if !m.account do
+ {:ok, %IRC.Message{m | account: account}}
+ else
+ :ok
end
-
- Storage.op(fn(ets) ->
- :ets.insert(ets, User.to_tuple(user))
- end)
end
- def renamed(old_nick, new_nick) do
- if user = find_by_nick(old_nick) do
+ def renamed(network, old_nick, new_nick) do
+ if user = find_by_nick(network, old_nick) do
+ old_account = IRC.Account.lookup(user)
user = %User{user | nick: new_nick, nicks: [old_nick|user.nicks]}
+ account = IRC.Account.lookup(user, false) || old_account
+ user = %User{user | nick: new_nick, account: account.id, nicks: [old_nick|user.nicks]}
Storage.insert(User.to_tuple(user))
end
end
- def change_privileges(channel, nick, {add, remove}) do
- if user = find_by_nick(nick) do
+ def change_privileges(network, channel, nick, {add, remove}) do
+ if user = find_by_nick(network, nick) do
privs = Map.get(user.privileges, channel)
privs = Enum.reduce(add, privs, fn(priv, acc) -> [priv|acc] end)
@@ -139,9 +234,18 @@ defmodule IRC.UserTrack do
end
end
- def parted(channel, nick) do
- if user = find_by_nick(nick) do
+ def parted(channel, %{network: network, nick: nick}) do
+ parted(network, channel, nick)
+ end
+
+ def parted(network, channel, nick) do
+ if user = find_by_nick(network, nick) do
+ if user.account do
+ IRC.Membership.touch(user.account, network, channel)
+ end
+
privs = Map.delete(user.privileges, channel)
+ lasts = Map.delete(user.last_active, channel)
if Enum.count(privs) > 0 do
user = %User{user | privileges: privs}
Storage.insert(User.to_tuple(user))
@@ -152,9 +256,20 @@ defmodule IRC.UserTrack do
end
def quitted(sender) do
- if user = find_by_nick(sender.nick) do
- Storage.delete(user.id)
+ if user = find_by_nick(sender.network, sender.nick) do
+ if user.account do
+ for({channel, _} <- user.privileges, do: IRC.Membership.touch(user.account, sender.network, channel))
+ end
+ Storage.delete(user.id)
end
end
+ defp touch_struct(user = %User{last_active: last_active}, channel) do
+ now = NaiveDateTime.utc_now()
+ last_active = last_active
+ |> Map.put(channel, now)
+ |> Map.put(nil, now)
+ %User{user | last_active: last_active}
+ end
+
end
diff --git a/lib/irc/user_track_handler.ex b/lib/irc/user_track_handler.ex
deleted file mode 100644
index 0ae802a..0000000
--- a/lib/irc/user_track_handler.ex
+++ /dev/null
@@ -1,93 +0,0 @@
-defmodule IRC.UserTrackHandler do
- @moduledoc """
- # User Track Handler
-
- This handlers keeps track of users presence and privileges.
-
- Planned API:
-
- UserTrackHandler.operator?(%ExIRC.Sender{nick: "href", …}, "#channel") :: boolean
-
- """
-
- def irc_doc, do: nil
-
- def start_link(client) do
- GenServer.start_link(__MODULE__, [client])
- end
-
- defstruct client: nil, ets: nil
-
- def init([client]) do
- ExIRC.Client.add_handler client, self
- {:ok, %__MODULE__{client: client}}
- end
-
- def handle_info({:joined, channel}, state) do
- ExIRC.Client.who(state.client, channel)
- {:noreply, state}
- end
-
- def handle_info({:who, channel, whos}, state) do
- Enum.map(whos, fn(who = %ExIRC.Who{nick: nick, operator?: operator}) ->
- priv = if operator, do: [:operator], else: []
- IRC.UserTrack.joined(channel, who, priv)
- end)
- {:noreply, state}
- end
-
- def handle_info({:quit, _reason, sender}, state) do
- IRC.UserTrack.quitted(sender)
- {:noreply, state}
- end
-
- def handle_info({:joined, channel, sender}, state) do
- IRC.UserTrack.joined(channel, sender, [])
- {:noreply, state}
- end
-
- def handle_info({:kicked, nick, _by, channel, _reason}, state) do
- parted(channel, nick)
- {:noreply, state}
- end
-
- def handle_info({:parted, channel, %ExIRC.SenderInfo{nick: nick}}, state) do
- parted(channel, nick)
- {:noreply, state}
- end
-
- def handle_info({:mode, [channel, mode, nick]}, state) do
- mode(channel, nick, mode)
- {:noreply, state}
- end
-
- def handle_info({:nick_changed, old_nick, new_nick}, state) do
- rename(old_nick, new_nick)
- {:noreply, state}
- end
-
- def handle_info(msg, state) do
- {:noreply, state}
- end
-
- defp parted(channel, nick) do
- IRC.UserTrack.parted(channel, nick)
- :ok
- end
-
- defp mode(channel, nick, "+o") do
- IRC.UserTrack.change_privileges(channel, nick, {[:operator], []})
- :ok
- end
-
- defp mode(channel, nick, "-o") do
- IRC.UserTrack.change_privileges(channel, nick, {[], [:operator]})
- :ok
- end
-
- defp rename(old, new) do
- IRC.UserTrack.renamed(old, new)
- :ok
- end
-
-end
diff --git a/lib/lsg.ex b/lib/lsg.ex
index ab53a70..cf1e50e 100644
--- a/lib/lsg.ex
+++ b/lib/lsg.ex
@@ -1,5 +1,9 @@
defmodule LSG do
+ def data_path(suffix) do
+ Path.join(data_path(), suffix)
+ end
+
def data_path do
Application.get_env(:lsg, :data_path)
end
diff --git a/lib/lsg/application.ex b/lib/lsg/application.ex
index 972767f..a12253b 100644
--- a/lib/lsg/application.ex
+++ b/lib/lsg/application.ex
@@ -15,13 +15,18 @@ defmodule LSG.Application do
worker(Registry, [[keys: :duplicate, name: LSG.BroadcastRegistry]], id: :registry_broadcast),
worker(LSG.IcecastAgent, []),
worker(LSG.Token, []),
+ worker(LSG.AuthToken, []),
+ {GenMagic.Pool, [name: LSG.GenMagic, pool_size: 2]},
+ LSG.Telegram,
#worker(LSG.Icecast, []),
] ++ LSG.IRC.application_childs
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: LSG.Supervisor]
- Supervisor.start_link(children, opts)
+ sup = Supervisor.start_link(children, opts)
+ LSG.IRC.after_start()
+ sup
end
# Tell Phoenix to update the endpoint configuration
diff --git a/lib/lsg/auth_token.ex b/lib/lsg/auth_token.ex
new file mode 100644
index 0000000..0c5ba58
--- /dev/null
+++ b/lib/lsg/auth_token.ex
@@ -0,0 +1,59 @@
+defmodule LSG.AuthToken do
+ use GenServer
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [], [name: __MODULE__])
+ end
+
+ def lookup(id) do
+ GenServer.call(__MODULE__, {:lookup, id})
+ end
+
+ def new_path(account, perks \\ nil) do
+ case new(account, perks) do
+ {:ok, id} ->
+ LSGWeb.Router.Helpers.login_path(LSGWeb.Endpoint, :token, id)
+ error ->
+ error
+ end
+ end
+
+ def new_url(account, perks \\ nil) do
+ case new(account, perks) do
+ {:ok, id} ->
+ LSGWeb.Router.Helpers.login_url(LSGWeb.Endpoint, :token, id)
+ error ->
+ error
+ end
+ end
+
+ def new(account, perks \\ nil) do
+ GenServer.call(__MODULE__, {:new, account, perks})
+ end
+
+ def init(_) do
+ {:ok, Map.new}
+ end
+
+ def handle_call({:lookup, id}, _, state) do
+ IO.inspect(state)
+ with \
+ {account, date, perks} <- Map.get(state, id),
+ d when d > 0 <- DateTime.diff(date, DateTime.utc_now())
+ do
+ {:reply, {:ok, account, perks}, Map.delete(state, id)}
+ else
+ x ->
+ IO.inspect(x)
+ {:reply, {:error, :invalid_token}, state}
+ end
+ end
+
+ def handle_call({:new, account, perks}, _, state) do
+ id = IRC.UserTrack.Id.token()
+ expire = DateTime.utc_now()
+ |> DateTime.add(15*60, :second)
+ {:reply, {:ok, id}, Map.put(state, id, {account, expire, perks})}
+ end
+
+end
diff --git a/lib/lsg/telegram.ex b/lib/lsg/telegram.ex
new file mode 100644
index 0000000..7541b13
--- /dev/null
+++ b/lib/lsg/telegram.ex
@@ -0,0 +1,254 @@
+defmodule LSG.Telegram do
+ require Logger
+
+ use Telegram.Bot,
+ token: Keyword.get(Application.get_env(:lsg, :telegram, []), :key),
+ username: Keyword.get(Application.get_env(:lsg, :telegram), :nick, "beauttebot"),
+ purge: true
+
+ def my_path() do
+ "https://t.me/beauttebot"
+ end
+
+ def init() do
+ # Create users in track: IRC.UserTrack.connected(...)
+ :ok
+ end
+
+ def handle_update(_, %{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/start"<>_}}) do
+ text = "Hello to beautte! Query the bot on IRC and say \"enable-telegram\" to continue."
+ send_message(m["chat"]["id"], text)
+ end
+
+ def handle_update(_, %{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/enable"<>_}}) do
+ key = case String.split(text, " ") do
+ ["/enable", key | _] -> key
+ _ -> "nil"
+ end
+
+ #Handled message "1247435154:AAGnSSCnySn0RuVxy_SUcDEoOX_rbF6vdq0" %{"message" =>
+ # %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
+ # "date" => 1591027272, "entities" =>
+ # [%{"length" => 7, "offset" => 0, "type" => "bot_command"}],
+ # "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
+ # "message_id" => 11, "text" => "/enable salope"}, "update_id" => 764148578}
+ account = IRC.Account.find_meta_account("telegram-validation-code", String.downcase(key))
+ text = if account do
+ net = IRC.Account.get_meta(account, "telegram-validation-target")
+ IRC.Account.put_meta(account, "telegram-id", m["chat"]["id"])
+ IRC.Account.put_meta(account, "telegram-username", m["chat"]["username"])
+ IRC.Account.delete_meta(account, "telegram-validation-code")
+ IRC.Account.delete_meta(account, "telegram-validation-target")
+ IRC.Connection.broadcast_message(net, account, "Telegram #{m["chat"]["username"]} account added!")
+ "Yay! Linked to account #{account.name}"
+ else
+ "Token invalid"
+ end
+ send_message(m["chat"]["id"], text)
+ end
+
+ #[debug] Unhandled update: %{"message" =>
+ # %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
+ # "date" => 1591096015,
+ # "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
+ # "message_id" => 29,
+ # "photo" => [
+ # %{"file_id" => "AgACAgQAAxkBAAMdXtYyz4RQqLcpOlR6xKK3w3NayHAAAnCzMRuL4bFSgl_cTXMl4m5G_T0kXQADAQADAgADbQADZVMBAAEaBA",
+ # "file_size" => 9544, "file_unique_id" => "AQADRv09JF0AA2VTAQAB", "height" => 95, "width" => 320},
+ # %{"file_id" => "AgACAgQAAxkBAAMdXtYyz4RQqLcpOlR6xKK3w3NayHAAAnCzMRuL4bFSgl_cTXMl4m5G_T0kXQADAQADAgADeAADZFMBAAEaBA",
+ # "file_size" => 21420, "file_unique_id" => "AQADRv09JF0AA2RTAQAB", "height" => 148, "width" => 501}]},
+ # "update_id" => 218161546}
+
+ def handle_update(token, data = %{"message" => %{"photo" => _}}) do
+ start_upload(token, "photo", data)
+ end
+
+ def handle_update(token, data = %{"message" => %{"voice" => _}}) do
+ start_upload(token, "voice", data)
+ end
+
+ def handle_update(token, data = %{"message" => %{"video" => _}}) do
+ start_upload(token, "video", data)
+ end
+
+ def handle_update(token, data = %{"message" => %{"document" => _}}) do
+ start_upload(token, "document", data)
+ end
+
+ def handle_update(token, data = %{"message" => %{"animation" => _}}) do
+ start_upload(token, "animation", data)
+ end
+
+ def start_upload(token, _, %{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}}}) do
+ account = IRC.Account.find_meta_account("telegram-id", id)
+ if account do
+ text = if(m["text"], do: m["text"], else: nil)
+ targets = IRC.Membership.of_account(account)
+ |> Enum.map(fn({net, chan}) -> "#{net}/#{chan}" end)
+ |> Enum.map(fn(i) -> %{"text" => i, "callback_data" => i} end)
+ kb = if Enum.count(targets) > 1 do
+ [%{"text" => "everywhere", "callback_data" => "everywhere"}] ++ targets
+ else
+ targets
+ end
+ |> Enum.chunk_every(2)
+ keyboard = %{"inline_keyboard" => kb}
+ Telegram.Api.request(token, "sendMessage", chat_id: id, text: "Where should I send this file?", reply_markup: keyboard, reply_to_message_id: m["message_id"])
+ end
+ end
+
+ #[debug] Unhandled update: %{"callback_query" =>
+ # %{
+ # "chat_instance" => "-7948978714441865930", "data" => "evolu.net/#dmz",
+ # "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
+ # "id" => "8913804780149600",
+ # "message" => %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
+ # "date" => 1591098553, "from" => %{"first_name" => "devbeautte", "id" => 1293058838, "is_bot" => true, "username" => "devbeauttebot"},
+ # "message_id" => 62,
+ # "reply_markup" => %{"inline_keyboard" => [[%{"callback_data" => "random/#", "text" => "random/#"},
+ # %{"callback_data" => "evolu.net/#dmz", "text" => "evolu.net/#dmz"}]]},
+ # "text" => "Where should I send the file?"}
+ # }
+ # , "update_id" => 218161568}
+
+ def handle_update(t, %{"callback_query" => cb = %{"data" => "resend", "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}) do
+ end
+
+ def handle_update(t, %{"callback_query" => cb = %{"data" => target, "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}) do
+ account = IRC.Account.find_meta_account("telegram-id", chat_id)
+ if account do
+ target = case String.split(target, "/") do
+ ["everywhere"] -> IRC.Membership.of_account(account)
+ [net, chan] -> [{net, chan}]
+ end
+ Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Processing...", reply_markup: %{})
+
+ {content, type} = cond do
+ op["photo"] -> {op["photo"], ""}
+ op["voice"] -> {op["voice"], " a voice message"}
+ op["video"] -> {op["video"], ""}
+ op["document"] -> {op["document"], ""}
+ op["animation"] -> {op["animation"], ""}
+ end
+
+ file = if is_list(content) && Enum.count(content) > 1 do
+ Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2)
+ |> List.first()
+ else
+ content
+ end
+ file_id = file["file_id"]
+ file_unique_id = file["file_unique_id"]
+ text = if(op["caption"], do: ": "<> op["caption"] <> "", else: "")
+ resend = %{"inline_keyboard" => [ [%{"text" => "re-share", "callback_data" => "resend"}] ]}
+ spawn(fn() ->
+ with \
+ {:ok, file} <- Telegram.Api.request(t, "getFile", file_id: file_id),
+ path = "https://api.telegram.org/file/bot#{t}/#{file["file_path"]}",
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path),
+ <<smol_body::binary-size(20), _::binary>> = body,
+ {:ok, magic} <- GenMagic.Pool.perform(LSG.GenMagic, {:bytes, smol_body}),
+ bucket = Application.get_env(:lsg, :s3, []) |> Keyword.get(:bucket),
+ ext = Path.extname(file["file_path"]),
+ s3path = "#{account.id}/#{file_unique_id}#{ext}",
+ Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Uploading...", reply_markup: %{}),
+ s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type),
+ {:ok, _} <- ExAws.request(s3req)
+ do
+ path = "https://s3.wasabisys.com/#{bucket}/#{s3path}"
+ sent = for {net, chan} <- target do
+ user = IRC.UserTrack.find_by_account(net, account)
+ nick = if(user, do: user.nick, else: account.name)
+ txt = "#{nick} sent#{type}#{text} #{path}"
+ IRC.Connection.broadcast_message(net, chan, txt)
+ "#{net}/#{chan}"
+ end
+ if caption = op["caption"], do: as_irc_message(chat_id, caption, account)
+ text = "Sent on " <> Enum.join(sent, ", ") <> " !"
+ Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Sent!", reply_markup: %{})
+ else
+ error ->
+ Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Something failed.", reply_markup: %{})
+ Logger.error("Failed upload from Telegram: #{inspect error}")
+ end
+ end)
+ end
+ end
+
+ def handle_update(_, %{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}, "text" => text}}) do
+ account = IRC.Account.find_meta_account("telegram-id", id)
+ if account do
+ as_irc_message(id, text, account)
+ end
+ end
+
+ def send_message(id, text) do
+ token = Keyword.get(Application.get_env(:lsg, :telegram, []), :key)
+ Telegram.Api.request(token, "sendMessage", chat_id: id, text: text)
+ end
+
+ def handle_update(token__, m) do
+ Logger.debug("Unhandled update: #{inspect m}")
+ end
+
+ defp as_irc_message(id, text, account) do
+ reply_fun = fn(text) -> send_message(id, text) end
+ trigger_text = cond do
+ String.starts_with?(text, "/") ->
+ "/"<>text = text
+ "!"<>text
+ Enum.any?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(text, trigger) end) ->
+ text
+ true ->
+ "!"<>text
+ end
+ message = %IRC.Message{
+ transport: :telegram,
+ network: "telegram",
+ channel: nil,
+ text: text,
+ account: account,
+ sender: %ExIRC.SenderInfo{nick: account.name},
+ replyfun: reply_fun,
+ trigger: IRC.Connection.extract_trigger(trigger_text)
+ }
+ IO.puts("converted telegram to message: #{inspect message}")
+ IRC.Connection.publish(message, ["message:private", "message:telegram"])
+ message
+ end
+
+ command "start" do
+ text = "Hello to beautte! Query the bot on IRC and say \"enable-telegram\" to continue."
+ request("sendMessage", chat_id: update["chat"]["id"],
+ text: text)
+ end
+
+ command "start", args do
+ IO.inspect(update)
+ key = "none"
+ account = IRC.Account.find_meta_account("telegram-validation-code", String.downcase(key))
+ text = if account do
+ net = IRC.Account.get_meta(account, "telegram-validation-target")
+ IRC.Account.put_meta(account, "telegram-id", update["chat"]["id"])
+ IRC.Account.delete_meta(account, "telegram-validation-code")
+ IRC.Account.delete_meta(account, "telegram-validation-target")
+ IRC.Connection.broadcast_message(net, account, "Telegram account added!")
+ "Yay! Linked to account #{account.name}\n\nThe bot doesn't work by sending telegram commands, just send what you would usually send on IRC."
+ else
+ "Token invalid"
+ end
+ request("sendMessage", chat_id: update["chat"]["id"], text: text)
+ end
+
+ command unknown do
+ request("sendMessage", chat_id: update["chat"]["id"],
+ text: "Hey! You sent me a command #{unknown} #{inspect update}")
+ end
+
+ message do
+ request("sendMessage", chat_id: update["chat"]["id"],
+ text: "Hey! You sent me a message: #{inspect update}")
+ end
+end
+
+
diff --git a/lib/lsg_irc.ex b/lib/lsg_irc.ex
index cb1e382..c811d18 100644
--- a/lib/lsg_irc.ex
+++ b/lib/lsg_irc.ex
@@ -1,29 +1,29 @@
defmodule LSG.IRC do
def application_childs do
- {:ok, irc_client} = ExIRC.start_link!
+ env = Application.get_env(:lsg, :irc)
import Supervisor.Spec
+
+ IRC.Connection.setup()
+ IRC.Plugin.setup()
+ for plugin <- Application.get_env(:lsg, :irc)[:plugins], do: IRC.Plugin.declare(plugin)
+
[
+ 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.ConnectionHandler, [irc_client]),
- worker(IRC.LoginHandler, [irc_client]),
- worker(IRC.UserTrackHandler, [irc_client]),
- worker(IRC.PubSubHandler, [irc_client], [name: :irc_pub_sub]),
- ]
- ++
- for handler <- Application.get_env(:lsg, :irc)[:handlers] do
- worker(handler, [irc_client], [name: handler])
- end
- ++
- for plugin <- Application.get_env(:lsg, :irc)[:plugins] do
- worker(plugin, [], [name: plugin])
- end
- ++ [
- worker(LSG.IRC.AlcoologAnnouncerPlugin, [])
+ worker(IRC.Account.AccountPlugin, []),
+ supervisor(IRC.Plugin.Supervisor, [], [name: IRC.Plugin.Supervisor]),
+ supervisor(IRC.Connection.Supervisor, [], [name: IRC.Connection.Supervisor]),
]
end
-
+ def after_start() do
+ # Start plugins first to let them get on connection events.
+ IRC.Plugin.start_all()
+ IRC.Connection.start_all()
+ end
end
diff --git a/lib/lsg_irc/alcolog_plugin.ex b/lib/lsg_irc/alcolog_plugin.ex
index f27dcd8..600dc1a 100644
--- a/lib/lsg_irc/alcolog_plugin.ex
+++ b/lib/lsg_irc/alcolog_plugin.ex
@@ -2,91 +2,29 @@ defmodule LSG.IRC.AlcoologPlugin do
require Logger
@moduledoc """
- # alcoolisme
+ # [alcoolog]({{context_path}}/alcoolog)
- * **`!santai <montant> <degrés d'alcool> [annotation]`**: enregistre un nouveau verre de `montant` d'une boisson à `degrés d'alcool`.
+ * **!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.
* **-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 `semaine`**: affiche le top des 7 derniers jours
+ * **!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**: lien pour voir l'état/statistiques et historique de l'alcoolémie du channel.
+ * **!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
-
- Anciennes commandes:
-
- * **!`<trigger>` `[coeff]` `[annotation]`**: enregistre de l'alcoolisme.
-
- Triggers/Coeffs:
-
- * bière: (coeffs: 25, 50/+, 75/++, 100/+++, ++++)
- * pinard: (coeffs: +, ++, +++, ++++)
- * shot/fort/whisky/rhum/..: (coeffs: +, ++, +++, ++++)
- * eau (-1)
+ 1 point = 1 volume d'alcool.
Annotation: champ libre!
"""
- @triggers %{
- "apero" =>
- %{
- triggers: ["apero", "apéro", "apairo", "apaireau"],
- default_coeff: "",
- coeffs: %{
- "" => 1.0,
- "+" => 2.0,
- "++" => 3.0,
- "+++" => 4.0,
- "++++" => 5.0,
- }
- },
- "bière" =>
- %{
- triggers: ["beer", "bière", "biere", "biaire"],
- default_coeff: "25",
- coeffs: %{
- "25" => 1.0,
- "50" => 2.0,
- "75" => 3.0,
- "100" => 4.0,
- "+" => 2.0,
- "++" => 3,
- "+++" => 4,
- "++++" => 5,
- }
- },
- "pinard" => %{
- triggers: ["pinard", "vin", "rouge", "blanc", "rosé", "rose"],
- annotations: ["rouge", "blanc", "rosé", "rose"],
- default_coeff: "",
- coeffs: %{
- "" => 1,
- "+" => 2,
- "++" => 3,
- "+++" => 4,
- "++++" => 5,
- "vase" => 6,
- }
- },
- "fort" => %{
- triggers: ["shot", "royaume", "whisky", "rhum", "armagnac", "dijo"],
- default_coeff: "",
- coeffs: %{
- "" => 1,
- "+" => 2,
- "++" => 3,
- "+++" => 4,
- "++++" => 5
- }
- }
- }
@user_states %{
:sober => %{
false => ["n'a pas encore bu", "est actuellement sobre"],
@@ -149,9 +87,64 @@ defmodule LSG.IRC.AlcoologPlugin do
"ZHU NIN JIANKANG",
"祝您健康",
"SANTÉ",
- "CHEERS"
+ "CHEERS",
+ "Nuş olsun",
+ "Živjeli",
+ "Biba",
+ "Pura vida",
+ "Terviseks",
+ "Mabuhay",
+ "Kippis",
+ "Sláinte",
+ "건배",
+ "į sveikatą",
+ "Эрүүл мэндийн төлөө",
+ "Saúde",
+ "Sanatate",
+ "živeli",
+ "Proscht",
+ "Chai yo",
+ "Choc tee",
+ "Şerefe",
+ "будьмо",
+ "Budmo",
+ "Iechyd da",
+ "Sei gesund",
+ "за наш", # Russe original. --kienma
+ ]
- ]
+ @bad_drinks %{
+ :negative => [
+ "on peut pas boire en négatif...",
+ "tu crois que ça marche comme ça ? :s",
+ "e{{'u' | rrepeat}}h"
+ ],
+ :null => [
+ "tu me prends pas un peu pour un con là ?",
+ "zéro ? agis comme un homme et bois une pinte de whisky",
+ "gros naze",
+ "FATAL ERROR: java.lang.ArithmeticException (cannot divide by zero)",
+ "?!?!",
+ "votre médecin vous conseille de boire une bouteille de rouge cul sec plutôt"
+ ],
+ :huge => [
+ "tu veux pas aller à l'hopital plutôt ?",
+ "tu me prends pas un peu pour un con là ?",
+ "ok; j'ai appelé le SAMU, ils arrivent",
+ "j'accepterais le !santai quand tu m'auras envoyé la preuve vidéo"
+ ],
+ :eau => [
+ "Le barman, c'est moi ! C'est moi qui suis barman ! Vous êtes la police républicaine ou une bande ? Vous savez qui je suis ? Enfoncez la bouteille, camarades !",
+ "L'alcool est notre pire ennemi, fuir est lâche !",
+ "attention... l'alcool permet de rendre l'eau potable",
+ "{{'QUOI ?' | bold}}",
+ "QUO{{'I' | rrepeat}}?",
+ "{{'COMMENT ÇA DE L'EAU ?' | red}}",
+ "resaisis toi et va ouvrir une bonne teille de rouge...",
+ "bwais tu veux pas un \"petit\" rhum plutôt ?"
+ ]
+
+ }
def irc_doc, do: @moduledoc
@@ -170,21 +163,16 @@ defmodule LSG.IRC.AlcoologPlugin do
end
def init(_) do
+ {:ok, _} = Registry.register(IRC.PubSub, "account", [])
{:ok, _} = Registry.register(IRC.PubSub, "trigger:santai", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:santo", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:santeau", [])
{:ok, _} = Registry.register(IRC.PubSub, "trigger:alcoolog", [])
{:ok, _} = Registry.register(IRC.PubSub, "trigger:sobre", [])
{:ok, _} = Registry.register(IRC.PubSub, "trigger:sobrepour", [])
{:ok, _} = Registry.register(IRC.PubSub, "trigger:soif", [])
{:ok, _} = Registry.register(IRC.PubSub, "trigger:alcoolisme", [])
{:ok, _} = Registry.register(IRC.PubSub, "trigger:alcool", [])
- for {_, config} <- @triggers do
- for trigger <- config.triggers do
- {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{trigger}", [])
- for coeff when byte_size(coeff) > 0 <- Map.keys(config.coeffs) do
- {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{trigger}#{coeff}", [])
- end
- end
- end
dets_filename = (LSG.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
ets = :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
@@ -192,42 +180,41 @@ defmodule LSG.IRC.AlcoologPlugin do
{:ok, meta} = :dets.open_file(dets_meta_filename, [{:type,:set}])
state = %{dets: dets, meta: meta, ets: ets}
- # Upgrade dets format
- update_fun = fn(obj, dets) ->
+ traverse_fun = fn(obj, dets) ->
case obj do
- object = {nick, date, volumes, name, comment} ->
- :dets.delete_object(dets, object)
- :dets.insert(dets, {nick, date, volumes || 0.0, 0, name, comment})
- dets
object = {nick, date, volumes, active, name, comment} ->
- volumes = if is_integer(volumes) do
- volumes = volumes+0.0
- :dets.delete_object(dets, object)
- :dets.insert(dets, {nick, date, volumes || 0.0, 0, name, comment})
- volumes
- else
- volumes
- end
:ets.insert(ets, {{nick, date}, volumes, active, name, comment})
dets
_ ->
dets
end
end
- :dets.foldl(update_fun, dets, dets)
+ :dets.foldl(traverse_fun, dets, dets)
:dets.sync(dets)
+
{: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
+ m.replyfun.(Tmpl.render(Enum.random(Enum.shuffle(Map.get(@bad_drinks, :eau))), m))
+ {: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 = cond do
now.hour >= 0 && now.hour < 6 ->
["apéro tardif ? Je dis OUI ! SANTAI !"]
now.hour >= 6 && now.hour < 12 ->
- ["C'est quand même un peu tôt non ? Prochain apéro #{apero}"]
+ if day_of_week >= 6 do
+ ["C'est quand même un peu tôt non ? Prochain apéro #{apero}"]
+ else
+ ["de l'alcool pour le petit dej ? le week-end, pas de problème !"]
+ 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 !"
@@ -240,9 +227,14 @@ defmodule LSG.IRC.AlcoologPlugin do
"préparez les teilles, apéro dans #{apero}!"
]
now.hour >= 14 && now.hour < 18 ->
- ["tiens bon! apéro #{apero}",
- "courage... apéro dans #{apero}",
- "pas encore :'( apéro dans #{apero}"
+ 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}"
]
true ->
[
@@ -293,8 +285,8 @@ defmodule LSG.IRC.AlcoologPlugin do
end
if time do
- meta = get_user_meta(state, m.sender.nick)
- stats = get_full_statistics(state, m.sender.nick)
+ meta = get_user_meta(state, m.account.id)
+ stats = get_full_statistics(state, m.account.id)
duration = round(DateTime.diff(time, now)/60.0)
@@ -318,9 +310,15 @@ defmodule LSG.IRC.AlcoologPlugin do
{:noreply, state}
end
+ def handle_info({:irc, :trigger, "alcoolog", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do
+ {:ok, token} = LSG.Token.new({:alcoolog, :index, m.sender.network, m.channel})
+ url = LSGWeb.Router.Helpers.alcoolog_url(LSGWeb.Endpoint, :index, m.network, LSGWeb.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
- {:ok, token} = LSG.Token.new({:alcoolog, :index, m.channel})
- url = LSGWeb.Router.Helpers.alcoolog_url(LSGWeb.Endpoint, :index, token)
+ url = LSGWeb.Router.Helpers.alcoolog_url(LSGWeb.Endpoint, :index, m.network, LSGWeb.format_chan(m.channel))
m.replyfun.("-> #{url}")
{:noreply, state}
end
@@ -329,7 +327,7 @@ defmodule LSG.IRC.AlcoologPlugin do
{cl, _} = Util.float_paparse(cl)
{deg, _} = Util.float_paparse(deg)
points = Alcool.units(cl, deg)
- meta = get_user_meta(state, m.sender.nick)
+ 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)
@@ -345,41 +343,67 @@ defmodule LSG.IRC.AlcoologPlugin do
{:noreply, state}
end
- def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
- case args do
- [cl, deg | comment] ->
- comment = if comment == [] do
- nil
- else
- Enum.join(comment, " ")
+ def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: [cl, deg | comment], type: :bang}}}, state) do
+ comment = if comment == [] do
+ nil
+ else
+ Enum.join(comment, " ")
+ end
+
+ {cl, cl_extra} = case {Util.float_paparse(cl), cl} do
+ {{cl, extra}, _} -> {cl, extra}
+ {:error, "("<>_} ->
+ try do
+ {:ok, result} = Abacus.eval(cl)
+ {result, nil}
+ rescue
+ _ -> {nil, "cl: invalid calc expression"}
end
+ {:error, _} -> {nil, "cl: invalid value"}
+ end
- {cl, _} = Util.float_paparse(cl)
- {deg, _} = Util.float_paparse(deg)
- points = Alcool.units(cl, deg)
+ {deg, comment, auto_set, beer_id} = case Util.float_paparse(deg) do
+ {deg, _} -> {deg, comment, 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
- if cl > 0 && deg > 0 do
- now = DateTime.to_unix(DateTime.utc_now(), :millisecond)
- user_meta = get_user_meta(state, m.sender.nick)
- name = "#{cl}cl #{deg}°"
- old_stats = get_full_statistics(state, m.sender.nick)
- :ok = :dets.insert(state.dets, {String.downcase(m.sender.nick), now, points, if(old_stats, do: old_stats.active, else: 0), name, comment})
- true = :ets.insert(state.ets, {{String.downcase(m.sender.nick), now}, points, if(old_stats, do: old_stats.active, else: 0),name, comment})
- sante = @santai |> Enum.shuffle() |> Enum.random()
- meta = get_user_meta(state, m.sender.nick)
- k = if meta.sex, do: 0.7, else: 0.6
- weight = meta.weight
- peak = Float.round((10*points||0.0)/(k*weight), 4)
- stats = get_full_statistics(state, m.sender.nick)
- 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")
+
+ cond do
+ cl == nil -> m.replyfun.(cl_extra)
+ deg == nil -> m.replyfun.(comment)
+ cl >= 500 || deg >= 100 -> m.replyfun.(Tmpl.render(Enum.random(Enum.shuffle(Map.get(@bad_drinks, :huge))), m))
+ cl == 0 || deg == 0 -> m.replyfun.(Tmpl.render(Enum.random(Enum.shuffle(Map.get(@bad_drinks, :null))), m))
+ cl < 0 || deg < 0 -> m.replyfun.(Tmpl.render(Enum.random(Enum.shuffle(Map.get(@bad_drinks, :negative))), m))
+ true ->
+ points = Alcool.units(cl, deg)
+ now = DateTime.to_unix(DateTime.utc_now(), :millisecond)
+ user_meta = get_user_meta(state, m.account.id)
+ name = "#{cl}cl #{deg}°"
+ old_stats = get_full_statistics(state, m.account.id)
+ :ok = :dets.insert(state.dets, {m.account.id, now, points, if(old_stats, do: old_stats.active, else: 0), name, comment})
+ true = :ets.insert(state.ets, {{m.account.id, now}, points, if(old_stats, do: old_stats.active, else: 0),name, comment})
+ sante = @santai |> Enum.map(fn(s) -> String.trim(String.upcase(s)) end) |> Enum.shuffle() |> Enum.random()
+ meta = get_user_meta(state, m.account.id)
+ k = if meta.sex, do: 0.7, else: 0.6
+ weight = 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
@@ -388,29 +412,139 @@ defmodule LSG.IRC.AlcoologPlugin do
detail
end
- up = if stats.active_drinks > 1 do
- " " <> Enum.join(for(_ <- 1..stats.active_drinks, do: "▲")) <> ""
- else
- ""
- end
+ up = if stats.active_drinks > 1 do
+ " " <> Enum.join(for(_ <- 1..stats.active_drinks, do: "▲")) <> ""
+ else
+ ""
+ end
- m.replyfun.("#{sante} #{m.sender.nick}#{up} #{format_points(points)} @#{stats.active}g/l [+#{peak} g/l]"
+ 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}) (sobriété #{at} (dans #{stats.sober_in_s})#{sober_add}) !"
- <> " (aujourd'hui #{stats.daily_volumes} points - #{stats.daily_gl} g/l)")
- else
- m.replyfun.("on ne peut pas boire en négatif...")
+ <> " (aujourd'hui #{stats.daily_volumes} points - #{stats.daily_gl} g/l)"
end
- _ ->
- m.replyfun.("!santai <cl> <degrés> [commentaire]")
+
+ 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(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: " #{comment} (#{deg}°)", else: ""
+ 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
+ stats.active30m >= 2.9 && stats.active30m < 3 -> :miss3
+ stats.active30m >= 1.9 && stats.active30m < 2 -> :miss2
+ stats.active30m >= 0.9 && stats.active30m < 1 -> :miss1
+ stats.active30m >= 0.45 && stats.active30m < 0.5 -> :miss05
+ stats.active30m >= 0.20 && stats.active30m < 0.20 -> :miss025
+ stats.active30m >= 3 && stats.active1h < 3.15 -> :small3
+ stats.active30m >= 2 && stats.active1h < 2.15 -> :small2
+ stats.active30m >= 1.5 && stats.active1h < 1.5 -> :small15
+ stats.active30m >= 1 && stats.active1h < 1.15 -> :small1
+ stats.active30m >= 0.5 && stats.active1h <= 0.51 -> :small05
+ stats.active30m >= 0.25 && stats.active30m <= 0.255 -> :small025
+ true -> nil
+ end
+
+ miss = case miss do
+ :miss025 -> [
+ "si peu ?"
+ ]
+ :miss05 -> [
+ "tu pourras encore conduire, faut boire plus!"
+ ]
+ :miss1 -> [
+ "alerte, tu vas rater le gramme !"
+ ]
+ :miss2 -> [
+ "petit joueur ? rater deux grammes de si peu c'est pas sérieux"
+ ]
+ :miss3 -> [
+ "même pas 3 grammes ? allez ! dernière ligne droite!"
+ ]
+ :small025 -> [
+ "tu vas quand même pas en rester là si ?"
+ ]
+ :small05 -> [
+ "c'est un bon début, faut continuer sur cette lancée !"
+ ]
+ :small1 -> [
+ "un peu plus de motivation sur le gramme et c'est parfait !"
+ ]
+ :small15 -> [
+ "essaie de garder ton gramme et demi plus longtemps !"
+ ]
+ :small2 -> [
+ "même pas une demi heure de deux grammes ? allez, fait un effort !",
+ "installe toi mieux aux deux grammes !"
+ ]
+ :small3 -> [
+ "ALLAIIIII CONTINUE FAUT MAINTENIR !",
+ "tu vas quand même pas en rester là ?"
+ ]
+ _ -> nil
+ end
+ if miss do
+ msg = Enum.random(Enum.shuffle(miss))
+ 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}: #{msg}")
+ end
+ end
+
end
{:noreply, state}
end
- def get_channel_statistics(channel) do
- IRC.UserTrack.to_list()
- |> Enum.filter(fn({_, nick, _, _, _, _, channels}) -> Map.get(channels, channel) end)
- |> Enum.map(fn({_, nick, _, _, _, _, _}) -> nick end)
- |> Enum.map(fn(nick) -> {nick, get_full_statistics(nick)} 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
@@ -423,8 +557,9 @@ defmodule LSG.IRC.AlcoologPlugin do
case get_statistics_for_nick(state, nick) do
{count, {_, last_at, last_points, last_active, last_type, last_descr}} ->
{active, active_drinks} = current_alcohol_level(state, nick)
- {rising, m30} = alcohol_level_rising(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
@@ -474,7 +609,7 @@ defmodule LSG.IRC.AlcoologPlugin do
%{active: active, last_at: last_at, last_points: last_points, last_type: last_type, last_descr: last_descr,
trend_symbol: trend,
- active15m: m15, active30m: m30, active1h: h1,
+ active5m: m5, active15m: m15, active30m: m30, active1h: h1,
rising: rising,
active_drinks: active_drinks,
user_status: user_status,
@@ -487,9 +622,8 @@ defmodule LSG.IRC.AlcoologPlugin do
end
def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :dot}}}, state) do
- nicks = Enum.filter(IRC.UserTrack.to_list, fn({_, nick, _, _, _, _, channels}) -> Map.get(channels, m.channel) end) |> Enum.map(fn({_, nick, _, _, _, _, _}) -> nick end)
- nicks = nicks
- |> Enum.map(fn(nick) -> {nick, get_full_statistics(state, nick)} end)
+ 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}) ->
@@ -518,34 +652,39 @@ defmodule LSG.IRC.AlcoologPlugin do
end
def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
- nick = case args do
- [nick] -> nick
- [] -> m.sender.nick
+ account = case args do
+ [nick] -> IRC.Account.find_always_by_nick(m.network, m.channel, nick)
+ [] -> m.account
end
- stats = get_full_statistics(state, nick)
- 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})!")
+ 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, nick)
+ 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.("#{nick} est déjà sobre. aidez le !")
+ 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 = Enum.filter(IRC.UserTrack.to_list, fn({_, nick, _, _, _, _, channels}) -> Map.get(channels, m.channel) end) |> Enum.map(fn({_, nick, _, _, _, _, _}) -> nick end)
- nicks = nicks
- |> Enum.map(fn(nick) -> {nick, get_full_statistics(state, nick)} end)
+ 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}) ->
@@ -554,7 +693,7 @@ defmodule LSG.IRC.AlcoologPlugin do
else
status.trend_symbol
end
- "#{nick} #{status.user_status} #{trend_symbol} #{status.active} g/l"
+ "#{nick} #{status.user_status} #{trend_symbol} #{Float.round(status.active, 4)} g/l"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
@@ -569,21 +708,116 @@ defmodule LSG.IRC.AlcoologPlugin do
{: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
+
+ # TODO Fix with user/channel membership
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: ["semaine"], type: :bang}}}, state) do
aday = 7*((24 * 60)*60)
now = DateTime.utc_now()
before = now
|> DateTime.add(-aday, :second)
- |> DateTime.to_unix(:millisecond)
+ |> DateTime.to_unix(:millisecond)
+ over_time_stats(before, 7, m, state)
+ end
+
+ def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: ["30j"], type: :bang}}}, state) do
+ aday = 31*((24 * 60)*60)
+ now = DateTime.utc_now()
+ before = now
+ |> DateTime.add(-aday, :second)
+ |> DateTime.to_unix(:millisecond)
+ over_time_stats(before, 30, m, state)
+ end
+
+ def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: ["90j"], type: :bang}}}, state) do
+ aday = 91*((24 * 60)*60)
+ now = DateTime.utc_now()
+ before = now
+ |> DateTime.add(-aday, :second)
+ |> DateTime.to_unix(:millisecond)
+ over_time_stats(before, 90, m, state)
+ end
+
+ def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: ["180j"], type: :bang}}}, state) do
+ aday = 180*((24 * 60)*60)
+ now = DateTime.utc_now()
+ before = now
+ |> DateTime.add(-aday, :second)
+ |> DateTime.to_unix(:millisecond)
+ over_time_stats(before, 180, m, state)
+ end
+
+
+ def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: ["365j"], type: :bang}}}, state) do
+ aday = 365*((24 * 60)*60)
+ now = DateTime.utc_now()
+ before = now
+ |> DateTime.add(-aday, :second)
+ |> DateTime.to_unix(:millisecond)
+ over_time_stats(before, 365, m, state)
+ end
+
+ defp 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
+
+ defp over_time_stats(before, j, m, state) do
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
- match = [
- {{{:_, :"$1"}, :_, :_, :_, :_},
- [
- {:>, :"$1", {:const, before}},
- ], [:"$_"]}
+ match = [{{{:_, :"$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) ->
@@ -591,15 +825,20 @@ defmodule LSG.IRC.AlcoologPlugin do
Map.put(acc, nick, all + vol)
end)
|> Enum.sort_by(fn({_nick, count}) -> count end, &>/2)
- |> Enum.map(fn({nick, count}) -> "#{nick}: #{Float.round(count, 4)}" end)
+ |> 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 7 jours: #{top}")
+ 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.sender.nick)
+ 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}
@@ -629,13 +868,15 @@ defmodule LSG.IRC.AlcoologPlugin do
if h == nil || weight == nil do
m.replyfun.("paramètres invalides")
else
- old_meta = get_user_meta(state, m.sender.nick)
+ 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.sender.nick, meta)
- msg = if old_meta.weight < meta.weight do
- "t'as grossi..."
- else
- "ok"
+ put_user_meta(state, m.account.id, meta)
+ fat = ["t'as grossi...", "bientôt la tonne ?", "t'as encore abusé du fromage ?"]
+ thin = ["en route vers l'anorexie ?", "t'as grossi...", "faut manger plus de fromage!"]
+ msg = cond do
+ old_meta.weight < meta.weight -> Enum.random(Enum.shuffle(fat))
+ old_meta.weight == meta.weight -> "ok"
+ true -> Enum.random(Enum.shuffle(thin))
end
m.replyfun.(msg)
end
@@ -644,12 +885,18 @@ defmodule LSG.IRC.AlcoologPlugin do
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.sender.nick) do
+ case get_statistics_for_nick(state, m.account.id) do
{_, obj = {_, date, points, _last_active, type, descr}} ->
:dets.delete_object(state.dets, obj)
- :ets.delete(state.ets, {String.downcase(m.sender.nick), date})
+ :ets.delete(state.ets, {m.account.id, date})
m.replyfun.("supprimé: #{m.sender.nick} #{points} #{type} #{descr}")
m.replyfun.("faudrait quand même penser à boire")
+ 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}
@@ -657,61 +904,134 @@ defmodule LSG.IRC.AlcoologPlugin do
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
- nick = case args do
- [nick] -> nick
- [] -> m.sender.nick
+ {account, duration} = case args do
+ [nick | rest] -> {IRC.Account.find_always_by_nick(m.network, m.channel, nick), rest}
+ [] -> {m.account, []}
end
- if stats = get_full_statistics(state, nick) do
- trend_symbol = if stats.active_drinks > 1 do
- Enum.join(for(_ <- 1..stats.active_drinks, do: stats.trend_symbol))
+ 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
- stats.trend_symbol
+ # 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
- msg = "#{nick} #{stats.user_status} "
- <> (if stats.active > 0 || stats.active15m > 0 || stats.active30m > 0 || stats.active1h > 0, do: ": #{trend_symbol} #{stats.active}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: "")
- <> "— 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")
+ 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: "")
+ <> "— 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
- for {name, config} <- @triggers do
- coeffs = Map.get(config, "coeffs")
- default_coeff_value = Map.get(config.coeffs, config.default_coeff)
-
+ # 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
- #IO.puts "at triggers #{inspect config}"
- # Handle each trigger
- for trigger <- config.triggers do
+ def terminate(_, state) do
+ for dets <- [state.dets, state.meta] do
+ :dets.sync(dets)
+ :dets.close(dets)
+ end
+ end
- # … with a known coeff …
- for {coef, value} when byte_size(coef) > 0 <- config.coeffs do
- def handle_info({:irc, :trigger, unquote(trigger), m = %IRC.Message{trigger: %IRC.Trigger{args: [unquote(coef)<>_ | args], type: :bang}}}, state) do
- handle_bang(unquote(name), unquote(value), m, args, state)
- {:noreply, state}
- end
- def handle_info({:irc, :trigger, unquote(trigger)<>unquote(coef), m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
- handle_bang(unquote(name), unquote(value), m, args, state)
- {:noreply, state}
- end
- end
+ defp rename_object_owner(table, ets, object = {old_id, date, volume, current, name, comment}, old_id, new_id) do
+ :dets.delete_object(table, object)
+ :ets.delete(ets, {old_id, date})
+ :dets.insert(table, {new_id, date, volume, current, name, comment})
+ :ets.insert(ets, {{new_id, date}, volume, current, name, comment})
+ end
- # … or without
- def handle_info({:irc, :trigger, unquote(trigger), m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
- handle_bang(unquote(name), unquote(default_coeff_value), m, args, state)
- {:noreply, state}
+ # 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, _name, _comment}) ->
+ #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
@@ -719,35 +1039,8 @@ defmodule LSG.IRC.AlcoologPlugin do
{:noreply, state}
end
- defp handle_bang(name, points, message, args, state) do
- description = case args do
- [] -> nil
- [something] -> something
- something when is_list(something) -> Enum.join(something, " ")
- _ -> nil
- end
- now = DateTime.to_unix(DateTime.utc_now(), :milliseconds)
- points = points+0.0
- user_meta = get_user_meta(state, message.sender.nick)
- # TODO: Calculer la perte sur last_active depuis last_at.
- # TODO: Ajouter les g/L de la nouvelle boisson.
- :ok = :dets.insert(state.dets, {String.downcase(message.sender.nick), now, points, 0, name, description})
- true = :ets.insert(state.ets, {{String.downcase(message.sender.nick), now}, points, 0, name, description})
- {active,_} = current_alcohol_level(state, message.sender.nick)
- {count, {_, last_at, last_points, _active, last_type, last_descr}} = get_statistics_for_nick(state, message.sender.nick)
- sante = @santai |> Enum.shuffle() |> Enum.random()
- k = if user_meta.sex, do: 0.7, else: 0.6
- weight = user_meta.weight
- peak = (10*points)/(k*weight)
- {_, m30} = alcohol_level_rising(state, message.sender.nick)
- {_, m15} = alcohol_level_rising(state, message.sender.nick, 15)
- {_, h1} = alcohol_level_rising(state, message.sender.nick, 60)
- #message.replyfun.("#{sante} #{message.sender.nick} #{format_points(points)}! #{active}~ g/L (total #{Float.round(count+0.0, 4)} points)")
- message.replyfun.("#{sante} #{message.sender.nick} #{format_points(points)} @#{active}~ g/l [+#{Float.round(peak+0.0, 4)} g/l] (15m: #{m15}, 30m: #{m30}, 1h: #{h1})! (total #{Float.round(count+0.0, 4)} points)")
- end
-
- defp get_statistics_for_nick(state, nick) do
- qvc = :dets.lookup(state.dets, String.downcase(nick))
+ 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, _type, _descr}, acc) -> acc + (points||0) end)
last = List.last(qvc) || nil
@@ -785,13 +1078,13 @@ defmodule LSG.IRC.AlcoologPlugin do
relative <> detail
end
- defp put_user_meta(state, nick, meta) do
- :dets.insert(state.meta, {{:meta, String.downcase(nick)}, meta})
+ 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}, nick) do
- case :dets.lookup(meta, {:meta, String.downcase(nick)}) do
+ 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)
_ ->
@@ -809,8 +1102,8 @@ defmodule LSG.IRC.AlcoologPlugin do
# stop folding when ?
#
- defp user_stats(state = %{ets: ets}, nick) do
- meta = get_user_meta(state, nick)
+ 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
@@ -818,12 +1111,12 @@ defmodule LSG.IRC.AlcoologPlugin do
|> DateTime.to_unix(:millisecond)
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
match = [
- {{{:"$1", :"$2"}, :_, :_, :_, :_},
- [
- {:>, :"$2", {:const, before}},
- {:"=:=", {:const, String.downcase(nick)}, :"$1"}
- ], [:"$_"]}
-]
+ {{{:"$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}
@@ -836,11 +1129,11 @@ defmodule LSG.IRC.AlcoologPlugin do
{Float.round(total_volume + 0.0, 4), Float.round(gl + 0.0, 4)}
end
- defp alcohol_level_rising(state, nick, minutes \\ 30) do
- {now, _} = current_alcohol_level(state, nick)
+ 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, nick, soon_date)
+ {soon, _} = current_alcohol_level(state, account_id, soon_date)
soon = cond do
soon < 0 -> 0.0
true -> soon
@@ -849,8 +1142,8 @@ defmodule LSG.IRC.AlcoologPlugin do
{soon > now, Float.round(soon+0.0, 4)}
end
- defp current_alcohol_level(state = %{ets: ets}, nick, now \\ nil) do
- meta = get_user_meta(state, nick)
+ 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
@@ -860,15 +1153,14 @@ defmodule LSG.IRC.AlcoologPlugin do
before = now
|> DateTime.add(-aday, :second)
|> DateTime.to_unix(:millisecond)
- nick = String.downcase(nick)
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
match = [
- {{{:"$1", :"$2"}, :_, :_, :_, :_},
- [
- {:>, :"$2", {:const, before}},
- {:"=:=", {:const, nick}, :"$1"}
- ], [:"$_"]}
-]
+ {{{:"$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)
@@ -880,7 +1172,7 @@ defmodule LSG.IRC.AlcoologPlugin do
date = DateTime.from_unix!(date, :millisecond)
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}"
+ #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
@@ -898,7 +1190,7 @@ defmodule LSG.IRC.AlcoologPlugin do
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})"
+ #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}
diff --git a/lib/lsg_irc/alcoolisme_plugin.ex b/lib/lsg_irc/alcoolisme_plugin.ex
deleted file mode 100644
index 03dc75b..0000000
--- a/lib/lsg_irc/alcoolisme_plugin.ex
+++ /dev/null
@@ -1,198 +0,0 @@
-defmodule LSG.IRC.AlcoolismePlugin do
- require Logger
-
- @moduledoc """
- # Alcoolisme
-
- * **!`<trigger>` `[coeff]` `[annotation]`** enregistre de l'alcoolisme.
- * **!alcoolisme `[pseudo]`** affiche les points d'alcoolisme.
-
- Triggers/Coeffs:
-
- * bière: (coeffs: 25, 50/+, 75/++, 100/+++, ++++)
- * pinard: (coeffs: +, ++, +++, ++++)
- * shot/fort/whisky/rhum/..: (coeffs: +, ++, +++, ++++)
- * eau (-1)
-
- Annotation: champ libre!
-
- """
-
- @triggers %{
- "apero" =>
- %{
- triggers: ["apero", "apéro", "apairo", "santai"],
- default_coeff: "25",
- coeffs: %{
- "+" => 2,
- "++" => 3,
- "+++" => 4,
- "++++" => 5,
- }
- },
- "bière" =>
- %{
- triggers: ["beer", "bière", "biere", "biaire"],
- default_coeff: "25",
- coeffs: %{
- "25" => 1,
- "50" => 2,
- "75" => 3,
- "100" => 4,
- "+" => 2,
- "++" => 3,
- "+++" => 4,
- "++++" => 5,
- }
- },
- "pinard" => %{
- triggers: ["pinard", "vin", "rouge", "blanc", "rosé", "rose"],
- annotations: ["rouge", "blanc", "rosé", "rose"],
- default_coeff: "",
- coeffs: %{
- "" => 1,
- "+" => 2,
- "++" => 3,
- "+++" => 4,
- "++++" => 5,
- "vase" => 6,
- }
- },
- "fort" => %{
- triggers: ["shot", "royaume", "whisky", "rhum", "armagnac", "dijo"],
- default_coeff: "",
- coeffs: %{
- "" => 3,
- "+" => 5,
- "++" => 7,
- "+++" => 9,
- "++++" => 11
- }
- },
- "eau" => %{
- triggers: ["eau"],
- default_coeff: "1",
- coeffs: %{
- "1" => -2,
- "+" => -3,
- "++" => -4,
- "+++" => -6,
- "++++" => -8
- }
- }
- }
-
- @santai ["SANTÉ", "SANTÉ", "SANTAIIII", "SANTAIIIIIIIIII", "SANTAI", "A LA TIENNE"]
-
- def irc_doc, do: @moduledoc
-
- def start_link(), do: GenServer.start_link(__MODULE__, [])
-
- def init(_) do
- {:ok, _} = Registry.register(IRC.PubSub, "trigger:alcoolisme", [])
- for {_, config} <- @triggers do
- for trigger <- config.triggers do
- {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{trigger}", [])
- for coeff when byte_size(coeff) > 0 <- Map.keys(config.coeffs) do
- {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{trigger}#{coeff}", [])
- end
- end
- end
- dets_filename = (LSG.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
- {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
- {:ok, dets}
- end
-
- for {name, config} <- @triggers do
- coeffs = Map.get(config, "coeffs")
- default_coeff_value = Map.get(config.coeffs, config.default_coeff)
-
-
- IO.puts "at triggers #{inspect config}"
- # Handle each trigger
- for trigger <- config.triggers do
-
- # … with a known coeff …
- for {coef, value} when byte_size(coef) > 0 <- config.coeffs do
- def handle_info({:irc, :trigger, unquote(trigger), m = %IRC.Message{trigger: %IRC.Trigger{args: [unquote(coef)<>_ | args], type: :bang}}}, dets) do
- handle_bang(unquote(name), unquote(value), m, args, dets)
- {:noreply, dets}
- end
- def handle_info({:irc, :trigger, unquote(trigger)<>unquote(coef), m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, dets) do
- handle_bang(unquote(name), unquote(value), m, args, dets)
- {:noreply, dets}
- end
- end
-
- # … or without
- def handle_info({:irc, :trigger, unquote(trigger), m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, dets) do
- handle_bang(unquote(name), unquote(default_coeff_value), m, args, dets)
- {:noreply, dets}
- end
-
- end
-
- end
-
- def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, dets) do
- nick = case args do
- [nick] -> nick
- [] -> m.sender.nick
- end
- case get_statistics_for_nick(dets, nick) do
- {count, {_, last_at, last_points, last_type, last_descr}} ->
- m.replyfun.("#{nick} a #{count} points d'alcoolisme. Dernier verre: #{present_type(last_type,
- last_descr)} [#{last_points}] #{format_relative_timestamp(last_at)}")
- _ ->
- m.replyfun.("honteux mais #{nick} n'a pas l'air alcoolique du tout. /kick")
- end
- {:noreply, dets}
- end
-
- defp handle_bang(name, points, message, args, dets) do
- description = case args do
- [] -> nil
- [something] -> something
- something when is_list(something) -> Enum.join(something, " ")
- _ -> nil
- end
- now = DateTime.to_unix(DateTime.utc_now(), :milliseconds)
- :ok = :dets.insert(dets, {String.downcase(message.sender.nick), now, points, name, description})
- {count, {_, last_at, last_points, last_type, last_descr}} = get_statistics_for_nick(dets, message.sender.nick)
- sante = @santai |> Enum.shuffle() |> Enum.random()
- message.replyfun.("#{sante} #{message.sender.nick} #{format_points(points)}! (total #{count} points)")
- end
-
- defp get_statistics_for_nick(dets, nick) do
- qvc = :dets.lookup(dets, String.downcase(nick))
- IO.puts inspect(qvc)
- count = Enum.reduce(qvc, 0, fn({_nick, _ts, points, _type, _descr}, acc) -> acc + points end)
- last = List.last(qvc) || nil
- {count, last}
- end
-
- def present_type(type, descr) when descr in [nil, ""], do: "#{type}"
- def present_type(type, description), do: "#{type} (#{description})"
-
- def format_points(int) when int > 0 do
- "+#{Integer.to_string(int)}"
- end
- def format_points(int) when int < 0 do
- Integer.to_string(int)
- end
- def format_points(0), do: "0"
-
- defp format_relative_timestamp(timestamp) do
- alias Timex.Format.DateTime.Formatters
- alias Timex.Timezone
- date = timestamp
- |> DateTime.from_unix!(:milliseconds)
- |> Timezone.convert("Europe/Paris")
-
- {:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
- {:ok, detail} = Formatters.Default.lformat(date, " ({h24}:{m})", "fr")
-
- relative <> detail
- end
-end
-
diff --git a/lib/lsg_irc/alcoolog_announcer_plugin.ex b/lib/lsg_irc/alcoolog_announcer_plugin.ex
index 214debd..28973ca 100644
--- a/lib/lsg_irc/alcoolog_announcer_plugin.ex
+++ b/lib/lsg_irc/alcoolog_announcer_plugin.ex
@@ -27,19 +27,22 @@ defmodule LSG.IRC.AlcoologAnnouncerPlugin do
def start_link(), do: GenServer.start_link(__MODULE__, [])
def init(_) do
+ {:ok, _} = Registry.register(IRC.PubSub, "account", [])
stats = get_stats()
Process.send_after(self(), :stats, :timer.seconds(30))
dets_filename = (LSG.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
+ #:ok = LSG.IRC.SettingPlugin.declare("alcoolog.alerts", __MODULE__, true, :boolean)
+ #:ok = LSG.IRC.SettingPlugin.declare("alcoolog.aperoalert", __MODULE__, true, :boolean)
{:ok, {stats, now(), dets}}
end
def alcohol_reached(old, new, level) do
- (old.active < level && new.active >= level)
+ (old.active < level && new.active >= level) && (new.active5m >= level)
end
-
+
def alcohol_below(old, new, level) do
- (old.active > level && new.active <= level)
+ (old.active > level && new.active <= level) && (new.active5m <= level)
end
@@ -55,25 +58,25 @@ defmodule LSG.IRC.AlcoologAnnouncerPlugin do
{:timed, list} ->
spawn(fn() ->
for line <- list do
- IRC.PubSubHandler.privmsg(@channel, line)
+ IRC.Connection.broadcast_message("evolu.net", "#dmz", line)
:timer.sleep(:timer.seconds(5))
end
end)
string ->
- IRC.PubSubHandler.privmsg(@channel, string)
+ IRC.Connection.broadcast_message("evolu.net", "#dmz", string)
end
end
- IO.puts "newstats #{inspect stats}"
- events = for {nick, old} <- old_stats do
- new = Map.get(stats, nick, nil)
- IO.puts "#{nick}: #{inspect(old)} -> #{inspect(new)}"
+ #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)}"
if new && new[:active] do
- :dets.insert(dets, {nick, DateTime.utc_now(), new[:active]})
+ :dets.insert(dets, {acct, DateTime.utc_now(), new[:active]})
else
- :dets.insert(dets, {nick, DateTime.utc_now(), 0.0})
+ :dets.insert(dets, {acct, DateTime.utc_now(), 0.0})
end
event = cond do
@@ -98,10 +101,10 @@ defmodule LSG.IRC.AlcoologAnnouncerPlugin do
(old.rising) && (!new.rising) -> :lowering
true -> nil
end
- {nick, event}
+ {acct, event}
end
- for {nick, event} <- events do
+ for {acct, event} <- events do
message = case event do
:g1 -> [
"[vigicuite jaune] LE GRAMME! LE GRAMME O/",
@@ -141,6 +144,15 @@ defmodule LSG.IRC.AlcoologAnnouncerPlugin do
]
: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 !!"
]
@@ -148,6 +160,8 @@ defmodule LSG.IRC.AlcoologAnnouncerPlugin do
"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 -> [
@@ -157,13 +171,19 @@ defmodule LSG.IRC.AlcoologAnnouncerPlugin do
"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
@@ -172,23 +192,34 @@ defmodule LSG.IRC.AlcoologAnnouncerPlugin do
m when is_list(m) -> m |> Enum.shuffle() |> Enum.random()
nil -> nil
end
- if message, do: IO.puts "#{nick}: #{message}"
- if message, do: IRC.PubSubHandler.privmsg(@channel, "#{nick}: #{message}")
+ 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"
+ #IO.puts "tick stats ok"
{:noreply, {stats,now,dets}}
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(LSG.IRC.AlcoologPlugin.get_channel_statistics(@channel), %{})
+ Enum.into(LSG.IRC.AlcoologPlugin.get_all_stats(), %{})
end
defp timer() do
diff --git a/lib/lsg_irc/base_plugin.ex b/lib/lsg_irc/base_plugin.ex
index c0cfc59..69b02e8 100644
--- a/lib/lsg_irc/base_plugin.ex
+++ b/lib/lsg_irc/base_plugin.ex
@@ -9,12 +9,85 @@ defmodule LSG.IRC.BasePlugin do
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:version", [])
{:ok, _} = Registry.register(IRC.PubSub, "trigger:help", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:liquidrender", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:plugin", [])
{:ok, nil}
end
- def handle_info({:irc, :trigger, "help", message = %{trigger: %{type: :bang}}}, _) do
- url = LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :index)
- message.replyfun.(url)
+ def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :query, args: [plugin]}} = m}, _) do
+ module = Module.concat([LSG.IRC, Macro.camelize(plugin<>"_plugin")])
+ with true <- Code.ensure_loaded?(module),
+ pid when is_pid(pid) <- GenServer.whereis(module)
+ do
+ m.replyfun.("loaded, active: #{inspect(pid)}")
+ else
+ false -> m.replyfun.("not loaded")
+ nil ->
+ msg = case IRC.Plugin.get(module) do
+ :disabled -> "disabled"
+ {_, false, _} -> "disabled"
+ _ -> "not active"
+ end
+ m.replyfun.(msg)
+ end
+ {:noreply, nil}
+ end
+
+ def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :plus, args: [plugin]}} = m}, _) do
+ module = Module.concat([LSG.IRC, Macro.camelize(plugin<>"_plugin")])
+ with true <- Code.ensure_loaded?(module),
+ IRC.Plugin.switch(module, true),
+ {:ok, pid} <- IRC.Plugin.start(module)
+ do
+ m.replyfun.("started: #{inspect(pid)}")
+ else
+ false -> m.replyfun.("not loaded")
+ :ignore -> m.replyfun.("disabled or throttled")
+ {:error, _} -> m.replyfun.("start error")
+ end
+ {:noreply, nil}
+ end
+
+ def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :tilde, args: [plugin]}} = m}, _) do
+ module = Module.concat([LSG.IRC, Macro.camelize(plugin<>"_plugin")])
+ with true <- Code.ensure_loaded?(module),
+ pid when is_pid(pid) <- GenServer.whereis(module),
+ :ok <- GenServer.stop(pid),
+ {:ok, pid} <- IRC.Plugin.start(module)
+ do
+ m.replyfun.("restarted: #{inspect(pid)}")
+ else
+ false -> m.replyfun.("not loaded")
+ nil -> m.replyfun.("not active")
+ end
+ {:noreply, nil}
+ end
+
+
+ def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :minus, args: [plugin]}} = m}, _) do
+ module = Module.concat([LSG.IRC, Macro.camelize(plugin<>"_plugin")])
+ with true <- Code.ensure_loaded?(module),
+ pid when is_pid(pid) <- GenServer.whereis(module),
+ :ok <- GenServer.stop(pid)
+ do
+ IRC.Plugin.switch(module, false)
+ m.replyfun.("stopped: #{inspect(pid)}")
+ else
+ false -> m.replyfun.("not loaded")
+ nil -> m.replyfun.("not active")
+ end
+ {:noreply, nil}
+ end
+
+ def handle_info({:irc, :trigger, "liquidrender", m = %{trigger: %{args: args}}}, _) do
+ template = Enum.join(args, " ")
+ m.replyfun.(Tmpl.render(template, m))
+ {:noreply, nil}
+ end
+
+ def handle_info({:irc, :trigger, "help", m = %{trigger: %{type: :bang}}}, _) do
+ url = LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :index, m.network, LSGWeb.format_chan(m.channel))
+ m.replyfun.("-> #{url}")
{:noreply, nil}
end
diff --git a/lib/lsg_irc/calc_plugin.ex b/lib/lsg_irc/calc_plugin.ex
index 6e4e30c..b8eee39 100644
--- a/lib/lsg_irc/calc_plugin.ex
+++ b/lib/lsg_irc/calc_plugin.ex
@@ -24,7 +24,7 @@ defmodule LSG.IRC.CalcPlugin do
error -> inspect(error)
end
rescue
- error -> "#{error.message}"
+ error -> if(error[:message], do: "#{error.message}", else: "erreur")
end
message.replyfun.("#{message.sender.nick}: #{expr} = #{result}")
{:noreply, state}
diff --git a/lib/lsg_irc/coronavirus_plugin.ex b/lib/lsg_irc/coronavirus_plugin.ex
index debefa3..8038d14 100644
--- a/lib/lsg_irc/coronavirus_plugin.ex
+++ b/lib/lsg_irc/coronavirus_plugin.ex
@@ -102,7 +102,8 @@ defmodule LSG.IRC.CoronavirusPlugin do
|> Enum.reduce(%{}, fn(line, acc) ->
case line do
# FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Deaths,Recovered,Active,Combined_Key
- [_, _, state, region, update, _lat, _lng, 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)
diff --git a/lib/lsg_irc/correction_plugin.ex b/lib/lsg_irc/correction_plugin.ex
index a5d3d31..e7b2577 100644
--- a/lib/lsg_irc/correction_plugin.ex
+++ b/lib/lsg_irc/correction_plugin.ex
@@ -12,10 +12,21 @@ defmodule LSG.IRC.CorrectionPlugin do
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "message", [])
- {:ok, []}
+ {:ok, _} = Registry.register(IRC.PubSub, "triggers", [])
+ {:ok, %{}}
end
- def handle_info({:irc, :text, m = %IRC.Message{}}, history) do
+ # 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
+
+ defp 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 | _] ->
@@ -31,7 +42,7 @@ defmodule LSG.IRC.CorrectionPlugin do
end
_ -> m.replyfun.("correction: invalid regex format")
end
- {:noreply, history}
+ state
else
history = if length(history) > 100 do
{_, history} = List.pop_at(history, 99)
@@ -39,8 +50,10 @@ defmodule LSG.IRC.CorrectionPlugin do
else
[m | history]
end
- {:noreply, history}
+ Map.put(state, key(m), history)
end
end
+ defp key(%{network: net, channel: chan}), do: "#{net}/#{chan}"
+
end
diff --git a/lib/lsg_irc/last_fm_plugin.ex b/lib/lsg_irc/last_fm_plugin.ex
index 2446473..067a44e 100644
--- a/lib/lsg_irc/last_fm_plugin.ex
+++ b/lib/lsg_irc/last_fm_plugin.ex
@@ -18,6 +18,7 @@ defmodule LSG.IRC.LastFmPlugin do
end
def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "account", [])
{:ok, _} = Registry.register(IRC.PubSub, "trigger:lastfm", [])
{:ok, _} = Registry.register(IRC.PubSub, "trigger:lastfmall", [])
dets_filename = (LSG.data_path() <> "/" <> "lastfm.dets") |> String.to_charlist
@@ -27,13 +28,13 @@ defmodule LSG.IRC.LastFmPlugin do
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :plus, args: [username]}}}, state) do
username = String.strip(username)
- :ok = :dets.insert(state.dets, {String.downcase(message.sender.nick), 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.sender.nick) do
+ text = case :dets.lookup(state.dets, message.account.id) do
[{_nick, username}] ->
:dets.delete(state.dets, String.downcase(message.sender.nick))
message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm enlevé.")
@@ -43,7 +44,7 @@ defmodule LSG.IRC.LastFmPlugin do
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :bang, args: []}}}, state) do
- irc_now_playing(message.sender.nick, message, state)
+ irc_now_playing(message.account.id, message, state)
{:noreply, state}
end
@@ -53,9 +54,12 @@ defmodule LSG.IRC.LastFmPlugin do
end
def handle_info({:irc, :trigger, "lastfmall", message = %{trigger: %{type: :bang}}}, state) do
- foldfun = fn({_nick, user}, acc) -> [user|acc] end
+ 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.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
@@ -74,6 +78,12 @@ defmodule LSG.IRC.LastFmPlugin do
defp irc_now_playing(nick_or_user, message, state) do
nick_or_user = String.strip(nick_or_user)
+ if account = 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, String.downcase(nick_or_user)) do
[{^nick_or_user, username}] -> username
_ -> nick_or_user
diff --git a/lib/lsg_irc/link_plugin.ex b/lib/lsg_irc/link_plugin.ex
index bc9764a..97835e4 100644
--- a/lib/lsg_irc/link_plugin.ex
+++ b/lib/lsg_irc/link_plugin.ex
@@ -39,7 +39,7 @@ defmodule LSG.IRC.LinkPlugin do
require Logger
def start_link() do
- GenServer.start_link(__MODULE__, [])
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@callback match(uri :: URI.t, options :: Keyword.t) :: {true, params :: Map.t} | false
@@ -49,6 +49,7 @@ defmodule LSG.IRC.LinkPlugin do
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "message", [])
+ #{:ok, _} = Registry.register(IRC.PubSub, "message:telegram", [])
Logger.info("Link handler started")
{:ok, %__MODULE__{}}
end
@@ -66,7 +67,12 @@ defmodule LSG.IRC.LinkPlugin do
[uri] -> text
[uri | _] -> ["-> #{URI.to_string(uri)}", text]
end
- message.replyfun.(text)
+ IO.inspect(text)
+ if is_list(text) do
+ for line <- text, do: message.replyfun.(line)
+ else
+ message.replyfun.(text)
+ end
_ -> nil
end
end)
@@ -91,8 +97,8 @@ defmodule LSG.IRC.LinkPlugin do
# 4. ?
# Over five redirections: cancel.
- def expand_link([_, _, _, _, _, _ | _]) do
- :error
+ def expand_link(acc = [_, _, _, _, _ | _]) do
+ {:ok, acc, "link redirects more than five times"}
end
def expand_link(acc=[uri | _]) do
@@ -128,14 +134,14 @@ defmodule LSG.IRC.LinkPlugin do
end
defp get(url, headers \\ [], options \\ []) do
- get_req(:hackney.get(url, headers, <<>>, options))
+ get_req(url, :hackney.get(url, headers, <<>>, options))
end
- defp get_req({:error, reason}) do
+ defp get_req(_, {:error, reason}) do
{:error, reason}
end
- defp get_req({:ok, 200, headers, client}) do
+ 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)
@@ -145,14 +151,14 @@ defmodule LSG.IRC.LinkPlugin do
cond do
String.starts_with?(content_type, "text/html") && length <= 30_000_000 ->
- get_body(30_000_000, client, <<>>)
+ get_body(url, 30_000_000, client, <<>>)
true ->
:hackney.close(client)
{:ok, "file: #{content_type}, size: #{length} bytes"}
end
end
- defp get_req({:ok, redirect, headers, client}) when redirect in 300..399 do
+ 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)
@@ -162,32 +168,70 @@ defmodule LSG.IRC.LinkPlugin do
{:redirect, location}
end
- defp get_req({:ok, status, headers, client}) do
+ defp get_req(_, {:ok, status, headers, client}) do
:hackney.close(client)
{:error, status, headers}
end
- defp get_body(len, client, acc) when len >= byte_size(acc) do
+ defp get_body(url, len, client, acc) when len >= byte_size(acc) do
case :hackney.stream_body(client) do
{:ok, data} ->
- get_body(len, client, << acc::binary, data::binary >>)
+ get_body(url, len, client, << acc::binary, data::binary >>)
:done ->
html = Floki.parse(acc)
- title = case Floki.find(html, "title") do
- [{"title", [], [title]} | _] ->
- String.trim(title)
- _ ->
- nil
+ title = collect_title(html)
+ opengraph = collect_open_graph(html)
+ itemprops = collect_itemprops(html)
+ Logger.debug("OG: #{inspect opengraph}")
+ 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, title}
+ {:ok, text}
{:error, reason} ->
{:ok, "failed to fetch body: #{inspect reason}"}
end
end
+ defp clean_text(text) do
+ text
+ |> String.replace("\n", " ")
+ |> HtmlEntities.decode()
+ end
+
defp get_body(len, client, _acc) do
:hackney.close(client)
- {:ok, "mais il rentrera jamais en ram ce fichier !"}
+ {:ok, "Error: file over 30"}
end
def expand_default(acc = [uri = %URI{scheme: scheme} | _]) when scheme in ["http", "https"] do
@@ -201,9 +245,10 @@ defmodule LSG.IRC.LinkPlugin do
new_uri = %URI{new_uri | scheme: scheme, authority: uri.authority, host: uri.host, port: uri.port}
expand_link([new_uri | acc])
{:error, status, _headers} ->
- {:ok, acc, "Error #{status}"}
+ text = Plug.Conn.Status.reason_phrase(status)
+ {:ok, acc, "Error: HTTP #{text} (#{status})"}
{:error, reason} ->
- {:ok, acc, "Error #{to_string(reason)}"}
+ {:ok, acc, "Error: #{to_string(reason)}"}
end
end
@@ -212,4 +257,47 @@ defmodule LSG.IRC.LinkPlugin do
{:ok, [uri], "-> #{URI.to_string(uri)}"}
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
+
end
diff --git a/lib/lsg_irc/link_plugin/github.ex b/lib/lsg_irc/link_plugin/github.ex
new file mode 100644
index 0000000..c7444c2
--- /dev/null
+++ b/lib/lsg_irc/link_plugin/github.ex
@@ -0,0 +1,44 @@
+defmodule LSG.IRC.LinkPlugin.Github do
+ @behaviour LSG.IRC.LinkPlugin
+
+ 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
+
+ def expand(_uri, %{user: user, repo: repo}, _opts) do
+ case HTTPoison.get("https://api.github.com/repos/#{user}/#{repo}") do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ {:ok, json} = Jason.decode(body)
+ src = json["source"]["full_name"]
+ disabled = if(json["disabled"], do: " (disabled)", else: "")
+ archived = if(json["archived"], do: " (archived)", else: "")
+ fork = if src && src != json["full_name"] do
+ " (⑂ #{json["source"]["full_name"]})"
+ else
+ ""
+ end
+ start = "#{json["full_name"]}#{disabled}#{archived}#{fork} - #{json["description"]}"
+ tags = for(t <- json["topics"]||[], do: "##{t}") |> Enum.intersperse(", ") |> Enum.join("")
+ lang = if(json["language"], do: "#{json["language"]} - ", else: "")
+ issues = if(json["open_issues_count"], do: "#{json["open_issues_count"]} issues - ", else: "")
+ last_push = if at = json["pushed_at"] do
+ {:ok, date, _} = DateTime.from_iso8601(at)
+ " - last pushed #{DateTime.to_string(date)}"
+ else
+ ""
+ end
+ network = "#{lang}#{issues}#{json["stargazers_count"]} stars - #{json["subscribers_count"]} watchers - #{json["forks_count"]} forks#{last_push}"
+ {:ok, [start, tags, network]}
+ other ->
+ :error
+ end
+ end
+
+end
diff --git a/lib/lsg_irc/link_plugin/reddit_plugin.ex b/lib/lsg_irc/link_plugin/reddit_plugin.ex
new file mode 100644
index 0000000..a7f5235
--- /dev/null
+++ b/lib/lsg_irc/link_plugin/reddit_plugin.ex
@@ -0,0 +1,114 @@
+defmodule LSG.IRC.LinkPlugin.Reddit do
+ @behaviour LSG.IRC.LinkPlugin
+
+ 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
+
+ def expand(_, %{mode: :sub, sub: sub}, _opts) do
+ url = "https://api.reddit.com/r/#{sub}/about"
+ case HTTPoison.get(url) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ sr = Jason.decode!(body)
+ |> Map.get("data")
+ |> IO.inspect(limit: :infinity)
+ description = Map.get(sr, "public_description")||Map.get(sr, "description", "")
+ |> String.split("\n")
+ |> List.first()
+ name = if title = Map.get(sr, "title") do
+ Map.get(sr, "display_name_prefixed") <> ": " <> title
+ else
+ Map.get(sr, "display_name_prefixed")
+ end
+ nsfw = if Map.get(sr, "over18") do
+ "[NSFW] "
+ else
+ ""
+ end
+ quarantine = if Map.get(sr, "quarantine") do
+ "[Quarantined] "
+ else
+ ""
+ end
+ count = "#{Map.get(sr, "subscribers")} subscribers, #{Map.get(sr, "active_user_count")} active"
+ preview = "#{quarantine}#{nsfw}#{name} — #{description} (#{count})"
+ {:ok, preview}
+ _ ->
+ :error
+ end
+ end
+
+ def expand(_uri, %{mode: :post, path: path, sub: sub, post_id: post_id}, _opts) do
+ case HTTPoison.get("https://api.reddit.com#{path}?sr_detail=true") do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ json = Jason.decode!(body)
+ op = List.first(json)
+ |> Map.get("data")
+ |> Map.get("children")
+ |> List.first()
+ |> Map.get("data")
+ |> IO.inspect(limit: :infinity)
+ sr = get_in(op, ["sr_detail", "display_name_prefixed"])
+ {self?, url} = if Map.get(op, "selftext") == "" do
+ {false, Map.get(op, "url")}
+ else
+ {true, nil}
+ end
+
+ self_str = if(self?, do: "text", else: url)
+ up = Map.get(op, "ups")
+ down = Map.get(op, "downs")
+ comments = Map.get(op, "num_comments")
+ nsfw = if Map.get(op, "over_18") do
+ "[NSFW] "
+ else
+ ""
+ end
+ state = cond do
+ Map.get(op, "hidden") -> "hidden"
+ Map.get(op, "archived") -> "archived"
+ Map.get(op, "locked") -> "locked"
+ Map.get(op, "quarantine") -> "quarantined"
+ Map.get(op, "removed_by") || Map.get(op, "removed_by_category") -> "removed"
+ Map.get(op, "banned_by") -> "banned"
+ Map.get(op, "pinned") -> "pinned"
+ Map.get(op, "stickied") -> "stickied"
+ true -> nil
+ end
+ flair = if flair = Map.get(op, "link_flair_text") do
+ "[#{flair}] "
+ else
+ ""
+ end
+ title = "#{nsfw}#{sr}: #{flair}#{Map.get(op, "title")}"
+ state_str = if(state, do: "#{state}, ")
+ content = "by u/#{Map.get(op, "author")} - #{state_str}#{up} up, #{down} down, #{comments} comments - #{self_str}"
+
+ {:ok, [title, content]}
+ err ->
+ :error
+ end
+ end
+
+end
diff --git a/lib/lsg_irc/link_plugin/twitter.ex b/lib/lsg_irc/link_plugin/twitter.ex
index 04dea7c..a6b6e29 100644
--- a/lib/lsg_irc/link_plugin/twitter.ex
+++ b/lib/lsg_irc/link_plugin/twitter.ex
@@ -55,6 +55,17 @@ defmodule LSG.IRC.LinkPlugin.Twitter do
{:ok, at} = Timex.parse(tweet.created_at, "%a %b %e %H:%M:%S %z %Y", :strftime)
{:ok, format} = Timex.format(at, "{relative}", :relative)
+ replyto = if tweet.in_reply_to_status_id do
+ replyurl = "https://twitter.com/#{tweet.in_reply_to_screen_name}/status/#{tweet.in_reply_to_status_id}"
+ if tweet.in_reply_to_screen_name == tweet.user.screen_name do
+ "— continued from #{replyurl}"
+ else
+ "— replying to #{replyurl}"
+ end
+ else
+ ""
+ end
+
quoted = if tweet.quoted_status do
quote_url = "https://twitter.com/#{tweet.quoted_status.user.screen_name}/status/#{tweet.quoted_status.id}"
full_text = expand_twitter_text(tweet.quoted_status)
@@ -66,7 +77,7 @@ defmodule LSG.IRC.LinkPlugin.Twitter do
foot = "— #{format} - #{tweet.retweet_count} retweets - #{tweet.favorite_count} likes"
- text = ["#{tweet.user.name} (@#{tweet.user.screen_name}):"] ++ text ++ quoted ++ [foot]
+ text = ["#{tweet.user.name} (@#{tweet.user.screen_name}):", replyto] ++ text ++ quoted ++ [foot]
{:ok, text}
end
diff --git a/lib/lsg_irc/preums_plugin.ex b/lib/lsg_irc/preums_plugin.ex
index 98cd539..1f9a76b 100644
--- a/lib/lsg_irc/preums_plugin.ex
+++ b/lib/lsg_irc/preums_plugin.ex
@@ -6,31 +6,94 @@ defmodule LSG.IRC.PreumsPlugin do
* `.preums`: stats des preums
"""
+ 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}, nick, time, perfect, text}, acc) ->
+ if channel == chan do
+ [%{date: date, nick: nick, time: time, perfect: perfect, text: text} | acc]
+ else
+ acc
+ end
+ end
+ :dets.foldl(fun, [], dets)
+ end
+
+ def topnicks(dets, channel) do
+ fun = fn(x = {{chan, date}, nick, _time, _perfect, _text}, acc) ->
+ if (channel == nil and chan) or (channel == chan) do
+ count = Map.get(acc, nick, 0)
+ Map.put(acc, nick, count + 1)
+ else
+ acc
+ end
+ end
+ :dets.foldl(fun, %{}, dets)
+ |> Enum.sort_by(fn({nick, count}) -> count end, &>=/2)
+ end
+
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [])
end
+ def dets do
+ (LSG.data_path() <> "/preums.dets") |> String.to_charlist()
+ end
+
def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "account", [])
{:ok, _} = Registry.register(IRC.PubSub, "message", [])
{:ok, _} = Registry.register(IRC.PubSub, "triggers", [])
- dets_filename = (LSG.data_path() <> "/preums.dets") |> String.to_charlist()
- {:ok, dets} = :dets.open_file(dets_filename, [])
+ {:ok, dets} = :dets.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{channel: channel, trigger: %IRC.Trigger{type: :bang}}}, state) do
+ 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(channel)
+ tz = timezone(channelkey)
{:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
date = {now.year, now.month, now.day}
- key = {channel, date}
- chan_cache = Map.get(state, channel, %{})
+ key = {channelkey, date}
+ chan_cache = Map.get(state, channelkey, %{})
item = if i = Map.get(chan_cache, date) do
i
else
@@ -49,14 +112,30 @@ defmodule LSG.IRC.PreumsPlugin do
end
# Stats
- def handle_info({:irc, :trigger, "preums", m = %IRC.Message{channel: channel, trigger: %IRC.Trigger{type: :dot}}}, state) do
+ 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)
+ |> Enum.map(fn({nick, count}) ->
+ "#{nick} (#{count})"
+ end)
+ |> Enum.filter(fn(x) -> x 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{channel: channel, trigger: %IRC.Trigger{type: :query}}}, state) do
+ 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
@@ -71,6 +150,43 @@ defmodule LSG.IRC.PreumsPlugin 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(:lsg, LSG.IRC.PreumsPlugin, [])
channels = Keyword.get(env, :channels, %{})
@@ -79,7 +195,12 @@ defmodule LSG.IRC.PreumsPlugin do
Keyword.get(channel_settings, :tz, default) || default
end
- defp handle_preums(m = %IRC.Message{channel: channel, text: text, sender: sender}, state) do
+ 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}
@@ -89,15 +210,16 @@ defmodule LSG.IRC.PreumsPlugin do
case :dets.lookup(state.dets, key) do
[item = {^key, _nick, _now, _perfect, _text}] ->
# Preums lost, but wasn't cached
- state = Map.put(state, channel, %{date => item})
- state
- _ ->
+ Map.put(state, channel, %{date => item})
+ [] ->
# Preums won!
perfect? = Enum.any?(@perfects, fn(perfect) -> Regex.match?(perfect, text) end)
- item = {key, sender.nick, now, perfect?, text}
+ item = {key, m.account.id, now, perfect?, text}
:dets.insert(state.dets, item)
:dets.sync(state.dets)
- state = Map.put(state, channel, %{date => item})
+ Map.put(state, channel, %{date => item})
+ {:error, _} = error ->
+ Logger.error("#{__MODULE__} dets lookup failed: #{inspect error}")
state
end
else
diff --git a/lib/lsg_irc/quatre_cent_vingt_plugin.ex b/lib/lsg_irc/quatre_cent_vingt_plugin.ex
index f6f8a63..db85d49 100644
--- a/lib/lsg_irc/quatre_cent_vingt_plugin.ex
+++ b/lib/lsg_irc/quatre_cent_vingt_plugin.ex
@@ -36,20 +36,21 @@ defmodule LSG.IRC.QuatreCentVingtPlugin do
for coeff <- @coeffs do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:#{420*coeff}", [])
end
- dets_filename = (LSG.data_path() <> "/" <> "420.dets") |> String.to_charlist
- {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
+ {:ok, _} = Registry.register(IRC.PubSub, "account", [])
+ dets_filename = (LSG.data_path() <> "/420.dets") |> String.to_charlist
+ {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag},{:repair,:force}])
{:ok, dets}
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.sender.nick)
+ {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, {String.downcase(m.sender.nick), now+i})
+ :ok = :dets.insert(dets, {m.account.id, now+i})
end
last_s = if last do
last_s = format_relative_timestamp(last)
@@ -63,15 +64,56 @@ defmodule LSG.IRC.QuatreCentVingtPlugin do
end
def handle_info({:irc, :trigger, "420", m = %IRC.Message{trigger: %IRC.Trigger{args: [nick], type: :bang}}}, dets) do
- text = case get_statistics_for_nick(dets, nick) 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}"
+ 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
- m.replyfun.(text)
{: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
@@ -86,8 +128,8 @@ defmodule LSG.IRC.QuatreCentVingtPlugin do
relative <> detail
end
- defp get_statistics_for_nick(dets, nick) do
- qvc = :dets.lookup(dets, String.downcase(nick)) |> Enum.sort
+ 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}
diff --git a/lib/lsg_irc/say_plugin.ex b/lib/lsg_irc/say_plugin.ex
new file mode 100644
index 0000000..6a4f547
--- /dev/null
+++ b/lib/lsg_irc/say_plugin.ex
@@ -0,0 +1,71 @@
+defmodule LSG.IRC.SayPlugin do
+
+ def irc_doc do
+ """
+ # say
+
+ Say something...
+
+ * **!say `<channel>` `<text>`** say something on `channel`
+ * **!asay `<channel>` `<text>`** same but anonymously
+
+ You must be a member of the channel.
+ """
+ end
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:say", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:asay", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "message:private", [])
+ {: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
+ user = IRC.UserTrack.find_by_account(net, account)
+ nick = if(user, do: user.nick, else: account.name)
+ prefix = if(with_nick?, do: "<#{nick}> ", else: "")
+ IRC.Connection.broadcast_message(net, chan, "#{prefix}#{text}")
+ end
+ end
+ end
+
+end
diff --git a/lib/lsg_irc/script_plugin.ex b/lib/lsg_irc/script_plugin.ex
new file mode 100644
index 0000000..28ae2a7
--- /dev/null
+++ b/lib/lsg_irc/script_plugin.ex
@@ -0,0 +1,43 @@
+defmodule LSG.IRC.ScriptPlugin do
+ require Logger
+
+ @moduledoc """
+ Allows to run outside scripts. Scripts are expected to be long running and receive/send data as JSON over stdin/stdout.
+
+ """
+
+ @ircdoc """
+ # script
+
+ Allows to run an outside script.
+
+ * **+script `<name>` `[command]`** défini/lance un script
+ * **-script `<name>`** arrête un script
+ * **-script del `<name>`** supprime un script
+ """
+
+ def irc_doc, do: @ircdoc
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:script", [])
+ dets_filename = (LSG.data_path() <> "/" <> "scripts.dets") |> String.to_charlist
+ {:ok, dets} = :dets.open_file(dets_filename, [])
+ {:ok, %{dets: dets}}
+ end
+
+ def handle_info({:irc, :trigger, "script", m = %{trigger: %{type: :plus, args: [name | args]}}}, state) do
+ end
+
+ def handle_info({:irc, :trigger, "script", m = %{trigger: %{type: :minus, args: args}}}, state) do
+ case args do
+ ["del", name] -> :ok #prout
+ [name] -> :ok#stop
+ end
+ end
+
+end
+
diff --git a/lib/lsg_irc/sms_plugin.ex b/lib/lsg_irc/sms_plugin.ex
new file mode 100644
index 0000000..60554fb
--- /dev/null
+++ b/lib/lsg_irc/sms_plugin.ex
@@ -0,0 +1,163 @@
+defmodule LSG.IRC.SmsPlugin do
+ @moduledoc """
+ ## sms
+
+ * **!sms `<nick>` `<message>`** envoie un SMS.
+ """
+ def short_irc_doc, do: false
+ def irc_doc, do: @moduledoc
+ require Logger
+
+ def incoming(from, "enable "<>key) do
+ key = String.trim(key)
+ account = IRC.Account.find_meta_account("sms-validation-code", String.downcase(key))
+ if account do
+ net = IRC.Account.get_meta(account, "sms-validation-target")
+ IRC.Account.put_meta(account, "sms-number", from)
+ IRC.Account.delete_meta(account, "sms-validation-code")
+ IRC.Account.delete_meta(account, "sms-validation-number")
+ IRC.Account.delete_meta(account, "sms-validation-target")
+ IRC.Connection.broadcast_message(net, account, "SMS Number #{from} added!")
+ send_sms(from, "Yay! Number linked to account #{account.name}")
+ end
+ end
+
+ def incoming(from, message) do
+ account = IRC.Account.find_meta_account("sms-number", from)
+ if account do
+ reply_fun = fn(text) ->
+ send_sms(from, text)
+ end
+ trigger_text = if Enum.any?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(message, trigger) end) do
+ message
+ else
+ "!"<>message
+ end
+ message = %IRC.Message{
+ 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)
+ }
+ IO.puts("converted sms to message: #{inspect message}")
+ IRC.Connection.publish(message, ["message:sms"])
+ message
+ end
+ end
+
+ def my_number() do
+ Keyword.get(Application.get_env(:lsg, :sms, []), :number, "+33000000000")
+ end
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ def path() do
+ account = Keyword.get(Application.get_env(:lsg, :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", [])
+ :ok = register_ovh_callback()
+ {:ok, %{}}
+ 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" =>LSGWeb.Router.Helpers.sms_url(LSGWeb.Endpoint, :ovh_callback),
+ "smsResponse" => %{
+ "cgiUrl" => LSGWeb.Router.Helpers.sms_url(LSGWeb.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(:lsg, :sms)
+ end
+
+ defp env(key) do
+ Keyword.get(env(), key)
+ end
+end
diff --git a/lib/lsg_irc/txt_plugin.ex b/lib/lsg_irc/txt_plugin.ex
index ca1be9c..f8c3a29 100644
--- a/lib/lsg_irc/txt_plugin.ex
+++ b/lib/lsg_irc/txt_plugin.ex
@@ -3,11 +3,11 @@ defmodule LSG.IRC.TxtPlugin do
require Logger
@moduledoc """
- # [txt](/irc/txt)
+ # [txt]({{context_path}}/txt)
* **.txt**: liste des fichiers et statistiques.
Les fichiers avec une `*` sont vérrouillés.
- [Voir sur le web](/irc/txt).
+ [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.
@@ -56,7 +56,7 @@ defmodule LSG.IRC.TxtPlugin do
#
def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :plus}}}, state = %{rw: false}) do
- if channel && UserTrack.operator?(channel, msg.sender.nick) do
+ if channel && UserTrack.operator?(msg.network, channel, msg.sender.nick) do
msg.replyfun.("txt: écriture réactivée")
{:noreply, %__MODULE__{state | rw: true}}
else
@@ -65,7 +65,7 @@ defmodule LSG.IRC.TxtPlugin do
end
def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :minus}}}, state = %{rw: true}) do
- if channel && UserTrack.operator?(channel, msg.sender.nick) do
+ if channel && UserTrack.operator?(msg.network, channel, msg.sender.nick) do
msg.replyfun.("txt: écriture désactivée")
{:noreply, %__MODULE__{state | rw: false}}
else
@@ -80,7 +80,7 @@ defmodule LSG.IRC.TxtPlugin do
def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
- true <- UserTrack.operator?(msg.channel, msg.sender.nick)
+ true <- UserTrack.operator?(msg.network, msg.channel, msg.sender.nick)
do
:dets.insert(state.locks, {trigger})
msg.replyfun.("txt: #{trigger} verrouillé")
@@ -91,7 +91,7 @@ defmodule LSG.IRC.TxtPlugin do
def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :minus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
- true <- UserTrack.operator?(msg.channel, msg.sender.nick),
+ true <- UserTrack.operator?(msg.network, msg.channel, msg.sender.nick),
true <- :dets.member(state.locks, trigger)
do
:dets.delete(state.locks, trigger)
@@ -132,7 +132,6 @@ defmodule LSG.IRC.TxtPlugin do
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: []}}}, state) do
result = Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
- IO.puts inspect(data)
Enum.reduce(data, acc, fn({l, _}, acc) ->
[{trigger, l} | acc]
end)
@@ -141,28 +140,92 @@ defmodule LSG.IRC.TxtPlugin do
if !Enum.empty?(result) do
{source, line} = Enum.random(result)
- msg.replyfun.(format_line(line, "#{source}: "))
+ 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, " ")
- result = Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
- Enum.reduce(data, acc, fn({l, _}, acc) ->
- [{trigger, l} | acc]
+ result = with_stateful_results(msg, {:bang,"txt",grep}, fn() ->
+ Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
+ Enum.reduce(data, acc, fn({l, _}, acc) ->
+ [{trigger, l} | acc]
+ end)
end)
+ |> Enum.filter(fn({_, line}) -> String.contains?(String.downcase(line), String.downcase(grep)) end)
+ |> Enum.shuffle()
end)
- |> Enum.filter(fn({_, line}) -> String.contains?(String.downcase(line), String.downcase(grep)) end)
- |> Enum.shuffle()
- if !Enum.empty?(result) do
- {source, line} = Enum.random(result)
+ 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
+ IO.puts("Stateful results key is #{inspect key}")
+ 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
#
@@ -209,12 +272,25 @@ defmodule LSG.IRC.TxtPlugin do
# 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
+ LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :txt, m.network, LSGWeb.format_chan(m.channel), trigger)
+ else
+ LSGWeb.Router.Helpers.irc_url(LSGWeb.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)
IO.puts "OPTS : #{inspect {trigger, opts}}"
- line = get_random(state.triggers, trigger, String.trim(Enum.join(opts, " ")))
+ line = get_random(msg, state.triggers, trigger, String.trim(Enum.join(opts, " ")))
if line do
- msg.replyfun.(format_line(line))
+ msg.replyfun.(format_line(line, nil, msg))
end
{:noreply, state}
end
@@ -310,7 +386,7 @@ defmodule LSG.IRC.TxtPlugin do
File.write!(directory() <> "/" <> trigger <> ".txt", data<>"\n", [])
end
- defp get_random(triggers, trigger, []) do
+ defp get_random(msg, triggers, trigger, []) do
if data = Map.get(triggers, trigger) do
{data, _idx} = Enum.random(data)
data
@@ -319,16 +395,16 @@ defmodule LSG.IRC.TxtPlugin do
end
end
- defp get_random(triggers, trigger, opt) do
+ 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(triggers, trigger, arg)
+ get_with_param(msg, triggers, trigger, arg)
end
- defp get_with_param(triggers, trigger, {:index, pos}) do
+ 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
@@ -336,14 +412,15 @@ defmodule LSG.IRC.TxtPlugin do
end
end
- defp get_with_param(triggers, trigger, {:grep, query}) do
- data = Map.get(triggers, trigger, %{})
- regex = Regex.compile!("#{query}", "i")
- out = Enum.filter(data, fn({txt, _}) -> Regex.match?(regex, txt) end)
- |> Enum.map(fn({txt, _}) -> txt end)
- if !Enum.empty?(out) do
- Enum.random(out)
- 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
@@ -380,7 +457,8 @@ defmodule LSG.IRC.TxtPlugin do
{trigger, opts}
end
- defp format_line(line, prefix \\ "") do
+ defp format_line(line, prefix, msg) do
+ prefix = unless(prefix, do: "", else: prefix)
prefix <> line
|> String.split("\\\\")
|> Enum.map(fn(line) ->
@@ -389,6 +467,7 @@ defmodule LSG.IRC.TxtPlugin do
|> List.flatten()
|> Enum.map(fn(line) ->
String.trim(line)
+ |> Tmpl.render(msg)
end)
end
@@ -415,7 +494,7 @@ defmodule LSG.IRC.TxtPlugin do
defp can_write?(state = %__MODULE__{rw: rw?, locks: locks}, msg = %{channel: channel, sender: sender}, trigger) do
admin? = IRC.admin?(sender)
- operator? = IRC.UserTrack.operator?(channel, sender.nick)
+ operator? = IRC.UserTrack.operator?(msg.network, channel, sender.nick)
locked? = case :dets.lookup(locks, trigger) do
[{trigger}] -> true
_ -> false
diff --git a/lib/lsg_irc/untappd_plugin.ex b/lib/lsg_irc/untappd_plugin.ex
new file mode 100644
index 0000000..02b22e3
--- /dev/null
+++ b/lib/lsg_irc/untappd_plugin.ex
@@ -0,0 +1,66 @@
+defmodule LSG.IRC.UntappdPlugin do
+
+ def irc_doc() do
+ """
+ # [Untappd](https://untappd.com)
+
+ * `!beer <beer name>` Information about the first beer matching `<beer name>`
+ * `?beer <beer name>` List the 10 firsts beer matching `<beer name>`
+
+ _Note_: The best way to search is always "Brewery Name + Beer Name", such as "Dogfish 60 Minute".
+
+ Link your Untappd account to the bot (for automated checkins on [alcoolog](#alcoolog), ...) with the `enable-untappd` command, in private.
+ """
+ end
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
+ end
+
+ def init(_) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:beer", [])
+ {:ok, %{}}
+ end
+
+ def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %{type: :bang, args: args}}}, state) do
+ case Untappd.search_beer(Enum.join(args, " "), limit: 1) do
+ {:ok, %{"response" => %{"beers" => %{"count" => count, "items" => [result | _]}}}} ->
+ %{"beer" => beer, "brewery" => brewery} = result
+ description = Map.get(beer, "beer_description")
+ |> String.replace("\n", " ")
+ |> String.replace("\r", " ")
+ |> String.trim()
+ beer_s = "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")} - #{Map.get(beer, "beer_abv")}°"
+ city = get_in(brewery, ["location", "brewery_city"])
+ location = [Map.get(brewery, "brewery_type"), city, Map.get(brewery, "country_name")]
+ |> Enum.filter(fn(x) -> x end)
+ |> Enum.join(", ")
+ extra = "#{Map.get(beer, "beer_style")} - IBU: #{Map.get(beer, "beer_ibu")} - #{location}"
+ m.replyfun.([beer_s, extra, description])
+ err ->
+ m.replyfun.("Error")
+ end
+ {:noreply, state}
+ end
+
+
+ def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %{type: :query, args: args}}}, state) do
+ case Untappd.search_beer(Enum.join(args, " ")) do
+ {:ok, %{"response" => %{"beers" => %{"count" => count, "items" => results}}}} ->
+ beers = for %{"beer" => beer, "brewery" => brewery} <- results do
+ "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")} - #{Map.get(beer, "beer_abv")}°"
+ end
+ |> Enum.intersperse(", ")
+ |> Enum.join("")
+ m.replyfun.("#{count}. #{beers}")
+ err ->
+ m.replyfun.("Error")
+ end
+ {:noreply, state}
+ end
+
+ def handle_info(info, state) do
+ {:noreply, state}
+ end
+
+end
diff --git a/lib/lsg_web.ex b/lib/lsg_web.ex
index e07d648..f5dee6f 100644
--- a/lib/lsg_web.ex
+++ b/lib/lsg_web.ex
@@ -17,6 +17,22 @@ defmodule LSGWeb do
and import those modules here.
"""
+ def format_chan("#") do
+ "♯"
+ end
+
+ def format_chan("#"<>chan) do
+ chan
+ end
+
+ def reformat_chan("♯") do
+ ""
+ end
+
+ def reformat_chan(chan) do
+ "#"<>chan
+ end
+
def controller do
quote do
use Phoenix.Controller, namespace: LSGWeb
diff --git a/lib/lsg_web/channels/user_socket.ex b/lib/lsg_web/channels/user_socket.ex
index 6fedf18..691a26c 100644
--- a/lib/lsg_web/channels/user_socket.ex
+++ b/lib/lsg_web/channels/user_socket.ex
@@ -5,7 +5,7 @@ defmodule LSGWeb.UserSocket do
# channel "room:*", LSGWeb.RoomChannel
## Transports
- transport :websocket, Phoenix.Transports.WebSocket
+ #transport :websocket, Phoenix.Transports.WebSocket
# transport :longpoll, Phoenix.Transports.LongPoll
# Socket params are passed from the client and can
diff --git a/lib/lsg_web/context_plug.ex b/lib/lsg_web/context_plug.ex
new file mode 100644
index 0000000..7896ace
--- /dev/null
+++ b/lib/lsg_web/context_plug.ex
@@ -0,0 +1,81 @@
+defmodule LSGWeb.ContextPlug do
+ import Plug.Conn
+ import Phoenix.Controller
+
+ def init(opts \\ []) do
+ opts || []
+ end
+
+ def call(conn, opts) do
+ account = with \
+ {:account, account_id} when is_binary(account_id) <- {:account, get_session(conn, :account)},
+ {:account, account} when not is_nil(account) <- {:account, IRC.Account.get(account_id)}
+ do
+ account
+ else
+ _ -> nil
+ end
+
+ network = Map.get(conn.params, "network")
+ network = if network == "-", do: nil, else: network
+
+ conns = IRC.Connection.get_network(network)
+ chan = if c = Map.get(conn.params, "chan") do
+ LSGWeb.reformat_chan(c)
+ end
+ chan_conn = IRC.Connection.get_network(network, chan)
+
+ memberships = if account do
+ IRC.Membership.of_account(account)
+ end
+
+ auth_required = cond do
+ Keyword.get(opts, :restrict) == :public -> false
+ account == nil -> true
+ network == nil -> false
+ Keyword.get(opts, :restrict) == :logged_in -> false
+ network && chan ->
+ !Enum.member?(memberships, {network, chan})
+ network ->
+ !Enum.any?(memberships, fn({n, _}) -> n == network end)
+ end
+
+ bot = cond do
+ network && chan && chan_conn -> chan_conn.nick
+ network && conns -> conns.nick
+ true -> nil
+ end
+
+
+ cond do
+ account && auth_required ->
+ conn
+ |> put_status(404)
+ |> text("Page not found")
+ |> halt()
+ auth_required ->
+ conn
+ |> put_status(403)
+ |> render(LSGWeb.AlcoologView, "auth.html", bot: bot, no_header: true, network: network)
+ |> halt()
+ (network && !conns) ->
+ conn
+ |> put_status(404)
+ |> text("Page not found")
+ |> halt()
+ (chan && !chan_conn) ->
+ conn
+ |> put_status(404)
+ |> text("Page not found")
+ |> halt()
+ true ->
+ conn = conn
+ |> assign(:network, network)
+ |> assign(:chan, chan)
+ |> assign(:bot, bot)
+ |> assign(:account, account)
+ |> assign(:memberships, memberships)
+ end
+ end
+
+end
diff --git a/lib/lsg_web/controllers/alcoolog_controller.ex b/lib/lsg_web/controllers/alcoolog_controller.ex
index 9d5d9d9..b88faa3 100644
--- a/lib/lsg_web/controllers/alcoolog_controller.ex
+++ b/lib/lsg_web/controllers/alcoolog_controller.ex
@@ -2,9 +2,12 @@ defmodule LSGWeb.AlcoologController do
use LSGWeb, :controller
require Logger
- def index(conn, %{"channel" => channel}) do
- case LSG.Token.lookup(channel) do
- {:ok, obj} -> index(conn, obj)
+ plug LSGWeb.ContextPlug when action not in [:token]
+ plug LSGWeb.ContextPlug, [restrict: :public] when action in [:token]
+
+ def token(conn, %{"token" => token}) do
+ case LSG.Token.lookup(token) do
+ {:ok, {:alcoolog, :index, network, channel}} -> index(conn, nil, network, channel)
err ->
Logger.debug("AlcoologControler: token #{inspect err} invalid")
conn
@@ -13,8 +16,31 @@ defmodule LSGWeb.AlcoologController do
end
end
- def index(conn, {:alcoolog, :index, channel}) do
- aday = 7*((24 * 60)*60)
+ def index(conn = %{assigns: %{account: account}}, %{"network" => network, "chan" => channel}) do
+ index(conn, account, network, LSGWeb.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
+ # LSGWeb.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 = 18*((24 * 60)*60)
now = DateTime.utc_now()
before = now
|> DateTime.add(-aday, :second)
@@ -27,22 +53,28 @@ defmodule LSGWeb.AlcoologController do
], [:"$_"]}
]
- nicks_in_channel = IRC.UserTrack.channel(channel)
- |> Enum.map(fn({_, nick, _, _, _, _, _}) -> String.downcase(nick) end)
# 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(LSG.IRC.AlcoologPlugin.ETS, match)
- #|> Enum.filter(fn({{nick, _}, _, _, _, _}) -> Enum.member?(nicks_in_channel, nick) end)
- |> Enum.sort_by(fn({{_, ts}, _, _, _, _}) -> ts end, &>/2)
+ |> Enum.filter(fn({{account, _}, _, _, _, _}) -> 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 = LSG.IRC.AlcoologPlugin.get_channel_statistics(channel)
+ stats = LSG.IRC.AlcoologPlugin.get_channel_statistics(account, network, channel)
- top = Enum.reduce(drinks, %{}, fn({{nick, _}, vol, _, _, _}, acc) ->
+ 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}
- render(conn, "index.html", channel: channel, drinks: drinks, top: top, stats: stats)
+ #
+ conn
+ |> assign(:title, "alcoolog")
+ |> render("index.html", network: network, channel: channel, drinks: drinks, top: top, stats: stats)
end
end
diff --git a/lib/lsg_web/controllers/irc_auth_sse_controller.ex b/lib/lsg_web/controllers/irc_auth_sse_controller.ex
new file mode 100644
index 0000000..c39a866
--- /dev/null
+++ b/lib/lsg_web/controllers/irc_auth_sse_controller.ex
@@ -0,0 +1,66 @@
+defmodule LSGWeb.IrcAuthSseController do
+ use LSGWeb, :controller
+ require Logger
+
+ @ping_interval 20_000
+ @expire_delay :timer.minutes(3)
+
+ def sse(conn, params) do
+ perks = if uri = Map.get(params, "redirect_to") do
+ {:redirect, uri}
+ else
+ nil
+ end
+ token = String.downcase(EntropyString.random_string(65))
+ conn
+ |> assign(:token, token)
+ |> assign(:perks, perks)
+ |> put_resp_header("X-Accel-Buffering", "no")
+ |> put_resp_header("content-type", "text/event-stream")
+ |> send_chunked(200)
+ |> subscribe()
+ |> send_sse_message("token", token)
+ |> sse_loop
+ end
+
+ def subscribe(conn) do
+ :timer.send_interval(@ping_interval, {:event, :ping})
+ :timer.send_after(@expire_delay, {:event, :expire})
+ {:ok, _} = Registry.register(IRC.PubSub, "message:private", [])
+ conn
+ end
+
+ def sse_loop(conn) do
+ {type, event, exit} = receive do
+ {:event, :ping} -> {"ping", "ping", false}
+ {:event, :expire} -> {"expire", "expire", true}
+ {:irc, :text, %{account: account, text: token} = m} ->
+ if String.downcase(String.trim(token)) == conn.assigns.token do
+ path = LSG.AuthToken.new_path(account.id, conn.assigns.perks)
+ m.replyfun.("ok!")
+ {"authenticated", path, true}
+ else
+ {nil, nil, false}
+ end
+ _ -> {nil, nil, false}
+ end
+
+ conn = if type do
+ send_sse_message(conn, type, event)
+ else
+ conn
+ end
+
+ if exit do
+ conn
+ else
+ sse_loop(conn)
+ end
+ end
+
+ defp send_sse_message(conn, type, data) do
+ {:ok, conn} = chunk(conn, "event: #{type}\ndata: #{data}\n\n")
+ conn
+ end
+
+end
diff --git a/lib/lsg_web/controllers/irc_controller.ex b/lib/lsg_web/controllers/irc_controller.ex
index 022807d..e0bf24d 100644
--- a/lib/lsg_web/controllers/irc_controller.ex
+++ b/lib/lsg_web/controllers/irc_controller.ex
@@ -1,13 +1,21 @@
defmodule LSGWeb.IrcController do
use LSGWeb, :controller
- def index(conn, _) do
- commands = for mod <- (Application.get_env(:lsg, :irc)[:plugins] ++ Application.get_env(:lsg, :irc)[:handlers]) do
+ plug LSGWeb.ContextPlug
+
+ def index(conn, params) do
+ network = Map.get(params, "network")
+ channel = if c = Map.get(params, "channel"), do: LSGWeb.reformat_chan(c)
+ commands = for mod <- ([IRC.Account.AccountPlugin] ++ Application.get_env(:lsg, :irc)[:plugins] ++ Application.get_env(:lsg, :irc)[:handlers]) do
identifier = Module.split(mod) |> List.last |> String.replace("Plugin", "") |> Macro.underscore
{identifier, mod.irc_doc()}
end
|> Enum.reject(fn({_, i}) -> i == nil end)
- render conn, "index.html", commands: commands
+ members = cond do
+ network -> IRC.Membership.expanded_members_or_friends(conn.assigns.account, network, channel)
+ 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
@@ -33,13 +41,22 @@ defmodule LSGWeb.IrcController do
doc = LSG.IRC.TxtPlugin.irc_doc()
data = data()
lines = Enum.reduce(data, 0, fn({_, lines}, acc) -> acc + Enum.count(lines) end)
- render conn, "txts.html", data: data, doc: doc, files: Enum.count(data), lines: lines
+ conn
+ |> assign(:title, "txt")
+ |> render("txts.html", data: data, doc: doc, files: Enum.count(data), lines: lines)
end
defp do_txt(conn, txt) do
data = data()
+ base_url = cond do
+ conn.assigns[:chan] -> "/#{conn.assigns.network}/#{LSGWeb.format_chan(conn.assigns.chan)}"
+ true -> "/-"
+ end
if Map.has_key?(data, txt) do
- render(conn, "txt.html", name: txt, data: data[txt], doc: nil)
+ conn
+ |> assign(:breadcrumbs, [{"txt", "#{base_url}/txt"}])
+ |> assign(:title, "#{txt}.txt")
+ |> render("txt.html", name: txt, data: data[txt], doc: nil)
else
conn
|> put_status(404)
diff --git a/lib/lsg_web/controllers/network_controller.ex b/lib/lsg_web/controllers/network_controller.ex
new file mode 100644
index 0000000..537c2f6
--- /dev/null
+++ b/lib/lsg_web/controllers/network_controller.ex
@@ -0,0 +1,11 @@
+defmodule LSGWeb.NetworkController do
+ use LSGWeb, :controller
+ plug LSGWeb.ContextPlug
+
+ def index(conn, %{"network" => network}) do
+ conn
+ |> assign(:title, network)
+ |> render("index.html")
+ end
+
+end
diff --git a/lib/lsg_web/controllers/page_controller.ex b/lib/lsg_web/controllers/page_controller.ex
index b356b9c..a87cf1d 100644
--- a/lib/lsg_web/controllers/page_controller.ex
+++ b/lib/lsg_web/controllers/page_controller.ex
@@ -1,12 +1,35 @@
defmodule LSGWeb.PageController do
use LSGWeb, :controller
- def index(conn, _params) do
- render conn, "index.html"
+ plug LSGWeb.ContextPlug when action not in [:token]
+ plug LSGWeb.ContextPlug, [restrict: :public] when action in [:token]
+
+ def token(conn, %{"token" => token}) do
+ with \
+ {:ok, account, perks} <- LSG.AuthToken.lookup(token)
+ do
+ IO.puts("Authenticated account #{inspect account}")
+ conn = put_session(conn, :account, account)
+ case perks do
+ nil -> redirect(conn, to: "/")
+ {:redirect, path} -> redirect(conn, to: path)
+ {:external_redirect, url} -> redirect(conn, external: url)
+ end
+ else
+ z ->
+ IO.inspect(z)
+ text(conn, "Error: invalid or expired token")
+ end
end
- def api(conn, _params) do
- render conn, "api.html"
+ def index(conn = %{assigns: %{account: account}}, _) do
+ memberships = IRC.Membership.of_account(account)
+ users = IRC.UserTrack.find_by_account(account)
+ metas = IRC.Account.get_all_meta(account)
+ predicates = IRC.Account.get_predicates(account)
+ conn
+ |> assign(:title, account.name)
+ |> render("user.html", users: users, memberships: memberships, metas: metas, predicates: predicates)
end
def irc(conn, _) do
@@ -16,16 +39,15 @@ defmodule LSGWeb.PageController do
render conn, "irc.html", bot_helps: bot_helps
end
- def icecast(conn, _params) do
- conn
- |> json(LSG.IcecastAgent.get)
- end
-
- def widget(conn, options) do
- icecast = LSG.IcecastAgent.get
- conn
- |> put_layout(false)
- |> render("widget.html", icecast: icecast)
+ def authenticate(conn, _) do
+ with \
+ {:account, account_id} when is_binary(account_id) <- {:account, get_session(conn, :account)},
+ {:account, account} when not is_nil(account) <- {:account, IRC.Account.get(account_id)}
+ do
+ assign(conn, :account, account)
+ else
+ _ -> conn
+ end
end
end
diff --git a/lib/lsg_web/controllers/sms_controller.ex b/lib/lsg_web/controllers/sms_controller.ex
new file mode 100644
index 0000000..00c6352
--- /dev/null
+++ b/lib/lsg_web/controllers/sms_controller.ex
@@ -0,0 +1,10 @@
+defmodule LSGWeb.SmsController do
+ use LSGWeb, :controller
+ require Logger
+
+ def ovh_callback(conn, %{"senderid" => from, "message" => message}) do
+ spawn(fn() -> LSG.IRC.SmsPlugin.incoming(from, String.trim(message)) end)
+ text(conn, "")
+ end
+
+end
diff --git a/lib/lsg_web/controllers/untappd_controller.ex b/lib/lsg_web/controllers/untappd_controller.ex
new file mode 100644
index 0000000..1c3ceb1
--- /dev/null
+++ b/lib/lsg_web/controllers/untappd_controller.ex
@@ -0,0 +1,18 @@
+defmodule LSGWeb.UntappdController do
+ use LSGWeb, :controller
+
+ def callback(conn, %{"code" => code}) do
+ with \
+ {:account, account_id} when is_binary(account_id) <- {:account, get_session(conn, :account)},
+ {:account, account} when not is_nil(account) <- {:account, IRC.Account.get(account_id)},
+ {:ok, auth_token} <- Untappd.auth_callback(code)
+ do
+ IRC.Account.put_meta(account, "untappd-token", auth_token)
+ text(conn, "OK!")
+ else
+ {:account, _} -> text(conn, "Error: account not found")
+ :error -> text(conn, "Error: untappd authentication failed")
+ end
+ end
+
+end
diff --git a/lib/lsg_web/endpoint.ex b/lib/lsg_web/endpoint.ex
index 2d0a6be..e89dc12 100644
--- a/lib/lsg_web/endpoint.ex
+++ b/lib/lsg_web/endpoint.ex
@@ -1,7 +1,7 @@
defmodule LSGWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :lsg
- socket "/socket", LSGWeb.UserSocket
+ socket "/socket", LSGWeb.UserSocket, websocket: true
# Serve at "/" the static files from "priv/static" directory.
#
diff --git a/lib/lsg_web/router.ex b/lib/lsg_web/router.ex
index a834083..de7fafb 100644
--- a/lib/lsg_web/router.ex
+++ b/lib/lsg_web/router.ex
@@ -3,8 +3,8 @@ defmodule LSGWeb.Router do
pipeline :browser do
plug :accepts, ["html", "txt"]
- #plug :fetch_session
- #plug :fetch_flash
+ plug :fetch_session
+ plug :fetch_flash
#plug :protect_from_forgery
#plug :put_secure_browser_headers
end
@@ -13,22 +13,33 @@ defmodule LSGWeb.Router do
plug :accepts, ["json", "sse"]
end
+ scope "/api", LSGWeb do
+ pipe_through :api
+ get "/irc-auth.sse", IrcAuthSseController, :sse
+ post "/sms/callback/Ovh", SmsController, :ovh_callback, as: :sms
+ end
+
scope "/", LSGWeb do
pipe_through :browser
get "/", PageController, :index
get "/embed/widget", PageController, :widget
get "/api", PageController, :api
- get "/irc", IrcController, :index
- get "/irc/txt", IrcController, :txt
- get "/irc/txt/:name", IrcController, :txt
- get "/irc/alcoolog/:channel", AlcoologController, :index
- get "/irc/alcoolog/~/:nick", AlcoologController, :nick
+ get "/login/irc/:token", PageController, :token, as: :login
+ get "/api/untappd/callback", UntappdController, :callback, as: :untappd_callback
+ get "/-", IrcController, :index
+ get "/-/txt", IrcController, :txt
+ get "/-/txt/:name", IrcController, :txt
+ get "/-/alcoolog", AlcoologController, :index
+ get "/-/alcoolog/~/:account_name", AlcoologController, :index
+ get "/:network", NetworkController, :index
+ get "/:network/:chan", IrcController, :index
+ get "/:network/:chan/txt", IrcController, :txt
+ get "/:network/:chan/txt/:name", IrcController, :txt
+ get "/:network/:channel/preums", IrcController, :preums
+ get "/:network/:chan/alcoolog", AlcoologController, :index
+ get "/:network/:chan/alcoolog/t/:token", AlcoologController, :token
+ get "/:network/alcoolog/~/:nick", AlcoologController, :nick
end
- scope "/api", LSGWeb do
- pipe_through :api
- get "/icecast.json", PageController, :icecast
- get "/icecast.sse", IcecastSseController, :sse
- end
end
diff --git a/lib/lsg_web/templates/alcoolog/auth.html.eex b/lib/lsg_web/templates/alcoolog/auth.html.eex
new file mode 100644
index 0000000..af6db53
--- /dev/null
+++ b/lib/lsg_web/templates/alcoolog/auth.html.eex
@@ -0,0 +1,39 @@
+<h1>authentication</h1>
+
+<div id="authenticator">
+ <p>
+ <%= if @bot, do: "Send this to #{@bot} on #{@network}:", else: "Find your bot nickname and send:" %><br /><br />
+ <strong>/msg <%= @bot || "the-bot-nickname" %> web</strong>
+ <br /><br />
+ ... then come back to this address.
+ </p>
+</div>
+
+<script type="text/javascript">
+ var authSse = new EventSource("/api/irc-auth.sse?redirect_to=" + window.location.pathname);
+ authSse.addEventListener("token", function(event) {
+ html = "<%= if @bot, do: "Send this to #{@bot} on #{@network}:", else: "Find your bot nickname and send:" %><br /><br /><strong>/msg <%= @bot || "the-bot-nickname" %> "+event.data+"</strong>";
+ elem = document.getElementById("authenticator");
+ elem.innerHTML = html;
+ });
+ authSse.addEventListener("authenticated", function(event) {
+ elem = document.getElementById("authenticator");
+ elem.innerHTML = "<strong>success, redirecting...</strong>";
+ window.keepInner = true;
+ window.location.pathname = event.data;
+ });
+ authSse.addEventListener("expire", function(event) {
+ elem = document.getElementById("authenticator");
+ elem.innerHTML = "<strong>authentication expired</strong>";
+ window.keepInner = true;
+ });
+ authSse.onerror = function() {
+ authSse.close();
+ if (!window.keepInner) {
+ elem = document.getElementById("authenticator");
+ elem.innerHTML = "<strong>server closed connection :(</strong>";
+ }
+ };
+
+</script>
+
diff --git a/lib/lsg_web/templates/alcoolog/index.html.eex b/lib/lsg_web/templates/alcoolog/index.html.eex
index 507be71..e656e64 100644
--- a/lib/lsg_web/templates/alcoolog/index.html.eex
+++ b/lib/lsg_web/templates/alcoolog/index.html.eex
@@ -1,16 +1,12 @@
<style type="text/css">
-h1 small {
- font-size: 14px;
-}
ol li {
margin-bottom: 5px
}
</style>
-<h1>
- <small><a href="/irc"><%= Keyword.get(Application.get_env(:lsg, :irc), :name, "ircbot") %></a> &rsaquo; </small><br/>
- alcoolog <%= @channel %>
-</h1>
+<%= if @stats == [] do %>
+ </strong><i>:o personne ne boit</i></strong>
+<% end %>
<ul>
<%= for {nick, status} <- @stats do %>
@@ -28,28 +24,35 @@ ol li {
<% end %>
</ul>
-<p>
- top consommateur par volume, les 7 derniers jours: <%= Enum.intersperse(for({nick, count} <- @top, do: "#{nick}: #{Float.round(count,4)}"), ", ") %>
-</p>
+<%= if @stats == %{} do %>
+ <strong><i>... et personne n'a bu :o :o :o</i></strong>
+<% else %>
+ <p>
+ top consommateur par volume, les 7 derniers jours: <%= Enum.intersperse(for({nick, count} <- @top, do: "#{nick}: #{Float.round(count,4)}"), ", ") %>
+ </p>
-<table class="table">
- <thead>
- <tr>
- <th scope="col">date</th>
- <th scope="col">nick</th>
- <th scope="col">points</th>
- <th scope="col">nom</th>
- </tr>
- </thead>
- <tbody>
- <%= for {{nick, date}, points, _, nom, comment} <- @drinks do %>
- <% date = DateTime.from_unix!(date, :millisecond) %>
- <th scope="row"><%= LSGWeb.LayoutView.format_time(date, false) %></th>
- <td><%= nick %></td>
- <td><%= Float.round(points+0.0, 5) %></td>
- <td><%= nom||"" %> <%= comment||"" %></td>
+ <table class="table">
+ <thead>
+ <tr>
+ <th scope="col">date</th>
+ <th scope="col">nick</th>
+ <th scope="col">points</th>
+ <th scope="col">nom</th>
</tr>
- <% end %>
- </tbody>
-</table>
+ </thead>
+ <tbody>
+ <%= for {{{account, date}, points, _, nom, comment}, nick} <- @drinks do %>
+ <% date = DateTime.from_unix!(date, :millisecond) %>
+ <th scope="row"><%= LSGWeb.LayoutView.format_time(date, false) %></th>
+ <td><%= nick %></td>
+ <td><%= Float.round(points+0.0, 5) %></td>
+ <td><%= nom||"" %> <%= comment||"" %></td>
+ </tr>
+ <% end %>
+ </tbody>
+ </table>
+<% end %>
+<%= if @conn.assigns.account && (@network || @channel) do %>
+ <%= link("alcoolog global", to: alcoolog_path(@conn, :index)) %>
+<% end %>
diff --git a/lib/lsg_web/templates/irc/index.html.eex b/lib/lsg_web/templates/irc/index.html.eex
index f127101..9e4f724 100644
--- a/lib/lsg_web/templates/irc/index.html.eex
+++ b/lib/lsg_web/templates/irc/index.html.eex
@@ -1,23 +1,4 @@
<style type="text/css">
-h1 small {
- font-size: 14px;
-}
-</style>
-
-<h1><%= Keyword.get(Application.get_env(:lsg, :irc), :name, "ircbot") %></h1>
-
-<!--<p>
-Serveur IRC: <a href="irc://irc.quakenet.org/lsg">irc.quakenet.org #lsg</a>.
-&nbsp;Matrix: <a href="https://matrix.random.sh/riot/#rooms/#lsg:random.sh">#lsg:random.sh</a>.
-<br />
-<a href="https://115ans.net/irc/">Webchat</a>.
-<br />
-<a href="/irc/stats/">Statistiques</a>.
-</del>
-</p>-->
-
-
-<style type="text/css">
.help-entry h1 {
font-size: 18px;
}
@@ -29,15 +10,22 @@ Serveur IRC: <a href="irc://irc.quakenet.org/lsg">irc.quakenet.org #lsg</a>.
<p>
<% list = for {identifier, _} <- @commands do %>
<% name = String.replace(identifier, "_", " ") %>
- <%= link(name, to: "##{identifier}") #raw("<a href='##{identifier}'>#{name}</a>") %>
+ <%= link(name, to: "##{identifier}") %>
<% end %>
<small><%= Enum.intersperse(list, " - ") %></small>
</p>
+<%= if @members != [] do %>
+<% users = for {_acc, user, nick} <- @members, do: if(user, do: nick, else: content_tag(:i, nick)) %>
+<%= for u <- Enum.intersperse(users, ", ") do %>
+ <%= u %>
+<% end %>
+<% end %>
+
<div class="irchelps">
<%= for {identifier, help} <- @commands do %>
<%= if help do %>
- <div class="help-entry" id="<%= identifier %>"><%= help |> Earmark.as_html! |> raw() %></div>
+ <div class="help-entry" id="<%= identifier %>"><%= LSGWeb.LayoutView.liquid_markdown(@conn, help) %></div>
<% end %>
<% end %>
</div>
diff --git a/lib/lsg_web/templates/irc/txt.html.eex b/lib/lsg_web/templates/irc/txt.html.eex
index 5b31320..e9df681 100644
--- a/lib/lsg_web/templates/irc/txt.html.eex
+++ b/lib/lsg_web/templates/irc/txt.html.eex
@@ -1,16 +1,9 @@
<style type="text/css">
-h1 small {
- font-size: 14px;
-}
ol li {
margin-bottom: 5px
}
</style>
-<h1>
- <small><a href="/irc"><%= Keyword.get(Application.get_env(:lsg, :irc), :name, "ircbot") %></a> &rsaquo; <a href="/irc/txt">txt</a> &rsaquo;</small><br/>
- <%= @name %>.txt</h1>
-
<ol>
<%= for {txt, id} <- Enum.with_index(@data) do %>
<li id="<%= @name %>-<%= id %>"><%= txt %></li>
diff --git a/lib/lsg_web/templates/irc/txts.html.eex b/lib/lsg_web/templates/irc/txts.html.eex
index 28d0220..5971ffa 100644
--- a/lib/lsg_web/templates/irc/txts.html.eex
+++ b/lib/lsg_web/templates/irc/txts.html.eex
@@ -5,13 +5,7 @@
.help-entry h2 {
font-size: 16px;
}
-h1 small {
- font-size: 14px;
-}
</style>
-<h1>
- <small><a href="/irc"><%= Keyword.get(Application.get_env(:lsg, :irc), :name, "ircbot") %></a> &rsaquo; </small><br/>
- txt</h1>
<p><strong><%= @lines %></strong> lignes dans <strong><%= @files %></strong> fichiers.
<a href="#help">Aide</a>.
@@ -19,9 +13,13 @@ h1 small {
<ul>
<%= for {txt, data} <- @data do %>
- <li><a href="/irc/txt/<%= txt %>"><%= txt %></a> <i>(<%= Enum.count(data) %>)</i></li>
+ <% base_url = cond do
+ @conn.assigns[:chan] -> "/#{@conn.assigns.network}/#{LSGWeb.format_chan(@conn.assigns.chan)}"
+ true -> "/-"
+ end %>
+ <li><a href="<%= base_url %>/txt/<%= txt %>"><%= txt %></a> <i>(<%= Enum.count(data) %>)</i></li>
<% end %>
</ul>
-<div class="help-entry" id="help"><%= @doc |> Earmark.as_html! |> raw() %></div>
+<div class="help-entry" id="help"><%= LSGWeb.LayoutView.liquid_markdown(@conn, @doc) %></div>
diff --git a/lib/lsg_web/templates/layout/app.html.eex b/lib/lsg_web/templates/layout/app.html.eex
index 4c2ad44..1871e8b 100644
--- a/lib/lsg_web/templates/layout/app.html.eex
+++ b/lib/lsg_web/templates/layout/app.html.eex
@@ -1,16 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
+ <%= page_title(@conn) %>
<meta charset="utf-8">
<meta name="robots" content="noindex, nofollow, nosnippet">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="robots" content="noindex, noarchive, nofollow, nosnippet" />
+ <title><%= Map.get(assigns, :title, "") %></title>
<link rel="stylesheet" href="<%= static_path(@conn, "/assets/css/app.css") %>">
</head>
<body>
<div class="container">
<main role="main">
+ <%= if !@conn.assigns[:no_header] do %>
+ <h1>
+ <small style="font-size: 14px;">
+ <a href="/"><%= Keyword.get(Application.get_env(:lsg, :irc), :name, "ircbot") %></a>
+ <%= if @conn.assigns[:account], do: @conn.assigns.account.name %>
+ &rsaquo;
+ <%= if n = @conn.assigns[:network] do %><a href="/<%= n %>"><%= n %></a> &rsaquo; <% end %>
+ <%= if c = @conn.assigns[:chan] do %><a href="/<%= @conn.assigns.network %>/<%= LSGWeb.format_chan(c) %>"><%= c %></a> &rsaquo; <% end %>
+ <%= for({name, href} <- @conn.assigns[:breadcrumbs]||[], do: [link(name, to: href), raw(" &rsaquo; ")]) %>
+ </small><br />
+ <%= @conn.assigns[:title] || @conn.assigns[:chan] || @conn.assigns[:network] %>
+ </h1>
+ <% end %>
<%= render @view_module, @view_template, assigns %>
</main>
</div>
diff --git a/lib/lsg_web/templates/network/index.html.eex b/lib/lsg_web/templates/network/index.html.eex
new file mode 100644
index 0000000..fc024dd
--- /dev/null
+++ b/lib/lsg_web/templates/network/index.html.eex
@@ -0,0 +1 @@
+pouet
diff --git a/lib/lsg_web/templates/page/user.html.eex b/lib/lsg_web/templates/page/user.html.eex
new file mode 100644
index 0000000..8a043a0
--- /dev/null
+++ b/lib/lsg_web/templates/page/user.html.eex
@@ -0,0 +1,38 @@
+<ul>
+ <li><%= link("Help", to: "/-") %></li>
+</ul>
+
+<h2>channels</h2>
+<ul>
+ <%= for {net, channel} <- @memberships do %>
+ <li>
+ <% url = LSGWeb.Router.Helpers.irc_path(LSGWeb.Endpoint, :index, net, LSGWeb.format_chan(channel)) %>
+ <%= link([net, ": ", content_tag(:strong, channel)], to: url) %>
+ </li>
+ <% end %>
+</ul>
+
+<h2>connections</h2>
+<ul>
+ <%= for user <- @users do %>
+ <li>
+ <strong><%= user.network %></strong>: <strong><%= user.nick %></strong>!<%= user.username %>@<%= user.host %> <i><%= user.realname %></i><br />
+ <%= Enum.join(Enum.intersperse(Enum.map(user.privileges, fn({c, _}) -> c end), ", ")) %>
+ </li>
+ <% end %>
+</ul>
+
+<h2>account</h2>
+<ul>
+ <li>account-id: <%= @conn.assigns.account.id %></li>
+ <%= for {k, v} <- @metas do %>
+ <li><%= k %>: <%= to_string(v) %></li>
+ <% end %>
+</ul>
+
+<strong>irc auths:</strong>
+<ul>
+ <%= for {net, {predicate, v}} <- @predicates do %>
+ <li><%= net %>: <%= to_string(predicate) %>, <%= v %></li>
+ <% end %>
+</ul>
diff --git a/lib/lsg_web/views/layout_view.ex b/lib/lsg_web/views/layout_view.ex
index 956d703..b28d3c5 100644
--- a/lib/lsg_web/views/layout_view.ex
+++ b/lib/lsg_web/views/layout_view.ex
@@ -1,20 +1,78 @@
defmodule LSGWeb.LayoutView do
use LSGWeb, :view
-
+
+ def liquid_markdown(conn, text) do
+ context_path = cond do
+ conn.assigns[:chan] -> "/#{conn.assigns[:network]}/#{LSGWeb.format_chan(conn.assigns[:chan])}"
+ conn.assigns[:network] -> "/#{conn.assigns[:network]}/-"
+ true -> "/-"
+ end
+
+ {:ok, ast} = Liquex.parse(text)
+ context = Liquex.Context.new(%{
+ "context_path" => context_path
+ })
+ {content, _} = Liquex.render(ast, context)
+ content
+ |> to_string()
+ |> Earmark.as_html!()
+ |> raw()
+ end
+
+ def page_title(conn) do
+ target = cond do
+ conn.assigns[:chan] ->
+ "#{conn.assigns.chan} @ #{conn.assigns.network}"
+ conn.assigns[:network] -> conn.assigns.network
+ true -> Keyword.get(Application.get_env(:lsg, :irc), :name, "ircbot")
+ end
+
+ breadcrumb_title = Enum.map(Map.get(conn.assigns, :breadcrumbs)||[], fn({title, _href}) -> title end)
+
+ title = [conn.assigns[:title], breadcrumb_title, target]
+ |> List.flatten()
+ |> Enum.uniq()
+ |> Enum.filter(fn(x) -> x end)
+ |> Enum.intersperse(" / ")
+ |> Enum.join()
+
+ content_tag(:title, title)
+ end
+
def format_time(date, with_relative \\ true) do
alias Timex.Format.DateTime.Formatters
alias Timex.Timezone
date = if is_integer(date) do
date
|> DateTime.from_unix!(:millisecond)
- |> Timezone.convert("Europe/Paris")
+ |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
else
date
+ |> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
+ end
+
+ now = DateTime.now!("Europe/Paris", Tzdata.TimeZoneDatabase)
+
+ now_week = Timex.iso_week(now)
+ date_week = Timex.iso_week(date)
+
+ {y, w} = now_week
+ now_last_week = {y, w-1}
+ now_last_roll = 7-Timex.days_to_beginning_of_week(now)
+
+ format = cond do
+ date.year != now.year -> "{D}/{M}/{YYYY} {h24}:{m}"
+ date.day == now.day -> "{h24}:{m}"
+ (now_week == date_week) || (date_week == now_last_week && (Date.day_of_week(date) >= now_last_roll)) -> "{WDfull} {h24}:{m}"
+ (now.month == date.month) -> "{WDfull} {D} {h24}:{m}"
+ true -> "{WDfull} {D} {M} {h24}:{m}"
end
+
{:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
- {:ok, detail} = Formatters.Default.lformat(date, "{D}/{M}/{YYYY} {h24}:{m}", "fr")
+ {:ok, full} = Formatters.Default.lformat(date, "{WDfull} {D} {YYYY} {h24}:{m}", "fr") #"{h24}:{m} {WDfull} {D}", "fr")
+ {:ok, detail} = Formatters.Default.lformat(date, format, "fr") #"{h24}:{m} {WDfull} {D}", "fr")
- content_tag(:time, if(with_relative, do: relative, else: detail), [title: detail])
+ content_tag(:time, if(with_relative, do: relative, else: detail), [title: full])
end
end
diff --git a/lib/lsg_web/views/network_view.ex b/lib/lsg_web/views/network_view.ex
new file mode 100644
index 0000000..c369ce6
--- /dev/null
+++ b/lib/lsg_web/views/network_view.ex
@@ -0,0 +1,4 @@
+defmodule LSGWeb.NetworkView do
+ use LSGWeb, :view
+
+end
diff --git a/lib/tmpl.ex b/lib/tmpl.ex
new file mode 100644
index 0000000..cc5ece2
--- /dev/null
+++ b/lib/tmpl.ex
@@ -0,0 +1,123 @@
+defmodule Tmpl do
+ require Logger
+
+ defmodule Filter do
+ use Liquex.Filter
+
+ def repeat(text, val, _) do
+ String.duplicate(text, val)
+ end
+
+ def rrepeat(text, max, _) do
+ String.duplicate(text, :random.uniform(max))
+ end
+
+ def rrepeat(text, var) do
+ rrepeat(text, 20, var)
+ end
+
+ def bold(text, %{variables: variables}) do
+ unless Map.get(variables, "_no_format") || Map.get(variables, "_no_bold") do
+ <<2>> <> text <> <<2>>
+ else
+ text
+ end
+ end
+
+ @colors [:white, :black, :blue, :green, :red, :brown, :purple, :orange, :yellow, :light_green, :cyan, :light_blue, :pink, :grey, :light_grey]
+
+ for {color, index} <- Enum.with_index(@colors) do
+ code = 48+index
+
+ def color_code(unquote(color)) do
+ unquote(code)
+ end
+
+ def unquote(color)(text, %{variables: variables}) do
+ unless Map.get(variables, "_no_format") || Map.get(variables, "_no_colors") do
+ <<3, unquote(code)>> <> text <> <<3>>
+ else
+ text
+ end
+ end
+ end
+
+ def account_nick(%{"id" => id, "name" => name}, %{variables: %{"message" => %{"network" => network}}}) do
+ if user = IRC.UserTrack.find_by_account(network, %IRC.Account{id: id}) do
+ user.nick
+ else
+ name
+ end
+ end
+
+ def account_nick(val, ctx) do
+ "{{account_nick}}"
+ end
+
+ end
+
+ def render(template, msg = %IRC.Message{}, context \\ %{}, safe \\ true) do
+ do_render(template, Map.put(context, "message", msg), safe)
+ end
+
+ defp do_render(template, context, safe) when is_binary(template) do
+ case Liquex.parse(template) do
+ {:ok, template_ast} ->
+ do_render(template_ast, context, safe)
+ {:error, err, pos} ->
+ "[liquid ast error (at #{pos}): #{inspect err}]"
+ end
+ end
+
+ defp do_render(template_ast, context, safe) when is_list(template_ast) do
+ context = Liquex.Context.new(mapify(context, safe))
+ |> Map.put(:filter_module, Tmpl.Filter)
+ {content, _context} = Liquex.render(template_ast, context)
+ to_string(content)
+ rescue
+ e ->
+ Logger.error("Liquid error: #{inspect e}")
+ "[liquid rendering error]"
+ end
+
+ defp mapify(struct = %{__struct__: _}, safe) do
+ mapify(Map.from_struct(struct), safe)
+ end
+
+ defp mapify(map = %{}, safe) do
+ map
+ |> Enum.reduce(Map.new, fn({k,v}, acc) ->
+ k = to_string(k)
+ if safe?(k, safe) do
+ if v = mapify(v, safe) do
+ Map.put(acc, k, v)
+ else
+ acc
+ end
+ else
+ acc
+ end
+ end)
+ end
+
+ defp mapify(fun, _) when is_function(fun) do
+ nil
+ end
+
+ defp mapify(atom, _) when is_atom(atom) do
+ to_string(atom)
+ end
+
+ defp mapify(v, _) do
+ v
+ end
+
+ defp safe?(_, false) do
+ true
+ end
+
+ defp safe?("token", true), do: false
+ defp safe?("password", true), do: false
+ defp safe?(_, true), do: true
+end
+
diff --git a/lib/untappd.ex b/lib/untappd.ex
new file mode 100644
index 0000000..1f78376
--- /dev/null
+++ b/lib/untappd.ex
@@ -0,0 +1,94 @@
+defmodule Untappd do
+
+ @env Mix.env
+ @version Mix.Project.config[:version]
+ require Logger
+
+ def auth_url() do
+ client_id = Keyword.get(env(), :client_id)
+ url = LSGWeb.Router.Helpers.untappd_callback_url(LSGWeb.Endpoint, :callback)
+ "https://untappd.com/oauth/authenticate/?client_id=#{client_id}&response_type=code&redirect_url=#{URI.encode(url)}"
+ end
+
+ def auth_callback(code) do
+ client_id = Keyword.get(env(), :client_id)
+ client_secret = Keyword.get(env(), :client_secret)
+ url = LSGWeb.Router.Helpers.untappd_callback_url(LSGWeb.Endpoint, :callback)
+ params = %{
+ "client_id" => client_id,
+ "client_secret" => client_secret,
+ "response_type" => code,
+ "redirect_url" => url,
+ "code" => code
+ }
+ case HTTPoison.get("https://untappd.com/oauth/authorize", headers(), params: params) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ json = Poison.decode!(body)
+ {:ok, get_in(json, ["response", "access_token"])}
+ error ->
+ Logger.error("Untappd auth callback failed: #{inspect error}")
+ :error
+ end
+ end
+
+ def maybe_checkin(account, beer_id) do
+ if token = IRC.Account.get_meta(account, "untappd-token") do
+ checkin(token, beer_id)
+ else
+ {:error, :no_token}
+ end
+ end
+
+ def checkin(token, beer_id) do
+ params = get_params(token: token)
+ |> Map.put("timezone", "CEST")
+ |> Map.put("bid", beer_id)
+ form_params = params
+ |> Enum.into([])
+ case HTTPoison.post("https://api.untappd.com/v4/checkin/add", {:form, form_params}, headers(), params: params) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ body = Jason.decode!(body)
+ |> Map.get("response")
+ {:ok, body}
+ {:ok, resp = %HTTPoison.Response{status_code: code, body: body}} ->
+ Logger.warn "Untappd checkin error: #{inspect resp}"
+ {:error, {:http_error, code}}
+ {:error, error} -> {:error, {:http_error, error}}
+ end
+ end
+
+ def search_beer(query, params \\ []) do
+ params = get_params(params)
+ |> Map.put("q", query)
+ |> Map.put("limit", 10)
+ #|> Map.put("sort", "name")
+ case HTTPoison.get("https://api.untappd.com/v4/search/beer", headers(), params: params) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ {:ok, Jason.decode!(body)}
+ error ->
+ Logger.error("Untappd search error: #{inspect error}")
+ end
+ end
+
+ def get_params(params) do
+ auth = %{"client_id" => Keyword.get(env(), :client_id), "client_secret" => Keyword.get(env(), :client_secret)}
+ if token = Keyword.get(params, :token) do
+ Map.put(auth, "access_token", token)
+ else
+ auth
+ end
+ end
+
+ def headers(extra \\ []) do
+ client_id = Keyword.get(env(), :client_id)
+ extra
+ ++ [
+ {"user-agent", "dmzbot (#{client_id}; #{@version}-#{@env})"}
+ ]
+ end
+
+ def env() do
+ Application.get_env(:lsg, :untappd)
+ end
+
+end
diff --git a/lib/util.ex b/lib/util.ex
index f515936..22d1034 100644
--- a/lib/util.ex
+++ b/lib/util.ex
@@ -10,4 +10,38 @@ defmodule Util do
|> Float.parse()
end
+ def ets_mutate_select_each(ets, table, spec, fun) do
+ ets.safe_fixtable(table, true)
+ first = ets.select(table, spec, 1)
+ do_ets_mutate_select_each(ets, table, fun, first)
+ after
+ ets.safe_fixtable(table, false)
+ end
+
+ defp do_ets_mutate_select_each(_, _, _, :'$end_of_table') do
+ :ok
+ end
+
+ defp do_ets_mutate_select_each(ets, table, fun, {objs, continuation}) do
+ for obj <- objs, do: fun.(table, obj)
+ do_ets_mutate_select_each(ets, table, fun, ets.select(continuation))
+ end
+
+
+ def ets_mutate_each(ets, table, fun) do
+ ets.safe_fixtable(table, true)
+ first = ets.first(table)
+ do_ets_mutate_each(ets, table, fun, first)
+ after
+ ets.safe_fixtable(table, false)
+ end
+
+ defp do_ets_mutate_each(ets, table, fun, key) do
+ case ets.lookup(table, key) do
+ [elem] -> fun.(table, elem)
+ _ -> nil
+ end
+ do_ets_mutate_each(ets, table, fun, ets.next(table, key))
+ end
+
end