Page MenuHomePhabricator

No OneTemporary

diff --git a/lib/irc/connection.ex b/lib/irc/connection.ex
index 49f5774..9bb09ed 100644
--- a/lib/irc/connection.ex
+++ b/lib/irc/connection.ex
@@ -1,521 +1,521 @@
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(Nola.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)
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 update_connection(connection) do
:dets.insert(dets(), to_tuple(connection))
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
Logger.metadata(conn: conn.id)
backoff = :backoff.init(@min_backoff, @max_backoff)
|> :backoff.type(:jitter)
{:ok, %{client: nil, backoff: backoff, conn: conn, connected_server: nil, connected_port: nil, network: conn.network}, {:continue, :connect}}
end
@triggers %{
"!" => :bang,
"+" => :plus,
"-" => :minus,
"?" => :query,
"." => :dot,
"~" => :tilde,
"@" => :at,
"++" => :plus_plus,
"--" => :minus_minus,
"!!" => :bang_bang,
"??" => :query_query,
".." => :dot_dot,
"~~" => :tilde_tilde,
"@@" => :at_at
}
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)
ExIRC.Client.add_handler(client, self())
client
end
opts = [{:nodelay, true}]
conn_fun = if state.conn.tls, do: :connect_ssl!, else: :connect!
apply(ExIRC.Client, conn_fun, [client, to_charlist(state.conn.host), state.conn.port, opts])
{: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, {channel, nil}, line)
{:noreply, state}
end
# Connection successful
def handle_info({:connected, server, port}, state) do
Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect state}")
{_, backoff} = :backoff.succeed(state.backoff)
ExIRC.Client.logon(state.client, state.conn.pass || "", state.conn.nick, state.conn.user, state.conn.name)
{:noreply, %{state | backoff: backoff, connected_server: server, connected_port: port}}
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
# ISUP
def handle_info({:isup, network}, state) when is_binary(network) do
- IRC.UserTrack.clear_network(state.network)
+ Nola.UserTrack.clear_network(state.network)
if network != state.network do
Logger.warn("Possibly misconfigured network: #{network} != #{state.network}")
end
{:noreply, state}
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
- user = if user = IRC.UserTrack.find_by_nick(state.network, sender.nick) do
+ user = if user = Nola.UserTrack.find_by_nick(state.network, sender.nick) do
user
else
Logger.error("Could not lookup user for message: #{inspect {state.network, chan, sender.nick}}")
- user = IRC.UserTrack.joined(chan, sender, [])
+ user = Nola.UserTrack.joined(chan, sender, [])
ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user..
user
end
if !user do
ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user..
Logger.error("Could not lookup user nor create it for message: #{inspect {state.network, chan, sender.nick}}")
else
if !Map.get(user.options, :puppet) do
reply_fun = fn(text) -> irc_reply(state, {chan, sender}, text) end
account = Nola.Account.lookup(sender)
message = %IRC.Message{id: FlakeId.get(), transport: :irc, at: NaiveDateTime.utc_now(), text: text, network: state.network,
account: account, sender: sender, channel: chan, replyfun: reply_fun,
trigger: extract_trigger(text)}
- message = case IRC.UserTrack.messaged(message) do
+ message = case Nola.UserTrack.messaged(message) do
:ok -> message
{:ok, message} -> message
end
publish(message, ["#{message.network}/#{chan}:messages"])
end
end
{:noreply, state}
end
# Received a private message
def handle_info({:received, text, sender}, state) do
reply_fun = fn(text) -> irc_reply(state, {sender.nick, sender}, text) end
account = Nola.Account.lookup(sender)
message = %IRC.Message{id: FlakeId.get(), transport: :irc, text: text, network: state.network, at: NaiveDateTime.utc_now(),
account: account, sender: sender, replyfun: reply_fun, trigger: extract_trigger(text)}
- message = case IRC.UserTrack.messaged(message) do
+ message = case Nola.UserTrack.messaged(message) do
:ok -> message
{:ok, message} -> message
end
publish(message, ["messages:private", "#{message.network}/#{account.id}:messages"])
{:noreply, state}
end
## -- Broadcast
def handle_info({:broadcast, net, account = %Nola.Account{}, message}, state) do
if net == state.conn.network do
- user = IRC.UserTrack.find_by_account(net, account)
+ user = Nola.UserTrack.find_by_account(net, account)
if user do
irc_reply(state, {user.nick, nil}, message)
end
end
{:noreply, state}
end
def handle_info({:broadcast, net, chan, message}, state) do
if net == state.conn.network && Enum.member?(state.conn.channels, chan) do
irc_reply(state, {chan, nil}, message)
end
{:noreply, state}
end
## -- UserTrack
def handle_info({:joined, channel}, state) do
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)
+ Nola.UserTrack.joined(channel, who, priv, false)
account = Nola.Account.lookup(who)
if account do
{:account, who.network, channel, who.nick, account.id}
end
end)
|> Enum.filter(fn(x) -> x end)
dispatch("account", {:accounts, accounts})
{:noreply, state}
end
def handle_info({:quit, reason, sender}, state) do
- IRC.UserTrack.quitted(sender, reason)
+ Nola.UserTrack.quitted(sender, reason)
{:noreply, state}
end
def handle_info({:joined, channel, sender}, state) do
- IRC.UserTrack.joined(channel, sender, [])
+ Nola.UserTrack.joined(channel, sender, [])
account = Nola.Account.lookup(sender)
if account do
dispatch("account", {:account, sender.network, channel, sender.nick, account.id})
end
{:noreply, state}
end
def handle_info({:kicked, nick, _by, channel, _reason}, state) do
- IRC.UserTrack.parted(state.network, channel, nick)
+ Nola.UserTrack.parted(state.network, channel, nick)
{:noreply, state}
end
def handle_info({:parted, channel, %ExIRC.SenderInfo{nick: nick}}, state) do
- IRC.UserTrack.parted(state.network, channel, nick)
+ Nola.UserTrack.parted(state.network, channel, nick)
{:noreply, state}
end
def handle_info({:mode, [channel, mode, nick]}, state) do
track_mode(state.network, channel, nick, mode)
{:noreply, state}
end
def handle_info({:nick_changed, old_nick, new_nick}, state) do
- IRC.UserTrack.renamed(state.network, old_nick, new_nick)
+ Nola.UserTrack.renamed(state.network, old_nick, new_nick)
{:noreply, state}
end
def handle_info(unhandled, client) do
Logger.debug("unhandled: #{inspect unhandled}")
{:noreply, client}
end
def publish(pub), do: publish(pub, [])
def publish(m = %IRC.Message{trigger: nil}, keys) do
dispatch(["messages"] ++ keys, {:irc, :text, m})
end
def publish(m = %IRC.Message{trigger: t = %IRC.Trigger{trigger: trigger}}, keys) do
dispatch(["triggers", "#{m.network}/#{m.channel}:triggers", "trigger:"<>trigger], {:irc, :trigger, trigger, m})
end
def publish_event(net, event = %{type: _}) when is_binary(net) do
event = event
|> Map.put(:at, NaiveDateTime.utc_now())
|> Map.put(:network, net)
dispatch("#{net}:events", {:irc, :event, event})
end
def publish_event({net, chan}, event = %{type: type}) do
event = event
|> Map.put(:at, NaiveDateTime.utc_now())
|> Map.put(:network, net)
|> Map.put(:channel, chan)
dispatch("#{net}/#{chan}:events", {:irc, :event, event})
end
def dispatch(keys, content, sub \\ Nola.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
Logger.debug("dispatch #{inspect 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(state = %{client: client, network: network}, {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()
outputs = for line <- lines do
ExIRC.Client.msg(client, :privmsg, target, line)
{:irc, :out, %IRC.Message{id: FlakeId.get(), transport: :irc, network: network,
channel: target, text: line, sender: %ExIRC.SenderInfo{nick: state.conn.nick}, at: NaiveDateTime.utc_now(), meta: %{self: true}}}
end
for f <- outputs, do: dispatch(["irc:outputs", "#{network}/#{target}:outputs"], f)
end
defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:kick, reason}) do
ExIRC.Client.kick(client, target, nick, reason)
end
defp irc_reply(%{client: client}, {target, _}, {:kick, nick, reason}) do
ExIRC.Client.kick(client, target, nick, reason)
end
defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:mode, mode}) do
ExIRC.Client.mode(%{client: client}, target, mode, nick)
end
defp irc_reply(%{client: client}, target, {:mode, mode, nick}) do
ExIRC.Client.mode(client, target, mode, nick)
end
defp irc_reply(%{client: 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], []})
+ Nola.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]})
+ Nola.UserTrack.change_privileges(network, channel, nick, {[], [:operator]})
:ok
end
defp track_mode(network, channel, nick, "+v") do
- IRC.UserTrack.change_privileges(network, channel, nick, {[:voice], []})
+ Nola.UserTrack.change_privileges(network, channel, nick, {[:voice], []})
:ok
end
defp track_mode(network, channel, nick, "-v") do
- IRC.UserTrack.change_privileges(network, channel, nick, {[], [:voice]})
+ Nola.UserTrack.change_privileges(network, channel, nick, {[], [:voice]})
:ok
end
defp track_mode(network, channel, nick, mode) do
Logger.warn("Unhandled track_mode: #{inspect {nick, mode}}")
:ok
end
defp server(%{conn: %{host: host, port: port}}) do
host <> ":" <> to_string(port)
end
end
diff --git a/lib/irc/irc.ex b/lib/irc/irc.ex
index 7cd9dc8..93525e4 100644
--- a/lib/irc/irc.ex
+++ b/lib/irc/irc.ex
@@ -1,79 +1,79 @@
defmodule IRC do
defmodule Message do
@derive {Poison.Encoder, except: [:replyfun]}
defstruct [:id,
:text,
{:transport, :irc},
:network,
:account,
:sender,
:channel,
:trigger,
:replyfun,
:at,
{:meta, %{}}
]
end
defmodule Trigger do
@derive Poison.Encoder
defstruct [:type, :trigger, :args]
end
def send_message_as(account, network, channel, text, force_puppet \\ false) do
connection = IRC.Connection.get_network(network)
if connection && (force_puppet || IRC.PuppetConnection.whereis(account, connection)) do
IRC.PuppetConnection.start_and_send_message(account, connection, channel, text)
else
- user = IRC.UserTrack.find_by_account(network, account)
+ user = Nola.UserTrack.find_by_account(network, account)
nick = if(user, do: user.nick, else: account.name)
IRC.Connection.broadcast_message(network, channel, "<#{nick}> #{text}")
end
end
def register(key) do
case Registry.register(Nola.PubSub, key, []) do
{:ok, _} -> :ok
error -> error
end
end
def admin?(%Message{sender: sender}), do: admin?(sender)
def admin?(%{nick: nick, user: user, host: host}) do
for {n, u, h} <- Nola.IRC.env(:admins, []) do
admin_part_match?(n, nick) && admin_part_match?(u, user) && admin_part_match?(h, host)
end
|> Enum.any?
end
defp admin_part_match?(:_, _), do: true
defp admin_part_match?(a, a), do: true
defp admin_part_match?(_, _), do: false
@max_chars 440
def splitlong(string, max_chars \\ 440)
def splitlong(string, max_chars) when is_list(string) do
Enum.map(string, fn(s) -> splitlong(s, max_chars) end)
|> List.flatten()
end
def splitlong(string, max_chars) do
string
|> String.codepoints
|> Enum.chunk_every(max_chars)
|> Enum.map(&Enum.join/1)
end
def splitlong_with_prefix(string, prefix, max_chars \\ 440) do
prefix = "#{prefix} "
max_chars = max_chars - (length(String.codepoints(prefix)))
string
|> String.codepoints
|> Enum.chunk_every(max_chars)
|> Enum.map(fn(line) -> prefix <> Enum.join(line) end)
end
end
diff --git a/lib/irc/puppet_connection.ex b/lib/irc/puppet_connection.ex
index fd0a98e..75a06f3 100644
--- a/lib/irc/puppet_connection.ex
+++ b/lib/irc/puppet_connection.ex
@@ -1,238 +1,238 @@
defmodule IRC.PuppetConnection do
require Logger
@min_backoff :timer.seconds(5)
@max_backoff :timer.seconds(2*60)
@max_idle :timer.hours(12)
@env Mix.env
defmodule Supervisor do
use DynamicSupervisor
def start_link() do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def start_child(%Nola.Account{id: account_id}, %IRC.Connection{id: connection_id}) do
spec = %{id: {account_id, connection_id}, start: {IRC.PuppetConnection, :start_link, [account_id, connection_id]}, 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 whereis(account = %Nola.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}) do
{:global, name} = name(account_id, connection_id)
case :global.whereis_name(name) do
:undefined -> nil
pid -> pid
end
end
def send_message(account = %Nola.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}, channel, text) do
GenServer.cast(name(account_id, connection_id), {:send_message, self(), channel, text})
end
def start_and_send_message(account = %Nola.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}, channel, text) do
{:global, name} = name(account_id, connection_id)
pid = whereis(account, connection)
pid = if !pid do
case IRC.PuppetConnection.Supervisor.start_child(account, connection) do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
end
else
pid
end
GenServer.cast(pid, {:send_message, self(), channel, text})
end
def start(account = %Nola.Account{}, connection = %IRC.Connection{}) do
IRC.PuppetConnection.Supervisor.start_child(account, connection)
end
def start_link(account_id, connection_id) do
GenServer.start_link(__MODULE__, [account_id, connection_id], name: name(account_id, connection_id))
end
def name(account_id, connection_id) do
{:global, {PuppetConnection, account_id, connection_id}}
end
def init([account_id, connection_id]) do
account = %Nola.Account{} = Nola.Account.get(account_id)
connection = %IRC.Connection{} = IRC.Connection.lookup(connection_id)
Logger.metadata(puppet_conn: account.id <> "@" <> connection.id)
backoff = :backoff.init(@min_backoff, @max_backoff)
|> :backoff.type(:jitter)
idle = :erlang.send_after(@max_idle, self, :idle)
{:ok, %{client: nil, backoff: backoff, idle: idle, connected: false, buffer: [], channels: [], connection_id: connection_id, account_id: account_id, connected_server: nil, connected_port: nil, network: connection.network}, {:continue, :connect}}
end
def handle_continue(:connect, state) do
#ipv6 = if @env == :prod do
# subnet = Nola.Subnet.assign(state.account_id)
# Nola.Account.put_meta(Nola.Account.get(state.account_id), "subnet", subnet)
# ip = Pfx.host(subnet, 1)
# {:ok, ipv6} = :inet_parse.ipv6_address(to_charlist(ip))
# System.cmd("add-ip6", [ip])
# ipv6
#end
conn = IRC.Connection.lookup(state.connection_id)
client_opts = []
|> Keyword.put(:network, conn.network)
client = if state.client && Process.alive?(state.client) do
Logger.info("Reconnecting client")
state.client
else
Logger.info("Connecting")
{:ok, client} = ExIRC.Client.start_link(debug: false)
ExIRC.Client.add_handler(client, self())
client
end
base_opts = [
{:nodelay, true}
]
#{ip, opts} = case {ipv6, :inet_res.resolve(to_charlist(conn.host), :in, :aaaa)} do
# {ipv6, {:ok, {:dns_rec, _dns_header, _query, rrs = [{:dns_rr, _, _, _, _, _, _, _, _, _} | _], _, _}}} ->
# ip = rrs
# |> Enum.map(fn({:dns_rr, _, :aaaa, :in, _, _, ipv6, _, _, _}) -> ipv6 end)
# |> Enum.shuffle()
# |> List.first()
# opts = [
# :inet6,
# {:ifaddr, ipv6}
# ]
# {ip, opts}
# _ ->
{ip, opts} = {to_charlist(conn.host), []}
#end
conn_fun = if conn.tls, do: :connect_ssl!, else: :connect!
apply(ExIRC.Client, conn_fun, [client, ip, conn.port, base_opts ++ opts])
{:noreply, %{state | client: client}}
end
def handle_continue(:connected, state) do
state = Enum.reduce(Enum.reverse(state.buffer), state, fn(b, state) ->
{:noreply, state} = handle_cast(b, state)
state
end)
{:noreply, %{state | buffer: []}}
end
def handle_cast(cast = {:send_message, _pid, _channel, _text}, state = %{connected: false, buffer: buffer}) do
{:noreply, %{state | buffer: [cast | buffer]}}
end
def handle_cast({:send_message, pid, channel, text}, state = %{connected: true}) do
channels = if !Enum.member?(state.channels, channel) do
ExIRC.Client.join(state.client, channel)
[channel | state.channels]
else
state.channels
end
ExIRC.Client.msg(state.client, :privmsg, channel, text)
meta = %{puppet: true, from: pid}
account = Nola.Account.get(state.account_id)
nick = make_nick(state)
sender = %ExIRC.SenderInfo{network: state.network, nick: suffix_nick(nick), user: nick, host: "puppet."}
reply_fun = fn(text) ->
IRC.Connection.broadcast_message(state.network, channel, text)
end
message = %IRC.Message{id: FlakeId.get(), at: NaiveDateTime.utc_now(), text: text, network: state.network, account: account, sender: sender, channel: channel, replyfun: reply_fun, trigger: IRC.Connection.extract_trigger(text), meta: meta}
- message = case IRC.UserTrack.messaged(message) do
+ message = case Nola.UserTrack.messaged(message) do
:ok -> message
{:ok, message} -> message
end
IRC.Connection.publish(message, ["#{message.network}/#{channel}:messages"])
idle = if length(state.buffer) == 0 do
:erlang.cancel_timer(state.idle)
:erlang.send_after(@max_idle, self(), :idle)
else
state.idle
end
{:noreply, %{state | idle: idle, channels: channels}}
end
def handle_info(:idle, state) do
ExIRC.Client.quit(state.client, "Puppet was idle for too long")
ExIRC.Client.stop!(state.client)
{:stop, :normal, state}
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 | connected: false, backoff: backoff}}
end
def handle_info(:connect, state) do
{:noreply, state, {:continue, :connect}}
end
# Connection successful
def handle_info({:connected, server, port}, state) do
Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect state}")
{_, backoff} = :backoff.succeed(state.backoff)
base_nick = make_nick(state)
ExIRC.Client.logon(state.client, "", suffix_nick(base_nick), base_nick, "#{base_nick}'s puppet")
{:noreply, %{state | backoff: backoff, connected_server: server, connected_port: port}}
end
# Logon successful
def handle_info(:logged_in, state) do
Logger.info("#{inspect(self())} Logged in")
{_, backoff} = :backoff.succeed(state.backoff)
# Create an UserTrack entry for the client so it's authenticated to the right account_id already.
- IRC.UserTrack.connected(state.network, suffix_nick(make_nick(state)), make_nick(state), "puppet.", state.account_id, %{puppet: true})
+ Nola.UserTrack.connected(state.network, suffix_nick(make_nick(state)), make_nick(state), "puppet.", state.account_id, %{puppet: true})
{:noreply, %{state | backoff: backoff}}
end
# ISUP
def handle_info({:isup, network}, state) do
{:noreply, %{state | network: network, connected: true}, {:continue, :connected}}
end
# Been kicked
def handle_info({:kicked, _sender, chan, _reason}, state) do
{:noreply, %{state | channels: state.channels -- [chan]}}
end
def handle_info(_info, state) do
{:noreply, state}
end
def make_nick(state) do
account = Nola.Account.get(state.account_id)
- user = IRC.UserTrack.find_by_account(state.network, account)
+ user = Nola.UserTrack.find_by_account(state.network, account)
base_nick = if(user, do: user.nick, else: account.name)
clean_nick = case String.split(base_nick, ":", parts: 2) do
["@"<>nick, _] -> nick
[nick] -> nick
end
clean_nick
end
if Mix.env == :dev do
def suffix_nick(nick), do: "#{nick}[d]"
else
def suffix_nick(nick), do: "#{nick}[p]"
end
end
diff --git a/lib/matrix/room.ex b/lib/matrix/room.ex
index 57f35b8..757aad0 100644
--- a/lib/matrix/room.ex
+++ b/lib/matrix/room.ex
@@ -1,196 +1,196 @@
defmodule Nola.Matrix.Room do
require Logger
alias Nola.Matrix
alias Polyjuice.Client
import Matrix, only: [client: 0, client: 1, user_name: 1, myself?: 1]
defmodule Supervisor do
use DynamicSupervisor
def start_link() do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def start_child(room_id) do
spec = %{id: room_id, start: {Nola.Matrix.Room, :start_link, [room_id]}, 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 start(room_id) do
__MODULE__.Supervisor.start_child(room_id)
end
def start_link(room_id) do
GenServer.start_link(__MODULE__, [room_id], name: name(room_id))
end
def start_and_send_matrix_event(room_id, event) do
pid = if pid = whereis(room_id) do
pid
else
case __MODULE__.start(room_id) do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
:ignore -> nil
end
end
if(pid, do: send(pid, {:matrix_event, event}))
end
def whereis(room_id) do
{:global, name} = name(room_id)
case :global.whereis_name(name) do
:undefined -> nil
pid -> pid
end
end
def name(room_id) do
{:global, {__MODULE__, room_id}}
end
def init([room_id]) do
case Matrix.lookup_room(room_id) do
{:ok, state} ->
Logger.metadata(matrix_room: room_id)
{:ok, _} = Registry.register(Nola.PubSub, "#{state.network}:events", plugin: __MODULE__)
for t <- ["messages", "triggers", "outputs", "events"] do
{:ok, _} = Registry.register(Nola.PubSub, "#{state.network}/#{state.channel}:#{t}", plugin: __MODULE__)
end
state = state
|> Map.put(:id, room_id)
Logger.info("Started Matrix room #{room_id}")
{:ok, state, {:continue, :update_state}}
error ->
Logger.info("Received event for nonexistent room #{inspect room_id}: #{inspect error}")
:ignore
end
end
def handle_continue(:update_state, state) do
{:ok, s} = Client.Room.get_state(client(), state.id)
members = Enum.reduce(s, [], fn(s, acc) ->
if s["type"] == "m.room.member" do
if s["content"]["membership"] == "join" do
[s["user_id"] | acc]
else
# XXX: The user left, remove from Nola.Memberships ?
acc
end
else
acc
end
end)
|> Enum.filter(& &1)
- for m <- members, do: IRC.UserTrack.joined(state.id, %{network: "matrix", nick: m, user: m, host: "matrix."}, [], true)
+ for m <- members, do: Nola.UserTrack.joined(state.id, %{network: "matrix", nick: m, user: m, host: "matrix."}, [], true)
- accounts = IRC.UserTrack.channel(state.network, state.channel)
+ accounts = Nola.UserTrack.channel(state.network, state.channel)
|> Enum.filter(& &1)
- |> Enum.map(fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple).account end)
+ |> Enum.map(fn(tuple) -> Nola.UserTrack.User.from_tuple(tuple).account end)
|> Enum.uniq()
|> Enum.each(fn(account_id) ->
introduce_irc_account(account_id, state)
end)
{:noreply, state}
end
def handle_info({:irc, :text, message}, state), do: handle_irc(message, state)
def handle_info({:irc, :out, message}, state), do: handle_irc(message, state)
def handle_info({:irc, :trigger, _, message}, state), do: handle_irc(message, state)
def handle_info({:irc, :event, event}, state), do: handle_irc(event, state)
def handle_info({:matrix_event, event}, state) do
if myself?(event.user_id) do
{:noreply, state}
else
handle_matrix(event, state)
end
end
def handle_irc(message = %IRC.Message{account: account}, state) do
unless Map.get(message.meta, :puppet) && Map.get(message.meta, :from) == self() do
opts = if Map.get(message.meta, :self) || is_nil(account) do
[]
else
mxid = Matrix.get_or_create_matrix_user(account.id)
[user_id: mxid]
end
Client.Room.send_message(client(opts),state.id, message.text)
end
{:noreply, state}
end
def handle_irc(%{type: :join, account_id: account_id}, state) do
introduce_irc_account(account_id, state)
{:noreply, state}
end
def handle_irc(%{type: quit_or_part, account_id: account_id}, state) when quit_or_part in [:quit, :part] do
mxid = Matrix.get_or_create_matrix_user(account_id)
Client.Room.leave(client(user_id: mxid), state.id)
{:noreply, state}
end
def handle_irc(event, state) do
Logger.warn("Skipped irc event #{inspect event}")
{:noreply, state}
end
def handle_matrix(event = %{type: "m.room.member", user_id: user_id, content: %{"membership" => "join"}}, state) do
_account = get_account(event, state)
- IRC.UserTrack.joined(state.id, %{network: "matrix", nick: user_id, user: user_id, host: "matrix."}, [], true)
+ Nola.UserTrack.joined(state.id, %{network: "matrix", nick: user_id, user: user_id, host: "matrix."}, [], true)
{:noreply, state}
end
def handle_matrix(event = %{type: "m.room.member", user_id: user_id, content: %{"membership" => "leave"}}, state) do
- IRC.UserTrack.parted(state.id, %{network: "matrix", nick: user_id})
+ Nola.UserTrack.parted(state.id, %{network: "matrix", nick: user_id})
{:noreply, state}
end
def handle_matrix(event = %{type: "m.room.message", user_id: user_id, content: %{"msgtype" => "m.text", "body" => text}}, state) do
IRC.send_message_as(get_account(event, state), state.network, state.channel, text, true)
{:noreply, state}
end
def handle_matrix(event, state) do
Logger.warn("Skipped matrix event #{inspect event}")
{:noreply, state}
end
def get_account(%{user_id: user_id}, %{id: id}) do
Nola.Account.find_by_nick("matrix", user_id)
end
defp introduce_irc_account(account_id, state) do
mxid = Matrix.get_or_create_matrix_user(account_id)
account = Nola.Account.get(account_id)
- user = IRC.UserTrack.find_by_account(state.network, account)
+ user = Nola.UserTrack.find_by_account(state.network, account)
base_nick = if(user, do: user.nick, else: account.name)
case Client.Profile.put_displayname(client(user_id: mxid), base_nick) do
:ok -> :ok
error ->
Logger.warn("Failed to update profile for #{mxid}: #{inspect error}")
end
case Client.Room.join(client(user_id: mxid), state.id) do
{:ok, _} -> :ok
error ->
Logger.warn("Failed to join room for #{mxid}: #{inspect error}")
end
:ok
end
end
diff --git a/lib/nola/account.ex b/lib/nola/account.ex
index 4f1e7ff..06ca993 100644
--- a/lib/nola/account.ex
+++ b/lib/nola/account.ex
@@ -1,263 +1,263 @@
defmodule Nola.Account do
- alias IRC.UserTrack.User
+ alias Nola.UserTrack.User
@moduledoc """
Account registry....
Maps a network predicate:
* `{net, {:nick, nickname}}`
* `{net, {:account, account}}`
* `{net, {:mask, user@host}}`
to an unique identifier, that can be shared over multiple networks.
If a predicate cannot be found for an existing account, a new account will be made in the database.
To link two existing accounts from different network onto a different one, a merge operation is provided.
"""
# FIXME: Ensure uniqueness of name?
@derive {Poison.Encoder, except: [:token]}
defstruct [:id, :name, :token]
@type t :: %__MODULE__{id: id(), name: String.t()}
@type id :: String.t()
defimpl Inspect, for: __MODULE__ do
import Inspect.Algebra
def inspect(%{id: id, name: name}, opts) do
concat(["#Nola.Account[", id, " ", name, "]"])
end
end
def file(base) do
to_charlist(Nola.data_path() <> "/account_#{base}.dets")
end
defp from_struct(%__MODULE__{id: id, name: name, token: token}) do
{id, name, token}
end
defp from_tuple({id, name, token}) do
%__MODULE__{id: id, name: name, token: token}
end
def start_link() do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def init(_) do
{:ok, accounts} = :dets.open_file(file("db"), [])
{:ok, meta} = :dets.open_file(file("meta"), [])
{:ok, predicates} = :dets.open_file(file("predicates"), [{:type, :set}])
{:ok, %{accounts: accounts, meta: meta, predicates: predicates}}
end
def get(id) do
case :dets.lookup(file("db"), id) do
[account] -> from_tuple(account)
_ -> nil
end
end
def get_by_name(name) do
spec = [{{:_, :"$1", :_}, [{:==, :"$1", {:const, name}}], [:"$_"]}]
case :dets.select(file("db"), spec) do
[account] -> from_tuple(account)
_ -> nil
end
end
def get_meta(%__MODULE__{id: id}, key, default \\ nil) do
case :dets.lookup(file("meta"), {id, key}) do
[{_, value}] -> (value || default)
_ -> default
end
end
@spec find_meta_accounts(String.t()) :: [{account :: t(), value :: String.t()}, ...]
@doc "Find all accounts that have a meta of `key`."
def find_meta_accounts(key) do
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$2", {:const, key}}], [{{:"$1", :"$3"}}]}]
for {id, val} <- :dets.select(file("meta"), spec), do: {get(id), val}
end
@doc "Find an account given a specific meta `key` and `value`."
@spec find_meta_account(String.t(), String.t()) :: t() | nil
def find_meta_account(key, value) do
#spec = [{{{:"$1", :"$2"}, :"$3"}, [:andalso, {:==, :"$2", {:const, key}}, {:==, :"$3", {:const, value}}], [:"$1"]}]
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:andalso, {:==, :"$2", {:const, key}}, {:==, {:const, value}, :"$3"}}], [:"$1"]}]
case :dets.select(file("meta"), spec) do
[id] -> get(id)
_ -> nil
end
end
def get_all_meta(%__MODULE__{id: id}) do
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$1", {:const, id}}], [{{:"$2", :"$3"}}]}]
:dets.select(file("meta"), spec)
end
def put_user_meta(account = %__MODULE__{}, key, value) do
put_meta(account, "u:"<>key, value)
end
def put_meta(%__MODULE__{id: id}, key, value) do
:dets.insert(file("meta"), {{id, key}, value})
end
def delete_meta(%__MODULE__{id: id}, key) do
:dets.delete(file("meta"), {id, key})
end
def all_accounts() do
:dets.traverse(file("db"), fn(obj) -> {:continue, from_tuple(obj)} end)
end
def all_predicates() do
:dets.traverse(file("predicates"), fn(obj) -> {:continue, obj} end)
end
def all_meta() do
:dets.traverse(file("meta"), fn(obj) -> {:continue, obj} end)
end
def merge_account(old_id, new_id) do
if old_id != new_id do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, old_id}}], [:"$1"]}]
predicates = :dets.select(file("predicates"), spec)
for pred <- predicates, do: :ok = :dets.insert(file("predicates"), {pred, new_id})
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$1", {:const, old_id}}], [{{:"$2", :"$3"}}]}]
metas = :dets.select(file("meta"), spec)
for {k,v} <- metas do
:dets.delete(file("meta"), {{old_id, k}})
:ok = :dets.insert(file("meta"), {{new_id, k}, v})
end
:dets.delete(file("db"), old_id)
Nola.Membership.merge_account(old_id, new_id)
- IRC.UserTrack.merge_account(old_id, new_id)
+ Nola.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 = Nola.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
+ if user = Nola.UserTrack.find_by_nick(sender) do
lookup(user, make_default)
else
#FIXME this will never work with continued lookup by other methods as Who isn't compatible
lookup_by_nick(sender, :dets.lookup(file("predicates"), {sender.network,{:nick, sender.nick}}), make_default)
end
end
defp do_lookup(sender = %ExIRC.SenderInfo{}, make_default) do
- lookup(IRC.UserTrack.find_by_nick(sender), make_default)
+ lookup(Nola.UserTrack.find_by_nick(sender), make_default)
end
defp do_lookup(user = %User{account: id}, make_default) when is_binary(id) do
get(id)
end
defp do_lookup(user = %User{network: server, nick: nick}, make_default) do
lookup_by_nick(user, :dets.lookup(file("predicates"), {server,{:nick, nick}}), make_default)
end
defp do_lookup(nil, _) do
nil
end
defp lookup_by_nick(_, [{_, id}], _make_default) do
get(id)
end
defp lookup_by_nick(user, _, make_default) do
#authenticate_by_host(user)
if make_default, do: new_account(user), else: nil
end
def new_account(nick) do
id = EntropyString.large_id()
:dets.insert(file("db"), {id, nick, EntropyString.token()})
get(id)
end
def new_account(%{nick: nick, network: server}) do
id = EntropyString.large_id()
:dets.insert(file("db"), {id, nick, EntropyString.token()})
:dets.insert(file("predicates"), {{server, {:nick, nick}}, id})
get(id)
end
def update_account_name(account = %__MODULE__{id: id}, name) do
account = %__MODULE__{account | name: name}
:dets.insert(file("db"), from_struct(account))
get(id)
end
def get_predicates(%__MODULE__{} = account) do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
:dets.select(file("predicates"), spec)
end
end
diff --git a/lib/nola/auth_token.ex b/lib/nola/auth_token.ex
index d125ea4..9760ec7 100644
--- a/lib/nola/auth_token.ex
+++ b/lib/nola/auth_token.ex
@@ -1,59 +1,59 @@
defmodule Nola.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} ->
NolaWeb.Router.Helpers.login_path(NolaWeb.Endpoint, :token, id)
error ->
error
end
end
def new_url(account, perks \\ nil) do
case new(account, perks) do
{:ok, id} ->
NolaWeb.Router.Helpers.login_url(NolaWeb.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()
+ id = Nola.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/nola/membership.ex b/lib/nola/membership.ex
index b98efd7..1c7303b 100644
--- a/lib/nola/membership.ex
+++ b/lib/nola/membership.ex
@@ -1,129 +1,129 @@
defmodule Nola.Membership do
@moduledoc """
Memberships (users in channels)
"""
# Key: {account, net, channel}
# Format: {key, last_seen}
defp dets() do
to_charlist(Nola.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(%Nola.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(%Nola.Account{id: id}, network, channel) do
:dets.insert(dets(), {{id, network, channel}, NaiveDateTime.utc_now()})
end
def touch(account_id, network, channel) do
if account = Nola.Account.get(account_id) do
touch(account, network, channel)
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 = %Nola.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 = Nola.Account.get(id) do
- user = IRC.UserTrack.find_by_account(network, account)
+ user = Nola.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/nola/plugins.ex b/lib/nola/plugins.ex
index 4b55f4c..b0c3ce3 100644
--- a/lib/nola/plugins.ex
+++ b/lib/nola/plugins.ex
@@ -1,99 +1,100 @@
defmodule Nola.Plugins do
require Logger
defmodule Supervisor do
use DynamicSupervisor
require Logger
def start_link() do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def start_child(module, opts \\ []) do
Logger.info("Starting #{module}")
spec = %{id: {__MODULE__,module}, start: {__MODULE__, :start_link, [module, opts]}, name: module, restart: :transient}
case DynamicSupervisor.start_child(__MODULE__, spec) do
{:ok, _} = res -> res
:ignore ->
Logger.warn("Ignored #{module}")
:ignore
{:error,_} = res ->
Logger.error("Could not start #{module}: #{inspect(res, pretty: true)}")
res
end
end
@impl true
def init(_init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
max_restarts: 10,
max_seconds: 1
)
end
end
def dets(), do: to_charlist(Nola.data_path("/plugins.dets"))
def setup() do
:dets.open_file(dets(), [])
end
def enabled() do
:dets.foldl(fn
{name, true, _}, acc -> [name | acc]
_, acc -> acc
end, [], dets())
end
def start_all() do
+ Logger.info("starting plugins.")
for mod <- enabled(), do: {mod, __MODULE__.Supervisor.start_child(mod)}
end
def declare(module) do
case get(module) do
:disabled -> :dets.insert(dets(), {module, true, nil})
_ -> nil
end
end
def start(module, opts \\ []) do
__MODULE__.Supervisor.start_child(module)
end
@doc "Enables a plugin"
def enable(name), do: switch(name, true)
@doc "Disables a plugin"
def disable(name), do: switch(name, false)
@doc "Enables or disables a plugin"
def switch(name, value) when is_boolean(value) do
last = case get(name) do
{:ok, last} -> last
_ -> nil
end
:dets.insert(dets(), {name, value, last})
end
@spec get(module()) :: {:ok, last_start :: nil | non_neg_integer()} | :disabled
def get(name) do
case :dets.lookup(dets(), name) do
[{name, enabled, last_start}] -> {:ok, enabled, last_start}
_ -> :disabled
end
end
def start_link(module, options \\ []) do
with {:disabled, {_, true, last}} <- {:disabled, get(module)},
{:throttled, false} <- {:throttled, false}
do
module.start_link()
else
{error, _} ->
Logger.info("#{__MODULE__}: #{to_string(module)} ignored start: #{to_string(error)}")
:ignore
end
end
end
diff --git a/lib/nola/token.ex b/lib/nola/token.ex
index 563ac72..179bed2 100644
--- a/lib/nola/token.ex
+++ b/lib/nola/token.ex
@@ -1,38 +1,38 @@
defmodule Nola.Token do
use GenServer
def start_link() do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def lookup(id) do
with \
[{_, cred, date}] <- :ets.lookup(__MODULE__.ETS, id),
IO.inspect("cred: #{inspect cred} valid for #{inspect date} now #{inspect DateTime.utc_now()}"),
d when d > 0 <- DateTime.diff(date, DateTime.utc_now())
do
{:ok, cred}
else
err -> {:error, err}
end
end
def new(cred) do
GenServer.call(__MODULE__, {:new, cred})
end
def init(_) do
ets = :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
{:ok, ets}
end
def handle_call({:new, cred}, _, ets) do
- id = IRC.UserTrack.Id.large_id()
+ id = Nola.UserTrack.Id.large_id()
expire = DateTime.utc_now()
|> DateTime.add(15*60, :second)
obj = {id, cred, expire}
:ets.insert(ets, obj)
{:reply, {:ok, id}, ets}
end
end
diff --git a/lib/irc/user_track.ex b/lib/nola/user_track.ex
similarity index 94%
rename from lib/irc/user_track.ex
rename to lib/nola/user_track.ex
index 56a319f..2a051f9 100644
--- a/lib/irc/user_track.ex
+++ b/lib/nola/user_track.ex
@@ -1,329 +1,329 @@
-defmodule IRC.UserTrack do
+defmodule Nola.UserTrack do
@moduledoc """
User Track DB & Utilities
"""
- @ets IRC.UserTrack.Storage
+ @ets Nola.UserTrack.Storage
# {uuid, network, nick, nicks, privilege_map}
# Privilege map:
# %{"#channel" => [:operator, :voice]
defmodule Storage do
def delete(id) do
op(fn(ets) -> :ets.delete(ets, id) end)
end
def insert(tuple) do
op(fn(ets) -> :ets.insert(ets, tuple) end)
end
def clear_network(network) do
op(fn(ets) ->
spec = [
{{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
{:==, :"$1", {:const, network}}
], [:"$_"]}
]
:ets.match_delete(ets, spec)
end)
end
def op(fun) do
GenServer.call(__MODULE__, {:op, fun})
end
def start_link do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def init([]) do
ets = :ets.new(__MODULE__, [:set, :named_table, :protected, {:read_concurrency, true}])
{:ok, ets}
end
def handle_call({:op, fun}, _from, ets) do
returned = try do
{:ok, fun.(ets)}
rescue
rescued -> {:error, rescued}
catch
rescued -> {:error, rescued}
end
{:reply, returned, ets}
end
def terminate(_reason, ets) do
:ok
end
end
defmodule Id, do: use EntropyString
defmodule User do
defstruct [:id, :account, :network, :nick, {:nicks, []}, :username, :host, :realname, {:privileges, %{}}, {:last_active, %{}}, {:options, %{}}]
def to_tuple(u = %__MODULE__{}) do
- {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, u.options}
+ {u.id || Nola.UserTrack.Id.large_id, u.network, u.account, String.downcase(u.nick), u.nick, u.nicks || [], u.username, u.host, u.realname, u.privileges, u.last_active, u.options}
end
#tuple size: 11
def from_tuple({id, network, account, _downcased_nick, nick, nicks, username, host, realname, privs, last_active, opts}) do
struct = %__MODULE__{id: id, account: account, network: network, nick: nick, nicks: nicks, username: username, host: host, realname: realname, privileges: privs, last_active: last_active, options: opts}
end
end
def find_by_account(%Nola.Account{id: id}) do
#iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
spec = [
{{:_, :_, :"$2", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
{:==, :"$2", {:const, id}}
], [:"$_"]}
]
results = :ets.select(@ets, spec)
|> Enum.filter(& &1)
for obj <- results, do: User.from_tuple(obj)
end
def find_by_account(network, nil) do
nil
end
def find_by_account(network, %Nola.Account{id: id}) do
#iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
spec = [
{{:_, :"$1", :"$2", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
{:andalso, {:==, :"$1", {:const, network}},
{:==, :"$2", {:const, id}}}
], [:"$_"]}
]
case :ets.select(@ets, spec) do
results = [_r | _] ->
result = results
|> Enum.reject(fn({_, net, _, _, _, _, _, _, _, _, actives, opts}) -> network != "matrix" && net == "matrix" end)
|> Enum.reject(fn({_, net, _, _, _, _, _, _, _, _, actives, opts}) -> network != "telegram" && net == "telegram" end)
|> Enum.reject(fn({_, _, _, _, _, _, _, _, _, _, actives, opts}) -> network not in ["matrix", "telegram"] && Map.get(opts, :puppet) end)
|> Enum.sort_by(fn({_, _, _, _, _, _, _, _, _, _, actives, _}) ->
Map.get(actives, nil)
end, {:desc, NaiveDateTime})
|> List.first
if result, do: User.from_tuple(result)
_ -> nil
end
end
def clear_network(network) do
Storage.clear_network(network)
end
def merge_account(old_id, new_id) do
#iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
spec = [
{{:_, :_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
{:==, :"$1", {:const, old_id}}
], [:"$_"]}
]
Enum.each(:ets.select(@ets, spec), fn({id, net, _, downcased_nick, nick, nicks, username, host, realname, privs, active, opts}) ->
Storage.op(fn(ets) ->
:ets.insert(@ets, {id, net, new_id, downcased_nick, nick, nicks, username, host, realname, privs, active, opts})
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
case :ets.lookup(@ets, id) do
[] -> nil
[tuple] -> User.from_tuple(tuple)
end
end
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
false
end
end
def channel(network, channel) do
Enum.filter(to_list(), fn({_, network, _, _, _, _, _, _, _, channels, _, _}) ->
Map.get(channels, channel)
end)
end
# TODO
def connected(network, nick, user, host, account_id, opts \\ %{}) do
if account = Nola.Account.get(account_id) do
user = if user = find_by_nick(network, nick) do
user
else
- user = %User{id: IRC.UserTrack.Id.large_id, account: account_id, network: network, nick: nick, username: user, host: host, privileges: %{}, options: opts}
+ user = %User{id: Nola.UserTrack.Id.large_id, account: account_id, network: network, nick: nick, username: user, host: host, privileges: %{}, options: opts}
Storage.op(fn(ets) ->
:ets.insert(ets, User.to_tuple(user))
end)
user
end
IRC.Connection.publish_event(network, %{type: :connect, user_id: user.id, account_id: user.account})
:ok
else
:error
end
end
def joined(c, s), do: joined(c,s,[])
def joined(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(sender.network, nick) do
%User{user | username: uname, host: host, privileges: Map.put(user.privileges || %{}, channel, privileges)}
else
- user = %User{id: IRC.UserTrack.Id.large_id, network: sender.network, nick: nick, username: uname, host: host, privileges: %{channel => privileges}}
+ user = %User{id: Nola.UserTrack.Id.large_id, network: sender.network, nick: nick, username: uname, host: host, privileges: %{channel => privileges}}
account = Nola.Account.lookup(user).id
user = %User{user | account: account}
end
user = touch_struct(user, channel)
if touch && user.account do
Nola.Membership.touch(user.account, sender.network, channel)
end
Storage.op(fn(ets) ->
:ets.insert(ets, User.to_tuple(user))
end)
IRC.Connection.publish_event({sender.network, channel}, %{type: :join, user_id: user.id, account_id: user.account})
user
end
#def joined(network, channel, nick, privileges) do
# 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 || Nola.Account.lookup(user)}
else
user = %User{network: network, nick: nick, privileges: %{}}
account = Nola.Account.lookup(user)
{%User{user | account: account.id}, account}
end
Storage.insert(User.to_tuple(user))
if chan, do: Nola.Membership.touch(account, network, chan)
if !m.account do
{:ok, %IRC.Message{m | account: account}}
else
:ok
end
end
def renamed(network, old_nick, new_nick) do
if user = find_by_nick(network, old_nick) do
old_account = Nola.Account.lookup(user)
user = %User{user | nick: new_nick, nicks: [old_nick|user.nicks]}
account = Nola.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))
channels = for {channel, _} <- user.privileges, do: channel
IRC.Connection.publish_event(network, %{type: :nick, user_id: user.id, account_id: account.id, nick: new_nick, old_nick: old_nick})
end
end
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)
privs = Enum.reduce(remove, privs, fn(priv, acc) -> List.delete(acc, priv) end)
user = %User{user | privileges: Map.put(user.privileges, channel, privs)}
Storage.insert(User.to_tuple(user))
IRC.Connection.publish_event({network, channel}, %{type: :privileges, user_id: user.id, account_id: user.account, added: add, removed: remove})
end
end
# XXX: Reason
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
Nola.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))
IRC.Connection.publish_event({network, channel}, %{type: :part, user_id: user.id, account_id: user.account, reason: nil})
else
IRC.Connection.publish_event(network, %{type: :quit, user_id: user.id, account_id: user.account, reason: "Left all known channels"})
Storage.delete(user.id)
end
end
end
def quitted(sender, reason) do
if user = find_by_nick(sender.network, sender.nick) do
if user.account do
for {channel, _} <- user.privileges do
Nola.Membership.touch(user.account, sender.network, channel)
end
IRC.Connection.publish_event(sender.network, %{type: :quit, user_id: user.id, account_id: user.account, reason: reason})
end
Storage.delete(user.id)
end
end
defp touch_struct(user = %User{last_active: last_active}, channel) do
now = NaiveDateTime.utc_now()
last_active = last_active
|> Map.put(channel, now)
|> Map.put(nil, now)
%User{user | last_active: last_active}
end
defp userchans(%{privileges: privileges}) do
for({chan, _} <- privileges, do: chan)
end
end
diff --git a/lib/plugins/account.ex b/lib/plugins/account.ex
index 747fbc7..96405bb 100644
--- a/lib/plugins/account.ex
+++ b/lib/plugins/account.ex
@@ -1,188 +1,188 @@
defmodule Nola.Plugins.Account do
@moduledoc """
# Account
* **account** Get current account id and token
* **auth `<account-id>` `<token>`** Authenticate and link the current nickname to an account
* **auth** list authentications methods
* **whoami** list currently authenticated users
* **web** get a one-time login link to web
* **enable-telegram** Link a Telegram account
* **enable-sms** Link a SMS number
* **enable-untappd** Link a Untappd account
* **set-name** set account name
* **setusermeta puppet-nick `<nick>`** Set puppet IRC nickname
"""
def irc_doc, do: @moduledoc
def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
def init(_) do
{:ok, _} = Registry.register(Nola.PubSub, "messages:private", [])
{:ok, nil}
end
def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "help"}}, state) do
text = [
"account: show current account and auth token",
"auth: show authentications methods",
"whoami: list authenticated users",
"set-name <name>: set account name",
"web: login to web",
"enable-sms | disable-sms: enable/change or disable sms",
"enable-telegram: link/change telegram",
"enable-untappd: link untappd account",
"getmeta: show meta datas",
"setusermeta: set user meta",
]
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "auth"}}, state) do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
predicates = :dets.select(Nola.Account.file("predicates"), spec)
text = for {net, {key, value}} <- predicates, do: "#{net}: #{to_string(key)}: #{value}"
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "whoami"}}, state) do
- users = for user <- IRC.UserTrack.find_by_account(m.account) do
+ users = for user <- Nola.UserTrack.find_by_account(m.account) do
chans = Enum.map(user.privileges, fn({chan, _}) -> chan end)
|> Enum.join(" ")
"#{user.network} - #{user.nick}!#{user.username}@#{user.host} - #{chans}"
end
m.replyfun.(users)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "account"}}, state) do
account = Nola.Account.lookup(m.sender)
text = ["Account Id: #{account.id}",
"Authenticate to this account from another network: \"auth #{account.id} #{account.token}\" to the other bot!"]
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{sender: sender, text: "auth"<>_}}, state) do
#account = Nola.Account.lookup(m.sender)
case String.split(m.text, " ") do
["auth", id, token] ->
join_account(m, id, token)
_ ->
m.replyfun.("Invalid parameters")
end
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "set-name "<>name}}, state) do
Nola.Account.update_account_name(account, name)
m.replyfun.("Name changed: #{name}")
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "disable-sms"}}, state) do
if Nola.Account.get_meta(m.account, "sms-number") do
Nola.Account.delete_meta(m.account, "sms-number")
m.replfyun.("SMS disabled.")
else
m.replyfun.("SMS already disabled.")
end
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "web"}}, state) do
auth_url = Untappd.auth_url()
login_url = Nola.AuthToken.new_url(m.account.id, nil)
m.replyfun.("-> " <> login_url)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "enable-sms"}}, state) do
code = String.downcase(EntropyString.small_id())
Nola.Account.put_meta(m.account, "sms-validation-code", code)
Nola.Account.put_meta(m.account, "sms-validation-target", m.network)
number = Nola.IRC.Sms.my_number()
text = "To enable or change your number for SMS messaging, please send:"
<> " \"enable #{code}\" to #{number}"
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "enable-telegram"}}, state) do
code = String.downcase(EntropyString.small_id())
Nola.Account.delete_meta(m.account, "telegram-id")
Nola.Account.put_meta(m.account, "telegram-validation-code", code)
Nola.Account.put_meta(m.account, "telegram-validation-target", m.network)
text = "To enable or change your number for telegram messaging, please open #{Nola.Telegram.my_path()} and send:"
<> " \"/enable #{code}\""
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "enable-untappd"}}, state) do
auth_url = Untappd.auth_url()
login_url = Nola.AuthToken.new_url(m.account.id, {:external_redirect, auth_url})
m.replyfun.(["To link your Untappd account, open this URL:", login_url])
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "getmeta"<>_}}, state) do
result = case String.split(m.text, " ") do
["getmeta"] ->
for {k, v} <- Nola.Account.get_all_meta(m.account) do
case k do
"u:"<>key -> "(user) #{key}: #{v}"
key -> "#{key}: #{v}"
end
end
["getmeta", key] ->
value = Nola.Account.get_meta(m.account, key)
text = if value do
"#{key}: #{value}"
else
"#{key} is not defined"
end
_ ->
"usage: getmeta [key]"
end
m.replyfun.(result)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "setusermeta"<>_}}, state) do
result = case String.split(m.text, " ") do
["setusermeta", key, value] ->
Nola.Account.put_user_meta(m.account, key, value)
"ok"
_ ->
"usage: setusermeta <key> <value>"
end
m.replyfun.(result)
{:noreply, state}
end
def handle_info(_, state) do
{:noreply, state}
end
defp join_account(m, id, token) do
old_account = Nola.Account.lookup(m.sender)
new_account = Nola.Account.get(id)
if new_account && token == new_account.token do
case Nola.Account.merge_account(old_account.id, new_account.id) do
:ok ->
if old_account.id == new_account.id do
m.replyfun.("Already authenticated, but hello")
else
m.replyfun.("Accounts merged!")
end
_ -> m.replyfun.("Something failed :(")
end
else
m.replyfun.("Invalid token")
end
end
end
diff --git a/lib/plugins/alcoolog.ex b/lib/plugins/alcoolog.ex
index 738be71..cc56c4f 100644
--- a/lib/plugins/alcoolog.ex
+++ b/lib/plugins/alcoolog.ex
@@ -1,1229 +1,1229 @@
defmodule Nola.Plugins.Alcoolog do
require Logger
@moduledoc """
# [alcoolog]({{context_path}}/alcoolog)
* **!santai `<cl | (calc)>` `<degrés d'alcool> [annotation]`**: enregistre un nouveau verre de `montant` d'une boisson à `degrés d'alcool`.
* **!santai `<cl | (calc)>` `<beer name>`**: enregistre un nouveau verre de `cl` de la bière `beer name`, et checkin sur Untappd.com.
* **!moar `[cl]` : enregistre un verre équivalent au dernier !santai.
* **-santai**: annule la dernière entrée d'alcoolisme.
* **.alcoolisme**: état du channel en temps réel.
* **.alcoolisme `<semaine | Xj>`**: points par jour, sur X j.
* **!alcoolisme `[pseudo]`**: affiche les points d'alcoolisme.
* **!alcoolisme `[pseudo]` `<semaine | Xj>`**: affiche les points d'alcoolisme par jour sur X j.
* **+alcoolisme `<h|f>` `<poids en kg>` `[facteur de perte en mg/l (10, 15, 20, 25)]`**: Configure votre profil d'alcoolisme.
* **.sobre**: affiche quand la sobriété frappera sur le chan.
* **!sobre `[pseudo]`**: affiche quand la sobriété frappera pour `[pseudo]`.
* **!sobrepour `<date>`**: affiche tu pourras être sobre pour `<date>`, et si oui, combien de volumes d'alcool peuvent encore être consommés.
* **!alcoolog**: ([voir]({{context_path}}/alcoolog)) lien pour voir l'état/statistiques et historique de l'alcoolémie du channel.
* **!alcool `<cl>` `<degrés>`**: donne le nombre d'unités d'alcool dans `<cl>` à `<degrés>°`.
* **!soif**: c'est quand l'apéro ?
1 point = 1 volume d'alcool.
Annotation: champ libre!
---
## `!txt`s
* status utilisateur: `alcoolog.user_(sober|legal|legalhigh|high|toohigh|sick)(|_rising)`
* mauvaises boissons: `alcoolog.drink_(negative|zero|negative)`
* santo: `alcoolog.santo`
* santai: `alcoolog.santai`
* plus gros, moins gros: `alcoolog.(fatter|thinner)`
"""
def irc_doc, do: @moduledoc
def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
# tuple dets: {nick, date, volumes, current_alcohol_level, nom, commentaire}
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
# tuple meta dets: {nick, map}
# %{:weight => float, :sex => true(h),false(f)}
@pubsub ~w(account)
@pubsub_triggers ~w(santai moar again bis santo santeau alcoolog sobre sobrepour soif alcoolisme alcool)
@default_user_meta %{weight: 77.4, sex: true, loss_factor: 15}
def data_state() do
dets_filename = (Nola.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
dets_meta_filename = (Nola.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
%{dets: dets_filename, meta: dets_meta_filename, ets: __MODULE__.ETS}
end
def init(_) do
triggers = for(t <- @pubsub_triggers, do: "trigger:"<>t)
for sub <- @pubsub ++ triggers do
{:ok, _} = Registry.register(Nola.PubSub, sub, plugin: __MODULE__)
end
dets_filename = (Nola.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
ets = :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
dets_meta_filename = (Nola.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
{:ok, meta} = :dets.open_file(dets_meta_filename, [{:type,:set}])
traverse_fun = fn(obj, dets) ->
case obj do
object = {nick, naive = %NaiveDateTime{}, volumes, active, cl, deg, name, comment} ->
date = naive
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_unix()
new = {nick, date, volumes, active, cl, deg, name, comment, Map.new()}
:dets.delete_object(dets, object)
:dets.insert(dets, new)
:ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, Map.new()})
dets
object = {nick, naive = %NaiveDateTime{}, volumes, active, cl, deg, name, comment, meta} ->
date = naive
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_unix()
new = {nick, date, volumes, active, cl, deg, name, comment, Map.new()}
:dets.delete_object(dets, object)
:dets.insert(dets, new)
:ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, Map.new()})
dets
object = {nick, date, volumes, active, cl, deg, name, comment, meta} ->
:ets.insert(ets, {{nick, date}, volumes, active, cl, deg, name, comment, meta})
dets
_ ->
dets
end
end
:dets.foldl(traverse_fun, dets, dets)
:dets.sync(dets)
state = %{dets: dets, meta: meta, ets: ets}
{:ok, state}
end
@eau ["santo", "santeau"]
def handle_info({:irc, :trigger, santeau, m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) when santeau in @eau do
Nola.Plugins.Txt.reply_random(m, "alcoolog.santo")
{:noreply, state}
end
def handle_info({:irc, :trigger, "soif", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do
now = DateTime.utc_now()
|> Timex.Timezone.convert("Europe/Paris")
apero = format_duration_from_now(%DateTime{now | hour: 18, minute: 0, second: 0}, false)
day_of_week = Date.day_of_week(now)
{txt, apero?} = cond do
now.hour >= 0 && now.hour < 6 ->
{["apéro tardif ? Je dis OUI ! SANTAI !"], true}
now.hour >= 6 && now.hour < 12 ->
if day_of_week >= 6 do
{["de l'alcool pour le petit dej ? le week-end, pas de problème !"], true}
else
{["C'est quand même un peu tôt non ? Prochain apéro #{apero}"], false}
end
now.hour >= 12 && (now.hour < 14) ->
{["oui! c'est l'apéro de midi! (et apéro #{apero})",
"tu peux attendre #{apero} ou y aller, il est midi !"
], true}
now.hour == 17 ->
{[
"ÇA APPROCHE !!! Apéro #{apero}",
"BIENTÔT !!! Apéro #{apero}",
"achetez vite les teilles, apéro dans #{apero}!",
"préparez les teilles, apéro dans #{apero}!"
], false}
now.hour >= 14 && now.hour < 18 ->
weekend = if day_of_week >= 6 do
" ... ou maintenant en fait, c'est le week-end!"
else
""
end
{["tiens bon! apéro #{apero}#{weekend}",
"courage... apéro dans #{apero}#{weekend}",
"pas encore :'( apéro dans #{apero}#{weekend}"
], false}
true ->
{[
"C'EST L'HEURE DE L'APÉRO !!! SANTAIIIIIIIIIIII !!!!"
], true}
end
txt = txt
|> Enum.shuffle()
|> Enum.random()
m.replyfun.(txt)
stats = get_full_statistics(state, m.account.id)
if !apero? && stats.active > 0.1 do
m.replyfun.("(... ou continue en fait, je suis pas ta mère !)")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "sobrepour", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
args = Enum.join(args, " ")
{:ok, now} = DateTime.now("Europe/Paris", Tzdata.TimeZoneDatabase)
time = case args do
"demain " <> time ->
{h, m} = case String.split(time, [":", "h"]) do
[hour, ""] ->
IO.puts ("h #{inspect hour}")
{h, _} = Integer.parse(hour)
{h, 0}
[hour, min] when min != "" ->
{h, _} = Integer.parse(hour)
{m, _} = Integer.parse(min)
{h, m}
[hour] ->
IO.puts ("h #{inspect hour}")
{h, _} = Integer.parse(hour)
{h, 0}
_ -> {0, 0}
end
secs = ((60*60)*24)
day = DateTime.add(now, secs, :second, Tzdata.TimeZoneDatabase)
%DateTime{day | hour: h, minute: m, second: 0}
"après demain " <> time ->
secs = 2*((60*60)*24)
DateTime.add(now, secs, :second, Tzdata.TimeZoneDatabase)
datetime ->
case Timex.Parse.DateTime.Parser.parse(datetime, "{}") do
{:ok, dt} -> dt
_ -> nil
end
end
if time do
meta = get_user_meta(state, m.account.id)
stats = get_full_statistics(state, m.account.id)
duration = round(DateTime.diff(time, now)/60.0)
IO.puts "diff #{inspect duration} sober in #{inspect stats.sober_in}"
if duration < stats.sober_in do
int = stats.sober_in - duration
m.replyfun.("désolé, aucune chance! tu seras sobre #{format_minute_duration(int)} après!")
else
remaining = duration - stats.sober_in
if remaining < 30 do
m.replyfun.("moins de 30 minutes de sobriété, c'est impossible de boire plus")
else
loss_per_minute = ((meta.loss_factor/100)/60)
remaining_gl = (remaining-30)*loss_per_minute
m.replyfun.("marge de boisson: #{inspect remaining} minutes, #{remaining_gl} g/l")
end
end
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolog", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do
{:ok, token} = Nola.Token.new({:alcoolog, :index, m.sender.network, m.channel})
url = NolaWeb.Router.Helpers.alcoolog_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel), token)
m.replyfun.("-> #{url}")
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolog", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :bang}}}, state) do
url = NolaWeb.Router.Helpers.alcoolog_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel))
m.replyfun.("-> #{url}")
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcool", m = %IRC.Message{trigger: %IRC.Trigger{args: args = [cl, deg], type: :bang}}}, state) do
{cl, _} = Util.float_paparse(cl)
{deg, _} = Util.float_paparse(deg)
points = Alcool.units(cl, deg)
meta = get_user_meta(state, m.account.id)
k = if meta.sex, do: 0.7, else: 0.6
weight = meta.weight
gl = (10*points)/(k*weight)
duration = round(gl/((meta.loss_factor/100)/60))+30
sober_in_s = if duration > 0 do
duration = Timex.Duration.from_minutes(duration)
Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
else
""
end
m.replyfun.("Il y a #{Float.round(points+0.0, 4)} unités d'alcool dans #{cl}cl à #{deg}° (#{Float.round(gl + 0.0, 4)} g/l, #{sober_in_s})")
{:noreply, state}
end
def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: [cl, deg | comment], type: :bang}}}, state) do
santai(m, state, cl, deg, comment)
{:noreply, state}
end
@moar [
"{{message.sender.nick}}: la même donc ?",
"{{message.sender.nick}}: et voilà la petite sœur !"
]
def handle_info({:irc, :trigger, "bis", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
handle_info({:irc, :trigger, "moar", m}, state)
end
def handle_info({:irc, :trigger, "again", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
handle_info({:irc, :trigger, "moar", m}, state)
end
def handle_info({:irc, :trigger, "moar", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
case get_statistics_for_nick(state, m.account.id) do
{_, obj = {_, _date, _points, _active, cl, deg, _name, comment, _meta}} ->
cl = case args do
[cls] ->
case Util.float_paparse(cls) do
{cl, _} -> cl
_ -> cl
end
_ -> cl
end
moar = @moar |> Enum.shuffle() |> Enum.random() |> Tmpl.render(m) |> m.replyfun.()
santai(m, state, cl, deg, comment, auto_set: true)
{_, obj = {_, date, points, _last_active, type, descr}} ->
case Regex.named_captures(~r/^(?<cl>\d+[.]\d+)cl\s+(?<deg>\d+[.]\d+)°$/, type) do
nil -> m.replyfun.("suce")
u ->
moar = @moar |> Enum.shuffle() |> Enum.random() |> Tmpl.render(m) |> m.replyfun.()
santai(m, state, u["cl"], u["deg"], descr, auto_set: true)
end
_ -> nil
end
{:noreply, state}
end
defp santai(m, state, cl, deg, comment, options \\ []) do
comment = cond do
comment == [] -> nil
is_binary(comment) -> comment
comment == nil -> nil
true -> Enum.join(comment, " ")
end
{cl, cl_extra} = case {Util.float_paparse(cl), cl} do
{{cl, extra}, _} -> {cl, extra}
{:error, "("<>_} ->
try do
{:ok, result} = Abacus.eval(cl)
{result, nil}
rescue
_ -> {nil, "cl: invalid calc expression"}
end
{:error, _} -> {nil, "cl: invalid value"}
end
{deg, comment, auto_set, beer_id} = case Util.float_paparse(deg) do
{deg, _} -> {deg, comment, Keyword.get(options, :auto_set, false), nil}
:error ->
beername = if(comment, do: "#{deg} #{comment}", else: deg)
case Untappd.search_beer(beername, limit: 1) do
{:ok, %{"response" => %{"beers" => %{"count" => count, "items" => [%{"beer" => beer, "brewery" => brewery} | _]}}}} ->
{Map.get(beer, "beer_abv"), "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")}", true, Map.get(beer, "bid")}
_ ->
{deg, "could not find beer", false, nil}
end
end
cond do
cl == nil -> m.replyfun.(cl_extra)
deg == nil -> m.replyfun.(comment)
cl >= 500 || deg >= 100 -> Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_toohuge")
cl == 0 || deg == 0 -> Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_zero")
cl < 0 || deg < 0 -> Nola.Plugins.Txt.reply_random(m, "alcoolog.drink_negative")
true ->
points = Alcool.units(cl, deg)
now = m.at || DateTime.utc_now()
|> DateTime.to_unix(:millisecond)
user_meta = get_user_meta(state, m.account.id)
name = "#{cl}cl #{deg}°"
old_stats = get_full_statistics(state, m.account.id)
meta = %{}
meta = Map.put(meta, "timestamp", now)
meta = Map.put(meta, "weight", user_meta.weight)
meta = Map.put(meta, "sex", user_meta.sex)
:ok = :dets.insert(state.dets, {m.account.id, now, points, if(old_stats, do: old_stats.active, else: 0), cl, deg, name, comment, meta})
true = :ets.insert(state.ets, {{m.account.id, now}, points, if(old_stats, do: old_stats.active, else: 0),cl, deg, name, comment, meta})
#sante = @santai |> Enum.map(fn(s) -> String.trim(String.upcase(s)) end) |> Enum.shuffle() |> Enum.random()
sante = Nola.Plugins.Txt.random("alcoolog.santai")
k = if user_meta.sex, do: 0.7, else: 0.6
weight = user_meta.weight
peak = Float.round((10*points||0.0)/(k*weight), 4)
stats = get_full_statistics(state, m.account.id)
sober_add = if old_stats && Map.get(old_stats || %{}, :sober_in) do
mins = round(stats.sober_in - old_stats.sober_in)
" [+#{mins}m]"
else
""
end
nonow = DateTime.utc_now()
sober = nonow |> DateTime.add(round(stats.sober_in*60), :second)
|> Timex.Timezone.convert("Europe/Paris")
at = if nonow.day == sober.day do
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
detail
else
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
detail
end
up = if stats.active_drinks > 1 do
" " <> Enum.join(for(_ <- 1..stats.active_drinks, do: "▲")) <> ""
else
""
end
since_str = if stats.since && stats.since_min > 180 do
"(depuis: #{stats.since_s}) "
else
""
end
msg = fn(nick, extra) ->
"#{sante} #{nick} #{extra}#{up} #{format_points(points)} @#{stats.active}g/l [+#{peak} g/l]"
<> " (15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) #{since_str}(sobriété #{at} (dans #{stats.sober_in_s})#{sober_add}) !"
<> " (aujourd'hui #{stats.daily_volumes} points - #{stats.daily_gl} g/l)"
end
meta = if beer_id do
Map.put(meta, "untappd:beer_id", beer_id)
else
meta
end
if beer_id do
spawn(fn() ->
case Untappd.maybe_checkin(m.account, beer_id) do
{:ok, body} ->
badges = get_in(body, ["badges", "items"])
if badges != [] do
badges_s = Enum.map(badges, fn(badge) -> Map.get(badge, "badge_name") end)
|> Enum.filter(fn(b) -> b end)
|> Enum.intersperse(", ")
|> Enum.join("")
badge = if(length(badges) > 1, do: "badges", else: "badge")
m.replyfun.("\\O/ Unlocked untappd #{badge}: #{badges_s}")
end
:ok
{:error, {:http_error, error}} when is_integer(error) -> m.replyfun.("Checkin to Untappd failed: #{to_string(error)}")
{:error, {:http_error, error}} -> m.replyfun.("Checkin to Untappd failed: #{inspect error}")
_ -> :error
end
end)
end
local_extra = if auto_set do
if comment do
" #{comment} (#{cl}cl @ #{deg}°)"
else
"#{cl}cl @ #{deg}°"
end
else
""
end
m.replyfun.(msg.(m.sender.nick, local_extra))
notify = Nola.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
for {net, chan} <- notify do
- user = IRC.UserTrack.find_by_account(net, m.account)
+ user = Nola.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
extra = " " <> present_type(name, comment) <> ""
IRC.Connection.broadcast_message(net, chan, msg.(nick, extra))
end
miss = cond do
points <= 0.6 -> :small
stats.active30m >= 2.9 && stats.active30m < 3 -> :miss3
stats.active30m >= 1.9 && stats.active30m < 2 -> :miss2
stats.active30m >= 0.9 && stats.active30m < 1 -> :miss1
stats.active30m >= 0.45 && stats.active30m < 0.5 -> :miss05
stats.active30m >= 0.20 && stats.active30m < 0.20 -> :miss025
stats.active30m >= 3 && stats.active1h < 3.15 -> :small3
stats.active30m >= 2 && stats.active1h < 2.15 -> :small2
stats.active30m >= 1.5 && stats.active1h < 1.5 -> :small15
stats.active30m >= 1 && stats.active1h < 1.15 -> :small1
stats.active30m >= 0.5 && stats.active1h <= 0.51 -> :small05
stats.active30m >= 0.25 && stats.active30m <= 0.255 -> :small025
true -> nil
end
if miss do
miss = Nola.Plugins.Txt.random("alcoolog.#{to_string(miss)}")
if miss do
for {net, chan} <- Nola.Membership.notify_channels(m.account) do
- user = IRC.UserTrack.find_by_account(net, m.account)
+ user = Nola.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
IRC.Connection.broadcast_message(net, chan, "#{nick}: #{miss}")
end
end
end
end
end
def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do
m.replyfun.("!santai <cl> <degrés> [commentaire]")
{:noreply, state}
end
def get_all_stats() do
Nola.Account.all_accounts()
|> Enum.map(fn(account) -> {account.id, get_full_statistics(account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
|> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
end
def get_channel_statistics(account, network, nil) do
Nola.Membership.expanded_members_or_friends(account, network, nil)
|> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
|> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
end
def get_channel_statistics(_, network, channel), do: get_channel_statistics(network, channel)
def get_channel_statistics(network, channel) do
Nola.Membership.expanded_members(network, channel)
|> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
|> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
end
@spec since() :: %{Nola.Account.id() => DateTime.t()}
@doc "Returns the last time the user was at 0 g/l"
def since() do
:ets.foldr(fn({{acct, timestamp_or_date}, _vol, current, _cl, _deg, _name, _comment, _m}, acc) ->
if !Map.get(acc, acct) && current == 0 do
date = Util.to_date_time(timestamp_or_date)
Map.put(acc, acct, date)
else
acc
end
end, %{}, __MODULE__.ETS)
end
def get_full_statistics(nick) do
get_full_statistics(data_state(), nick)
end
defp get_full_statistics(state, nick) do
case get_statistics_for_nick(state, nick) do
{count, {_, last_at, last_points, last_active, last_cl, last_deg, last_type, last_descr, _meta}} ->
{active, active_drinks} = current_alcohol_level(state, nick)
{_, m30} = alcohol_level_rising(state, nick)
{rising, m15} = alcohol_level_rising(state, nick, 15)
{_, m5} = alcohol_level_rising(state, nick, 5)
{_, h1} = alcohol_level_rising(state, nick, 60)
trend = if rising do
"▲"
else
"▼"
end
user_state = cond do
active <= 0.0 -> :sober
active <= 0.25 -> :low
active <= 0.50 -> :legal
active <= 1.0 -> :legalhigh
active <= 2.5 -> :high
active < 3 -> :toohigh
true -> :sick
end
rising_file_key = if rising, do: "_rising", else: ""
txt_file = "alcoolog." <> "user_" <> to_string(user_state) <> rising_file_key
user_status = Nola.Plugins.Txt.random(txt_file)
meta = get_user_meta(state, nick)
minutes_til_sober = h1/((meta.loss_factor/100)/60)
minutes_til_sober = cond do
active < 0 -> 0
m15 < 0 -> 15
m30 < 0 -> 30
h1 < 0 -> 60
minutes_til_sober > 0 ->
Float.round(minutes_til_sober+60)
true -> 0
end
duration = Timex.Duration.from_minutes(minutes_til_sober)
sober_in_s = if minutes_til_sober > 0 do
Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
else
nil
end
since = if active > 0 do
since()
|> Map.get(nick)
end
since_diff = if since, do: Timex.diff(DateTime.utc_now(), since, :minutes)
since_duration = if since, do: Timex.Duration.from_minutes(since_diff)
since_s = if since, do: Timex.Format.Duration.Formatter.lformat(since_duration, "fr", :humanized)
{total_volumes, total_gl} = user_stats(state, nick)
%{active: active, last_at: last_at, last_cl: last_cl, last_deg: last_deg, last_points: last_points, last_type: last_type, last_descr: last_descr,
trend_symbol: trend,
active5m: m5, active15m: m15, active30m: m30, active1h: h1,
rising: rising,
active_drinks: active_drinks,
user_status: user_status,
daily_gl: total_gl, daily_volumes: total_volumes,
sober_in: minutes_til_sober, sober_in_s: sober_in_s,
since: since, since_min: since_diff, since_s: since_s,
}
_ ->
nil
end
end
def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :dot}}}, state) do
nicks = Nola.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
|> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(state, account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && status.sober_in && status.sober_in > 0 end)
|> Enum.sort_by(fn({_, status}) -> status.sober_in end, &</2)
|> Enum.map(fn({nick, stats}) ->
now = DateTime.utc_now()
sober = now |> DateTime.add(round(stats.sober_in*60), :second)
|> Timex.Timezone.convert("Europe/Paris")
at = if now.day == sober.day do
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
detail
else
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
detail
end
"#{nick} sobre #{at} (dans #{stats.sober_in_s})"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
|> (fn(line) ->
case line do
"" -> "tout le monde est sobre......."
line -> line
end
end).()
|> m.replyfun.()
{:noreply, state}
end
def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
account = case args do
[nick] -> Nola.Account.find_always_by_nick(m.network, m.channel, nick)
[] -> m.account
end
if account do
- user = IRC.UserTrack.find_by_account(m.network, account)
+ user = Nola.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
stats = get_full_statistics(state, account.id)
if stats && stats.sober_in > 0 do
now = DateTime.utc_now()
sober = now |> DateTime.add(round(stats.sober_in*60), :second)
|> Timex.Timezone.convert("Europe/Paris")
at = if now.day == sober.day do
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
detail
else
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
detail
end
m.replyfun.("#{nick} sera sobre #{at} (dans #{stats.sober_in_s})!")
else
m.replyfun.("#{nick} est déjà sobre. aidez le !")
end
else
m.replyfun.("inconnu")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :dot}}}, state) do
nicks = Nola.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
|> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(state, account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
|> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
|> Enum.map(fn({nick, status}) ->
trend_symbol = if status.active_drinks > 1 do
Enum.join(for(_ <- 1..status.active_drinks, do: status.trend_symbol))
else
status.trend_symbol
end
since_str = if status.since_min > 180 do
"depuis: #{status.since_s} | "
else
""
end
"#{nick} #{status.user_status} #{trend_symbol} #{Float.round(status.active, 4)} g/l [#{since_str}sobre dans: #{status.sober_in_s}]"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
msg = if nicks == "" do
"wtf?!?! personne n'a bu!"
else
nicks
end
m.replyfun.(msg)
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [time], type: :dot}}}, state) do
time = case time do
"semaine" -> 7
string ->
case Integer.parse(string) do
{time, "j"} -> time
{time, "J"} -> time
_ -> nil
end
end
if time do
aday = time*((24 * 60)*60)
now = DateTime.utc_now()
before = now
|> DateTime.add(-aday, :second)
|> DateTime.to_unix(:millisecond)
over_time_stats(before, time, m, state)
else
m.replyfun.(".alcooolisme semaine|Xj")
end
{:noreply, state}
end
def user_over_time(account, count) do
user_over_time(data_state(), account, count)
end
def user_over_time(state, account, count) do
delay = count*((24 * 60)*60)
now = DateTime.utc_now()
before = DateTime.utc_now()
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
|> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
|> DateTime.to_unix(:millisecond)
#[
# {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
# [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]}
#]
match = [{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
[{:andalso, {:>, :"$2", {:const, before}}, {:==, :"$1", {:const, account.id}}}], [:"$_"]}
]
:ets.select(state.ets, match)
|> Enum.reduce(Map.new, fn({{_, ts}, vol, _, _, _, _, _, _}, acc) ->
date = DateTime.from_unix!(ts, :millisecond)
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
date = if date.hour <= 8 do
DateTime.add(date, -(60*(60*(date.hour+1))), :second, Tzdata.TimeZoneDatabase)
else
date
end
|> DateTime.to_date()
Map.put(acc, date, Map.get(acc, date, 0) + vol)
end)
end
def user_over_time_gl(account, count) do
state = data_state()
meta = get_user_meta(state, account.id)
delay = count*((24 * 60)*60)
now = DateTime.utc_now()
before = DateTime.utc_now()
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
|> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
|> DateTime.to_unix(:millisecond)
#[
# {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
# [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]}
#]
match = [{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
[{:andalso, {:>, :"$2", {:const, before}}, {:==, :"$1", {:const, account.id}}}], [:"$_"]}
]
:ets.select(state.ets, match)
|> Enum.reduce(Map.new, fn({{_, ts}, vol, _, _, _, _, _, _}, acc) ->
date = DateTime.from_unix!(ts, :millisecond)
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
date = if date.hour <= 8 do
DateTime.add(date, -(60*(60*(date.hour+1))), :second, Tzdata.TimeZoneDatabase)
else
date
end
|> DateTime.to_date()
weight = meta.weight
k = if meta.sex, do: 0.7, else: 0.6
gl = (10*vol)/(k*weight)
Map.put(acc, date, Map.get(acc, date, 0) + gl)
end)
end
defp over_time_stats(before, j, m, state) do
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _, _, _, _}) when date > before -> obj end)
match = [{{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_},
[{:>, :"$1", {:const, before}}], [:"$_"]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
members = Nola.Membership.members_or_friends(m.account, m.network, m.channel)
drinks = :ets.select(state.ets, match)
|> Enum.filter(fn({{account, _}, _, _, _, _, _, _, _}) -> Enum.member?(members, account) end)
|> Enum.sort_by(fn({{_, ts}, _, _, _, _, _, _, _}) -> ts end, &>/2)
top = Enum.reduce(drinks, %{}, fn({{nick, _}, vol, _, _, _, _, _, _}, acc) ->
all = Map.get(acc, nick, 0)
Map.put(acc, nick, all + vol)
end)
|> Enum.sort_by(fn({_nick, count}) -> count end, &>/2)
|> Enum.map(fn({nick, count}) ->
account = Nola.Account.get(nick)
- user = IRC.UserTrack.find_by_account(m.network, account)
+ user = Nola.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
"#{nick}: #{Float.round(count, 4)}"
end)
|> Enum.intersperse(", ")
m.replyfun.("sur #{j} jours: #{top}")
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do
meta = get_user_meta(state, m.account.id)
hf = if meta.sex, do: "h", else: "f"
m.replyfun.("+alcoolisme sexe: #{hf} poids: #{meta.weight} facteur de perte: #{meta.loss_factor}")
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [h, weight | rest], type: :plus}}}, state) do
h = case h do
"h" -> true
"f" -> false
_ -> nil
end
weight = case Util.float_paparse(weight) do
{weight, _} -> weight
_ -> nil
end
{factor} = case rest do
[factor] ->
case Util.float_paparse(factor) do
{float, _} -> {float}
_ -> {@default_user_meta.loss_factor}
end
_ -> {@default_user_meta.loss_factor}
end
if h == nil || weight == nil do
m.replyfun.("paramètres invalides")
else
old_meta = get_user_meta(state, m.account.id)
meta = Map.merge(@default_user_meta, %{sex: h, weight: weight, loss_factor: factor})
put_user_meta(state, m.account.id, meta)
cond do
old_meta.weight < meta.weight ->
Nola.Plugins.Txt.reply_random(m, "alcoolog.fatter")
old_meta.weight == meta.weight ->
m.replyfun.("aucun changement!")
true ->
Nola.Plugins.Txt.reply_random(m, "alcoolog.thinner")
end
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :minus}}}, state) do
case get_statistics_for_nick(state, m.account.id) do
{_, obj = {_, date, points, _last_active, _cl, _deg, type, descr, _meta}} ->
:dets.delete_object(state.dets, obj)
:ets.delete(state.ets, {m.account.id, date})
m.replyfun.("supprimé: #{m.sender.nick} #{points} #{type} #{descr}")
Nola.Plugins.Txt.reply_random(m, "alcoolog.delete")
notify = Nola.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
for {net, chan} <- notify do
- user = IRC.UserTrack.find_by_account(net, m.account)
+ user = Nola.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
IRC.Connection.broadcast_message(net, chan, "#{nick} -santai #{points} #{type} #{descr}")
end
{:noreply, state}
_ ->
{:noreply, state}
end
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
{account, duration} = case args do
[nick | rest] -> {Nola.Account.find_always_by_nick(m.network, m.channel, nick), rest}
[] -> {m.account, []}
end
if account do
duration = case duration do
["semaine"] -> 7
[j] ->
case Integer.parse(j) do
{j, "j"} -> j
_ -> nil
end
_ -> nil
end
- user = IRC.UserTrack.find_by_account(m.network, account)
+ user = Nola.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
if duration do
if duration > 90 do
m.replyfun.("trop gros, ça rentrera pas")
else
# duration stats
stats = user_over_time(state, account, duration)
|> Enum.sort_by(fn({k,_v}) -> k end, {:asc, Date})
|> Enum.map(fn({date, count}) ->
"#{date.day}: #{Float.round(count, 2)}"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
if stats == "" do
m.replyfun.("alcoolisme a zéro sur #{duration}j :/")
else
m.replyfun.("alcoolisme de #{nick}, #{duration} derniers jours: #{stats}")
end
end
else
if stats = get_full_statistics(state, account.id) do
trend_symbol = if stats.active_drinks > 1 do
Enum.join(for(_ <- 1..stats.active_drinks, do: stats.trend_symbol))
else
stats.trend_symbol
end
# TODO: Lookup nick for account_id
msg = "#{nick} #{stats.user_status} "
<> (if stats.active > 0 || stats.active15m > 0 || stats.active30m > 0 || stats.active1h > 0, do: ": #{trend_symbol} #{Float.round(stats.active, 4)}g/l ", else: "")
<> (if stats.active30m > 0 || stats.active1h > 0, do: "(15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) ", else: "")
<> (if stats.sober_in > 0, do: "— Sobre dans #{stats.sober_in_s} ", else: "")
<> (if stats.since && stats.since_min > 180, do: "— Paitai depuis #{stats.since_s} ", else: "")
<> "— Dernier verre: #{present_type(stats.last_type, stats.last_descr)} [#{Float.round(stats.last_points+0.0, 4)}] "
<> "#{format_duration_from_now(stats.last_at)} "
<> (if stats.daily_volumes > 0, do: "— Aujourd'hui: #{stats.daily_volumes} #{stats.daily_gl}g/l", else: "")
m.replyfun.(msg)
else
m.replyfun.("honteux mais #{nick} n'a pas l'air alcoolique du tout. /kick")
end
end
else
m.replyfun.("je ne connais pas cet utilisateur")
end
{:noreply, state}
end
# Account merge
def handle_info({:account_change, old_id, new_id}, state) do
spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
Logger.debug("alcolog/account_change:: merging #{old_id} -> #{new_id}")
rename_object_owner(table, state.ets, obj, old_id, new_id)
end)
case :dets.lookup(state.meta, {:meta, old_id}) do
[{_, meta}] ->
:dets.delete(state.meta, {:meta, old_id})
:dets.insert(state.meta, {{:meta, new_id}, meta})
_ ->
:ok
end
{:noreply, state}
end
def terminate(_, state) do
for dets <- [state.dets, state.meta] do
:dets.sync(dets)
:dets.close(dets)
end
end
defp rename_object_owner(table, ets, object = {old_id, date, volume, current, cl, deg, name, comment, meta}, old_id, new_id) do
:dets.delete_object(table, object)
:ets.delete(ets, {old_id, date})
:dets.insert(table, {new_id, date, volume, current, cl, deg, name, comment, meta})
:ets.insert(ets, {{new_id, date}, volume, current, cl, deg, name, comment, meta})
end
# Account: move from nick to account id
def handle_info({:accounts, accounts}, state) do
#for x={:account, _, _, _, _} <- accounts, do: handle_info(x, state)
#{:noreply, state}
mapping = Enum.reduce(accounts, Map.new, fn({:account, _net, _chan, nick, account_id}, acc) ->
Map.put(acc, String.downcase(nick), account_id)
end)
spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [], [:"$_"]}]
Logger.debug("accounts:: mappings #{inspect mapping}")
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj = {nick, _date, _vol, _cur, _cl, _deg, _name, _comment, _meta}) ->
#Logger.debug("accounts:: item #{inspect(obj)}")
if new_id = Map.get(mapping, nick) do
Logger.debug("alcolog/accounts:: merging #{nick} -> #{new_id}")
rename_object_owner(table, state.ets, obj, nick, new_id)
end
end)
{:noreply, state}
end
def handle_info({:account, _net, _chan, nick, account_id}, state) do
nick = String.downcase(nick)
spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
Logger.debug("alcoolog/account:: merging #{nick} -> #{account_id}")
rename_object_owner(table, state.ets, obj, nick, account_id)
end)
case :dets.lookup(state.meta, {:meta, nick}) do
[{_, meta}] ->
:dets.delete(state.meta, {:meta, nick})
:dets.insert(state.meta, {{:meta, account_id}, meta})
_ ->
:ok
end
{:noreply, state}
end
def handle_info(t, state) do
Logger.debug("#{__MODULE__}: unhandled info #{inspect t}")
{:noreply, state}
end
def nick_history(account) do
spec = [
{{{:"$1", :_}, :_, :_, :_, :_, :_, :_, :_},
[{:==, :"$1", {:const, account.id}}],
[:"$_"]}
]
:ets.select(data_state().ets, spec)
end
defp get_statistics_for_nick(state, account_id) do
qvc = :dets.lookup(state.dets, account_id)
|> Enum.sort_by(fn({_, ts, _, _, _, _, _, _, _}) -> ts end, &</2)
count = Enum.reduce(qvc, 0, fn({_nick, _ts, points, _active, _cl, _deg, _type, _descr, _meta}, acc) -> acc + (points||0) end)
last = List.last(qvc) || nil
{count, last}
end
def present_type(type, descr) when descr in [nil, ""], do: "#{type}"
def present_type(type, description), do: "#{type} (#{description})"
def format_points(int) when is_integer(int) and int > 0 do
"+#{Integer.to_string(int)}"
end
def format_points(int) when is_integer(int) and int < 0 do
Integer.to_string(int)
end
def format_points(int) when is_float(int) and int > 0 do
"+#{Float.to_string(Float.round(int,4))}"
end
def format_points(int) when is_float(int) and int < 0 do
Float.to_string(Float.round(int,4))
end
def format_points(0), do: "0"
def format_points(0.0), do: "0"
defp format_relative_timestamp(timestamp) do
alias Timex.Format.DateTime.Formatters
alias Timex.Timezone
date = timestamp
|> DateTime.from_unix!(:millisecond)
|> Timezone.convert("Europe/Paris")
{:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
{:ok, detail} = Formatters.Default.lformat(date, " ({h24}:{m})", "fr")
relative <> detail
end
defp put_user_meta(state, account_id, meta) do
:dets.insert(state.meta, {{:meta, account_id}, meta})
:ok
end
defp get_user_meta(%{meta: meta}, account_id) do
case :dets.lookup(meta, {:meta, account_id}) do
[{{:meta, _}, meta}] ->
Map.merge(@default_user_meta, meta)
_ ->
@default_user_meta
end
end
# Calcul g/l actuel:
# 1. load user meta
# 2. foldr ets
# for each object
# get_current_alcohol
# ((object g/l) - 0,15/l/60)* minutes_since_drink
# if minutes_since_drink < 10, reduce g/l (?!)
# acc + current_alcohol
# stop folding when ?
#
def user_stats(account) do
user_stats(data_state(), account.id)
end
defp user_stats(state = %{ets: ets}, account_id) do
meta = get_user_meta(state, account_id)
aday = (10 * 60)*60
now = DateTime.utc_now()
before = now
|> DateTime.add(-aday, :second)
|> DateTime.to_unix(:millisecond)
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
match = [
{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
[
{:>, :"$2", {:const, before}},
{:"=:=", {:const, account_id}, :"$1"}
], [:"$_"]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
drinks = :ets.select(ets, match)
# {date, single_peak}
total_volume = Enum.reduce(drinks, 0.0, fn({{_, date}, volume, _, _, _, _, _, _}, acc) ->
acc + volume
end)
k = if meta.sex, do: 0.7, else: 0.6
weight = meta.weight
gl = (10*total_volume)/(k*weight)
{Float.round(total_volume + 0.0, 4), Float.round(gl + 0.0, 4)}
end
defp alcohol_level_rising(state, account_id, minutes \\ 30) do
{now, _} = current_alcohol_level(state, account_id)
soon_date = DateTime.utc_now
|> DateTime.add(minutes*60, :second)
{soon, _} = current_alcohol_level(state, account_id, soon_date)
soon = cond do
soon < 0 -> 0.0
true -> soon
end
#IO.puts "soon #{soon_date} - #{inspect soon} #{inspect now}"
{soon > now, Float.round(soon+0.0, 4)}
end
defp current_alcohol_level(state = %{ets: ets}, account_id, now \\ nil) do
meta = get_user_meta(state, account_id)
aday = ((24*7) * 60)*60
now = if now do
now
else
DateTime.utc_now()
end
before = now
|> DateTime.add(-aday, :second)
|> DateTime.to_unix(:millisecond)
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
match = [
{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
[
{:>, :"$2", {:const, before}},
{:"=:=", {:const, account_id}, :"$1"}
], [:"$_"]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
drinks = :ets.select(ets, match)
|> Enum.sort_by(fn({{_, date}, _, _, _, _, _, _, _}) -> date end, &</2)
# {date, single_peak}
{all, last_drink_at, gl, active_drinks} = Enum.reduce(drinks, {0.0, nil, [], 0}, fn({{_, date}, volume, _, _, _, _, _, _}, {all, last_at, acc, active_drinks}) ->
k = if meta.sex, do: 0.7, else: 0.6
weight = meta.weight
peak = (10*volume)/(k*weight)
date = case date do
ts when is_integer(ts) -> DateTime.from_unix!(ts, :millisecond)
date = %NaiveDateTime{} -> DateTime.from_naive!(date, "Etc/UTC")
date = %DateTime{} -> date
end
last_at = last_at || date
mins_since = round(DateTime.diff(now, date)/60.0)
#IO.puts "Drink: #{inspect({date, volume})} - mins since: #{inspect mins_since} - last drink at #{inspect last_at}"
# Apply loss since `last_at` on `all`
#
all = if last_at do
mins_since_last = round(DateTime.diff(date, last_at)/60.0)
loss = ((meta.loss_factor/100)/60)*(mins_since_last)
#IO.puts "Applying last drink loss: from #{all}, loss of #{inspect loss} (mins since #{inspect mins_since_first})"
cond do
(all-loss) > 0 -> all - loss
true -> 0.0
end
else
all
end
#IO.puts "Applying last drink current before drink: #{inspect all}"
if mins_since < 30 do
per_min = (peak)/30.0
current = (per_min*mins_since)
#IO.puts "Applying current drink 30m: from #{peak}, loss of #{inspect per_min}/min (mins since #{inspect mins_since})"
{all + current, date, [{date, current} | acc], active_drinks + 1}
else
{all + peak, date, [{date, peak} | acc], active_drinks}
end
end)
#IO.puts "last drink #{inspect last_drink_at}"
mins_since_last = if last_drink_at do
round(DateTime.diff(now, last_drink_at)/60.0)
else
0
end
# Si on a déjà bu y'a déjà moins 15 minutes (big up le binge drinking), on applique plus de perte
level = if mins_since_last > 15 do
loss = ((meta.loss_factor/100)/60)*(mins_since_last)
Float.round(all - loss, 4)
else
all
end
#IO.puts "\n LEVEL #{inspect level}\n\n\n\n"
cond do
level < 0 -> {0.0, 0}
true -> {level, active_drinks}
end
end
defp format_duration_from_now(date, with_detail \\ true) do
date = if is_integer(date) do
date = DateTime.from_unix!(date, :millisecond)
|> Timex.Timezone.convert("Europe/Paris")
else
Util.to_naive_date_time(date)
end
now = DateTime.utc_now()
|> Timex.Timezone.convert("Europe/Paris")
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(date, "({h24}:{m})", "fr")
mins_since = round(DateTime.diff(now, date)/60.0)
if ago = format_minute_duration(mins_since) do
word = if mins_since > 0 do
"il y a "
else
"dans "
end
word <> ago <> if(with_detail, do: " #{detail}", else: "")
else
"maintenant #{detail}"
end
end
defp format_minute_duration(minutes) do
sober_in_s = if (minutes != 0) do
duration = Timex.Duration.from_minutes(minutes)
Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
else
nil
end
end
end
diff --git a/lib/plugins/alcoolog_announcer.ex b/lib/plugins/alcoolog_announcer.ex
index 9f66799..937002e 100644
--- a/lib/plugins/alcoolog_announcer.ex
+++ b/lib/plugins/alcoolog_announcer.ex
@@ -1,269 +1,269 @@
defmodule Nola.Plugins.AlcoologAnnouncer do
require Logger
@moduledoc """
Annonce changements d'alcoolog
"""
@channel "#dmz"
@seconds 30
@apero [
"C'EST L'HEURE DE L'APÉRRROOOOOOOO !!",
"SAAAAANNNNNNNTTTTTTTTAAAAAAAAIIIIIIIIIIIIIIIIIIIIII",
"APÉRO ? APÉRO !",
{:timed, ["/!\\ à vos tires bouchons…", "… débouchez …", "… BUVEZ! SANTAIII!"]},
"/!\\ ALERTE APÉRO /!\\",
"CED !!! VASE DE ROUGE !",
"DIDI UN PETIT RICARD™??!",
"ALLEZ GUIGUI UNE PETITE BIERE ?",
{:timed, ["/!\\ à vos tires bouchons…", "… débouchez …", "… BUVEZ! SANTAIII!"]},
"APPPPAIIIRRREAAUUUUUUUUUUU"
]
def irc_doc, do: nil
def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
def log(account) do
dets_filename = (Nola.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
from = ~U[2020-08-23 19:41:40.524154Z]
to = ~U[2020-08-24 19:41:40.524154Z]
select = [
{{:"$1", :"$2", :_},
[
{:andalso,
{:andalso, {:==, :"$1", {:const, account.id}},
{:>, :"$2", {:const, DateTime.to_unix(from)}}},
{:<, :"$2", {:const, DateTime.to_unix(to)}}}
], [:"$_"]}
]
res = :dets.select(dets, select)
:dets.close(dets)
res
end
def init(_) do
{:ok, _} = Registry.register(Nola.PubSub, "account", [])
stats = get_stats()
Process.send_after(self(), :stats, :timer.seconds(30))
dets_filename = (Nola.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
ets = nil # :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
{:ok, {stats, now(), dets, ets}}#, {:continue, :traverse}}
end
def handle_continue(:traverse, state = {_, _, dets, ets}) do
traverse_fun = fn(obj, dets) ->
case obj do
{nick, %DateTime{} = dt, active} ->
:dets.delete_object(dets, obj)
:dets.insert(dets, {nick, DateTime.to_unix(dt), active})
IO.puts("ok #{inspect obj}")
dets
{nick, ts, value} ->
:ets.insert(ets, { {nick, ts}, value })
dets
end
end
:dets.foldl(traverse_fun, dets, dets)
:dets.sync(dets)
IO.puts("alcoolog announcer fixed")
{:noreply, state}
end
def alcohol_reached(old, new, level) do
(old.active < level && new.active >= level) && (new.active5m >= level)
end
def alcohol_below(old, new, level) do
(old.active > level && new.active <= level) && (new.active5m <= level)
end
def handle_info(:stats, {old_stats, old_now, dets, ets}) do
stats = get_stats()
now = now()
if old_now.hour < 18 && now.hour == 18 do
apero = Enum.shuffle(@apero)
|> Enum.random()
case apero do
{:timed, list} ->
spawn(fn() ->
for line <- list do
IRC.Connection.broadcast_message("evolu.net", "#dmz", line)
:timer.sleep(:timer.seconds(5))
end
end)
string ->
IRC.Connection.broadcast_message("evolu.net", "#dmz", string)
end
end
#IO.puts "newstats #{inspect stats}"
events = for {acct, old} <- old_stats do
new = Map.get(stats, acct, nil)
#IO.puts "#{acct}: #{inspect(old)} -> #{inspect(new)}"
now = DateTime.to_unix(DateTime.utc_now())
if new && new[:active] do
:dets.insert(dets, {acct, now, new[:active]})
:ets.insert(ets, {{acct, now}, new[:active]})
else
:dets.insert(dets, {acct, now, 0.0})
:ets.insert(ets, {{acct, now}, new[:active]})
end
event = cond do
old == nil -> nil
(old.active > 0) && (new == nil) -> :sober
new == nil -> nil
alcohol_reached(old, new, 0.5) -> :stopconduire
alcohol_reached(old, new, 1.0) -> :g1
alcohol_reached(old, new, 2.0) -> :g2
alcohol_reached(old, new, 3.0) -> :g3
alcohol_reached(old, new, 4.0) -> :g4
alcohol_reached(old, new, 5.0) -> :g5
alcohol_reached(old, new, 6.0) -> :g6
alcohol_reached(old, new, 7.0) -> :g7
alcohol_reached(old, new, 10.0) -> :g10
alcohol_reached(old, new, 13.74) -> :record
alcohol_below(old, new, 0.5) -> :conduire
alcohol_below(old, new, 1.0) -> :fini1g
alcohol_below(old, new, 2.0) -> :fini2g
alcohol_below(old, new, 3.0) -> :fini3g
alcohol_below(old, new, 4.0) -> :fini4g
(old.rising) && (!new.rising) -> :lowering
true -> nil
end
{acct, event}
end
for {acct, event} <- events do
message = case event do
:g1 -> [
"[vigicuite jaune] LE GRAMME! LE GRAMME O/",
"début de vigicuite jaune ! LE GRAMME ! \\O/",
"waiiiiiiii le grammmeee",
"bourraiiiiiiiiiiide 1 grammeeeeeeeeeee",
]
:g2 -> [
"[vigicuite orange] \\o_YAY 2 GRAMMES ! _o/",
"PAITAIIIIIIIIII DEUX GRAMMEESSSSSSSSSSSSSSSSS",
"bourrrrrraiiiiiiiiiiiiiiiide 2 grammeeeeeeeeeees",
]
:g3 -> [
"et un ! et deux ! et TROIS GRAMMEEESSSSSSS",
"[vigicuite rouge] _o/ BOURRAIIDDDEEEE 3 GRAMMESSSSSSSSS \\o/ \\o/"
]
:g4 -> [
"[vigicuite écarlate] et un, et deux, et trois, ET QUATRES GRAMMEESSSSSSSSSSSSSSSSSSSssssss"
]
:g5 -> "[vigicuite écarlate+] PUTAIN 5 GRAMMES !"
:g6 -> "[vigicuite écarlate++] 6 grammes ? Vous pouvez joindre Alcool info service au 0 980 980 930"
:g7 -> "[vigicuite c'est la merde] 7 grammes. Le SAMU, c'est le 15."
:g10 -> "BORDLE 10 GRAMMES"
:record -> "RECORD DU MONDE BATTU ! >13.74g/l !!"
:fini1g -> [
"fin d'alerte vigicuite jaune, passage en vert (<1g/l)",
"/!\\ alerte moins de 1g/l /!\\"
]
:fini2g -> [
"t'as moins de 2 g/l, faut se reprendre là [vigicuite jaune]"
]
:fini3g -> [
"fin d'alerte vigicuite rouge, passage en orange (<3g/l)"
]
:fini4g -> [
"fin d'alerte vigicuite écarlate, passage en rouge (<4g/l)"
]
:lowering -> [
"attention ça baisse!",
"tu vas quand même pas en rester là ?",
"IL FAUT CONTINUER À BOIRE !",
"t'abandonnes déjà ?",
"!santai ?",
"faut pas en rester là",
"il faut se resservir",
"coucou faut reboire",
"encore un petit verre ?",
"abwaaaaaaaaaaaaarrrrrrrrrrrrrr",
"taux d'alcoolémie en chute ! agissez avant qu'il soit trop tard!",
"ÇA BAISSE !!"
]
:stopconduire -> [
"0.5g! bientot le gramme?",
"tu peux plus prendre la route... mais... tu peux prendre la route du gramme! !santai !",
"fini la conduite!",
"0.5! continues faut pas en rester là!",
"beau début, continues !",
"ça monte! 0.5g/l!"
]
:conduire -> [
"tu peux conduire, ou recommencer à boire! niveau critique!",
"!santai ?",
"tu peux reprendre la route, ou reprendre la route du gramme..",
"attention, niveau critique!",
"il faut boire !!",
"trop de sang dans ton alcool, c'est mauvais pour la santé",
"faut pas en rester là !",
]
:sober -> [
"sobre…",
"/!\\ alerte sobriété /!\\",
"... sobre?!?!",
"sobre :(",
"attention, t'es sobre :/",
"danger, alcoolémie à 0.0 !",
"sobre! c'était bien on recommence quand ?",
"sobre ? Faut recommencer...",
"T'es sobre. Ne te laisses pas abattre- ton caviste peut aider.",
"Vous êtes sobre ? Ceci n'est pas une fatalité - resservez vous vite !"
]
_ -> nil
end
message = case message do
m when is_binary(m) -> m
m when is_list(m) -> m |> Enum.shuffle() |> Enum.random()
nil -> nil
end
if message do
#IO.puts("#{acct}: #{message}")
account = Nola.Account.get(acct)
for {net, chan} <- Nola.Membership.notify_channels(account) do
- user = IRC.UserTrack.find_by_account(net, account)
+ user = Nola.UserTrack.find_by_account(net, account)
nick = if(user, do: user.nick, else: account.name)
IRC.Connection.broadcast_message(net, chan, "#{nick}: #{message}")
end
end
end
timer()
#IO.puts "tick stats ok"
{:noreply, {stats,now,dets,ets}}
end
def handle_info(_, state) do
{:noreply, state}
end
defp now() do
DateTime.utc_now()
|> Timex.Timezone.convert("Europe/Paris")
end
defp get_stats() do
Enum.into(Nola.Plugins.Alcoolog.get_all_stats(), %{})
end
defp timer() do
Process.send_after(self(), :stats, :timer.seconds(@seconds))
end
end
diff --git a/lib/plugins/last_fm.ex b/lib/plugins/last_fm.ex
index 68c55ee..8e872ea 100644
--- a/lib/plugins/last_fm.ex
+++ b/lib/plugins/last_fm.ex
@@ -1,187 +1,187 @@
defmodule Nola.Plugins.LastFm do
require Logger
@moduledoc """
# last.fm
* **!lastfm|np `[nick|username]`**
* **.lastfm|np**
* **+lastfm, -lastfm `<username last.fm>; ?lastfm`** Configurer un nom d'utilisateur last.fm
"""
@single_trigger ~w(lastfm np)
@pubsub_topics ~w(trigger:lastfm trigger:np)
defstruct dets: nil
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [type: __MODULE__]
for t <- @pubsub_topics, do: {:ok, _} = Registry.register(Nola.PubSub, t, type: __MODULE__)
dets_filename = (Nola.data_path() <> "/" <> "lastfm.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [])
{:ok, %__MODULE__{dets: dets}}
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :plus, args: [username]}}}, state) do
username = String.strip(username)
:ok = :dets.insert(state.dets, {message.account.id, username})
message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm configuré: \"#{username}\".")
{:noreply, state}
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :minus, args: []}}}, state) do
text = case :dets.lookup(state.dets, message.account.id) do
[{_nick, _username}] ->
:dets.delete(state.dets, message.account.id)
message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm enlevé.")
_ -> nil
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :query, args: []}}}, state) do
text = case :dets.lookup(state.dets, message.account.id) do
[{_nick, username}] ->
message.replyfun.("#{message.sender.nick}: #{username}.")
_ -> nil
end
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: []}}}, state) do
irc_now_playing(message.account.id, message, state)
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: [nick_or_user]}}}, state) do
irc_now_playing(nick_or_user, message, state)
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :dot}}}, state) do
members = Nola.Membership.members(message.network, message.channel)
foldfun = fn({nick, user}, acc) -> [{nick,user}|acc] end
usernames = :dets.foldl(foldfun, [], state.dets)
|> Enum.uniq()
|> Enum.filter(fn({acct,_}) -> Enum.member?(members, acct) end)
|> Enum.map(fn({_, u}) -> u end)
for u <- usernames, do: irc_now_playing(u, message, state)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
def terminate(_reason, state) do
if state.dets do
:dets.sync(state.dets)
:dets.close(state.dets)
end
:ok
end
defp irc_now_playing(nick_or_user, message, state) do
nick_or_user = String.strip(nick_or_user)
id_or_user = if account = Nola.Account.get(nick_or_user) || Nola.Account.find_always_by_nick(message.network, message.channel, nick_or_user) do
account.id
else
nick_or_user
end
username = case :dets.lookup(state.dets, id_or_user) do
[{_, username}] -> username
_ -> id_or_user
end
case now_playing(username) do
{:error, text} when is_binary(text) ->
message.replyfun.(text)
{:ok, map} when is_map(map) ->
track = fetch_track(username, map)
text = format_now_playing(map, track)
user = if account = Nola.Account.get(id_or_user) do
- user = IRC.UserTrack.find_by_account(message.network, account)
+ user = Nola.UserTrack.find_by_account(message.network, account)
if(user, do: user.nick, else: account.name)
else
username
end
if user && text do
message.replyfun.("#{user} #{text}")
else
message.replyfun.("#{username}: pas de résultat")
end
other ->
message.replyfun.("erreur :(")
end
end
defp now_playing(user) do
api = Application.get_env(:nola, :lastfm)[:api_key]
url = "http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&format=json&limit=1&extended=1" <> "&api_key=" <> api <> "&user="<> user
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} -> Jason.decode(body)
{:ok, %HTTPoison.Response{status_code: 404}} -> {:error, "last.fm: utilisateur \"#{user}\" inexistant"}
{:ok, %HTTPoison.Response{status_code: code}} -> {:error, "last.fm: erreur #{to_string(code)}"}
error ->
Logger.error "Lastfm http error: #{inspect error}"
:error
end
end
defp fetch_track(user, %{"recenttracks" => %{"track" => [ t = %{"name" => name, "artist" => %{"name" => artist}} | _]}}) do
api = Application.get_env(:nola, :lastfm)[:api_key]
url = "http://ws.audioscrobbler.com/2.0/?method=track.getInfo&format=json" <> "&api_key=" <> api <> "&username="<> user <> "&artist="<>URI.encode(artist)<>"&track="<>URI.encode(name)
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
case Jason.decode(body) do
{:ok, body} -> body["track"] || %{}
_ -> %{}
end
error ->
Logger.error "Lastfm http error: #{inspect error}"
:error
end
end
defp format_now_playing(%{"recenttracks" => %{"track" => [track = %{"@attr" => %{"nowplaying" => "true"}} | _]}}, et) do
format_track(true, track, et)
end
defp format_now_playing(%{"recenttracks" => %{"track" => [track | _]}}, et) do
format_track(false, track, et)
end
defp format_now_playing(%{"error" => err, "message" => message}, _) do
"last.fm error #{err}: #{message}"
end
defp format_now_playing(miss) do
nil
end
defp format_track(np, track, extended) do
artist = track["artist"]["name"]
album = if track["album"]["#text"], do: " (" <> track["album"]["#text"] <> ")", else: ""
name = track["name"] <> album
action = if np, do: "écoute ", else: "a écouté"
love = if track["loved"] != "0", do: "❤️"
count = if x = extended["userplaycount"], do: "x#{x} #{love}"
tags = (get_in(extended, ["toptags", "tag"]) || [])
|> Enum.map(fn(tag) -> tag["name"] end)
|> Enum.filter(& &1)
|> Enum.join(", ")
[action, artist, name, count, tags, track["url"]]
|> Enum.filter(& &1)
|> Enum.map(&String.trim(&1))
|> Enum.join(" - ")
end
end
diff --git a/lib/plugins/preums.ex b/lib/plugins/preums.ex
index c55248d..505ce7f 100644
--- a/lib/plugins/preums.ex
+++ b/lib/plugins/preums.ex
@@ -1,276 +1,276 @@
defmodule Nola.Plugins.Preums do
@moduledoc """
# preums !!!
* `!preums`: affiche le preums du jour
* `.preums`: stats des preums
"""
# WIP Scores
# L'idée c'est de donner un score pour mettre un peu de challenge en pénalisant les preums faciles.
#
# Un preums ne vaut pas 1 point, mais plutôt 0.10 ou 0.05, et on arrondi au plus proche. C'est un jeu sur le long
# terme. Un gros bonus pourrait apporter beaucoup de points.
#
# Il faudrait ces données:
# - moyenne des preums
# - activité récente du channel et par nb actifs d'utilisateurs
# (aggréger memberships+usertrack last_active ?)
# (faire des stats d'activité habituelle (un peu a la pisg) ?)
# - preums consécutifs
#
# Malus:
# - est proche de la moyenne en faible activité
# - trop consécutif de l'utilisateur sauf si activité
#
# Bonus:
# - plus le preums est éloigné de la moyenne
# - après 18h double
# - plus l'activité est élévée, exponentiel selon la moyenne
# - derns entre 4 et 6 (pourrait être adapté selon les stats d'activité)
#
# WIP Badges:
# - derns
# - streaks
# - faciles
# - ?
require Logger
@perfects [~r/preum(s|)/i]
# dets {{chan, day = {yyyy, mm, dd}}, nick, now, perfect?, text}
def all(dets) do
:dets.foldl(fn(i, acc) -> [i|acc] end, [], dets)
end
def all(dets, channel) do
fun = fn({{chan, date}, account_id, time, perfect, text}, acc) ->
if channel == chan do
[%{date: date, account_id: account_id, time: time, perfect: perfect, text: text} | acc]
else
acc
end
end
:dets.foldl(fun, [], dets)
end
def topnicks(dets, channel, options \\ []) do
sort_elem = case Keyword.get(options, :sort_by, :score) do
:score -> 1
:count -> 0
end
fun = fn(x = {{chan, date}, account_id, time, perfect, text}, acc) ->
if (channel == nil and chan) or (channel == chan) do
{count, points} = Map.get(acc, account_id, {0, 0})
score = score(chan, account_id, time, perfect, text)
Map.put(acc, account_id, {count + 1, points + score})
else
acc
end
end
:dets.foldl(fun, %{}, dets)
|> Enum.sort_by(fn({_account_id, value}) -> elem(value, sort_elem) end, &>=/2)
end
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def dets do
(Nola.data_path() <> "/preums.dets") |> String.to_charlist()
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(Nola.PubSub, "account", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "messages", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "triggers", regopts)
{:ok, dets} = :dets.open_file(dets(), [{:repair, :force}])
Util.ets_mutate_select_each(:dets, dets, [{:"$1", [], [:"$1"]}], fn(table, obj) ->
{key, nick, now, perfect, text} = obj
case key do
{{net, {bork,chan}}, date} ->
:dets.delete(table, key)
nick = if Nola.Account.get(nick) do
nick
else
if acct = Nola.Account.find_always_by_nick(net, nil, nick) do
acct.id
else
nick
end
end
:dets.insert(table, { { {net,chan}, date }, nick, now, perfect, text})
{{_net, nil}, _} ->
:dets.delete(table, key)
{{net, chan}, date} ->
if !Nola.Account.get(nick) do
if acct = Nola.Account.find_always_by_nick(net, chan, nick) do
:dets.delete(table, key)
:dets.insert(table, { { {net,chan}, date }, acct.id, now, perfect, text})
end
end
_ ->
Logger.debug("DID NOT FIX: #{inspect key}")
end
end)
{:ok, %{dets: dets}}
end
# Latest
def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) do
channelkey = {m.network, m.channel}
state = handle_preums(m, state)
tz = timezone(channelkey)
{:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
date = {now.year, now.month, now.day}
key = {channelkey, date}
chan_cache = Map.get(state, channelkey, %{})
item = if i = Map.get(chan_cache, date) do
i
else
case :dets.lookup(state.dets, key) do
[item = {^key, _account_id, _now, _perfect, _text}] -> item
_ -> nil
end
end
if item do
{_, account_id, date, _perfect, text} = item
h = "#{date.hour}:#{date.minute}:#{date.second}"
account = Nola.Account.get(account_id)
- user = IRC.UserTrack.find_by_account(m.network, account)
+ user = Nola.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
m.replyfun.("preums: #{nick} à #{h}: “#{text}”")
end
{:noreply, state}
end
# Stats
def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :dot}}}, state) do
channel = {m.network, m.channel}
state = handle_preums(m, state)
top = topnicks(state.dets, channel, sort_by: :score)
|> Enum.map(fn({account_id, {count, score}}) ->
account = Nola.Account.get(account_id)
- user = IRC.UserTrack.find_by_account(m.network, account)
+ user = Nola.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
"#{nick}: #{score} (#{count})"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
msg = unless top == "" do
"top preums: #{top}"
else
"vous êtes tous nuls"
end
m.replyfun.(msg)
{:noreply, state}
end
# Help
def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :query}}}, state) do
state = handle_preums(m, state)
msg = "!preums - preums du jour, .preums top preumseurs"
m.replymsg.(msg)
{:noreply, state}
end
# Trigger fallback
def handle_info({:irc, :trigger, _, m = %IRC.Message{}}, state) do
state = handle_preums(m, state)
{:noreply, state}
end
# Message fallback
def handle_info({:irc, :text, m = %IRC.Message{}}, state) do
{:noreply, handle_preums(m, state)}
end
# Account
def handle_info({:account_change, old_id, new_id}, state) do
spec = [{{:_, :"$1", :_, :_, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
rename_object_owner(table, obj, new_id)
end)
{:noreply, state}
end
# Account: move from nick to account id
# FIXME: Doesn't seem to work.
def handle_info({:accounts, accounts}, state) do
for x={:account, _net, _chan, _nick, _account_id} <- accounts do
handle_info(x, state)
end
{:noreply, state}
end
def handle_info({:account, _net, _chan, nick, account_id}, state) do
nick = String.downcase(nick)
spec = [{{:_, :"$1", :_, :_, :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
Logger.debug("account:: merging #{nick} -> #{account_id}")
rename_object_owner(table, obj, account_id)
end)
{:noreply, state}
end
def handle_info(_, dets) do
{:noreply, dets}
end
defp rename_object_owner(table, object = {key, _, now, perfect, time}, new_id) do
:dets.delete_object(table, key)
:dets.insert(table, {key, new_id, now, perfect, time})
end
defp timezone(channel) do
env = Application.get_env(:nola, Nola.Plugins.Preums, [])
channels = Keyword.get(env, :channels, %{})
channel_settings = Map.get(channels, channel, [])
default = Keyword.get(env, :default_tz, "Europe/Paris")
Keyword.get(channel_settings, :tz, default) || default
end
defp handle_preums(%IRC.Message{channel: nil}, state) do
state
end
defp handle_preums(m = %IRC.Message{text: text, sender: sender}, state) do
channel = {m.network, m.channel}
tz = timezone(channel)
{:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
date = {now.year, now.month, now.day}
key = {channel, date}
chan_cache = Map.get(state, channel, %{})
unless i = Map.get(chan_cache, date) do
case :dets.lookup(state.dets, key) do
[item = {^key, _nick, _now, _perfect, _text}] ->
# Preums lost, but wasn't cached
Map.put(state, channel, %{date => item})
[] ->
# Preums won!
perfect? = Enum.any?(@perfects, fn(perfect) -> Regex.match?(perfect, text) end)
item = {key, m.account.id, now, perfect?, text}
:dets.insert(state.dets, item)
:dets.sync(state.dets)
Map.put(state, channel, %{date => item})
{:error, _} = error ->
Logger.error("#{__MODULE__} dets lookup failed: #{inspect error}")
state
end
else
state
end
end
def score(_chan, _account, _time, _perfect, _text) do
1
end
end
diff --git a/lib/plugins/tell.ex b/lib/plugins/tell.ex
index 2f874d0..43da9e7 100644
--- a/lib/plugins/tell.ex
+++ b/lib/plugins/tell.ex
@@ -1,106 +1,106 @@
defmodule Nola.Plugins.Tell do
use GenServer
@moduledoc """
# Tell
* **!tell `<nick>` `<message>`**: tell `message` to `nick` when they reconnect.
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def dets do
(Nola.data_path() <> "/tell.dets") |> String.to_charlist()
end
def tell(m, target, message) do
GenServer.cast(__MODULE__, {:tell, m, target, message})
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(Nola.PubSub, "account", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:tell", regopts)
{:ok, dets} = :dets.open_file(dets(), [type: :bag])
{:ok, %{dets: dets}}
end
def handle_cast({:tell, m, target, message}, state) do
do_tell(state, m, target, message)
{:noreply, state}
end
def handle_info({:irc, :trigger, "tell", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [target | message]}}}, state) do
do_tell(state, m, target, message)
{:noreply, state}
end
def handle_info({:account, network, channel, nick, account_id}, state) do
messages = :dets.lookup(state.dets, {network, channel, account_id})
if messages != [] do
strs = Enum.map(messages, fn({_, from, message, at}) ->
account = Nola.Account.get(from)
- user = IRC.UserTrack.find_by_account(network, account)
+ user = Nola.UserTrack.find_by_account(network, account)
fromnick = if user, do: user.nick, else: account.name
"#{nick}: <#{fromnick}> #{message}"
end)
Enum.each(strs, fn(s) -> IRC.Connection.broadcast_message(network, channel, s) end)
:dets.delete(state.dets, {network, channel, account_id})
end
{:noreply, state}
end
def handle_info({:account_change, old_id, new_id}, state) do
#:ets.fun2ms(fn({ {_net, _chan, target_id}, from_id, _, _} = obj) when (target_id == old_id) or (from_id == old_id) -> obj end)
spec = [{{{:"$1", :"$2", :"$3"}, :"$4", :_, :_}, [{:orelse, {:==, :"$3", {:const, old_id}}, {:==, :"$4", {:const, old_id}}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
case obj do
{ {net, chan, ^old_id}, from_id, message, at } = obj ->
:dets.delete(obj)
:dets.insert(table, {{net, chan, new_id}, from_id, message, at})
{key, ^old_id, message, at} = obj ->
:dets.delete(table, obj)
:dets.insert(table, {key, new_id, message, at})
_ -> :ok
end
end)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
def terminate(_, state) do
:dets.close(state.dets)
:ok
end
defp do_tell(state, m, nick_target, message) do
target = Nola.Account.find_always_by_nick(m.network, m.channel, nick_target)
message = Enum.join(message, " ")
with \
{:target, %Nola.Account{} = target} <- {:target, target},
{:same, false} <- {:same, target.id == m.account.id},
- target_user = IRC.UserTrack.find_by_account(m.network, target),
+ target_user = Nola.UserTrack.find_by_account(m.network, target),
target_nick = if(target_user, do: target_user.nick, else: target.name),
present? = if(target_user, do: Map.has_key?(target_user.last_active, m.channel)),
{:absent, true, _} <- {:absent, !present?, target_nick},
{:message, message} <- {:message, message}
do
obj = { {m.network, m.channel, target.id}, m.account.id, message, NaiveDateTime.utc_now()}
:dets.insert(state.dets, obj)
m.replyfun.("will tell to #{target_nick}")
else
{:same, _} -> m.replyfun.("are you so stupid that you need a bot to tell yourself things ?")
{:target, _} -> m.replyfun.("#{nick_target} unknown")
{:absent, _, nick} -> m.replyfun.("#{nick} is here, tell yourself!")
{:message, _} -> m.replyfun.("can't tell without a message")
end
end
end
diff --git a/lib/plugins/txt.ex b/lib/plugins/txt.ex
index f33aab0..4f9a803 100644
--- a/lib/plugins/txt.ex
+++ b/lib/plugins/txt.ex
@@ -1,556 +1,556 @@
defmodule Nola.Plugins.Txt do
- alias IRC.UserTrack
+ alias Nola.UserTrack
require Logger
@moduledoc """
# [txt]({{context_path}}/txt)
* **.txt**: liste des fichiers et statistiques.
Les fichiers avec une `*` sont vérrouillés.
[Voir sur le web]({{context_path}}/txt).
* **!txt**: lis aléatoirement une ligne dans tous les fichiers.
* **!txt `<recherche>`**: recherche une ligne dans tous les fichiers.
* **~txt**: essaie de générer une phrase (markov).
* **~txt `<début>`**: essaie de générer une phrase commencant par `<debut>`.
* **!`FICHIER`**: lis aléatoirement une ligne du fichier `FICHIER`.
* **!`FICHIER` `<chiffre>`**: lis la ligne `<chiffre>` du fichier `FICHIER`.
* **!`FICHIER` `<recherche>`**: recherche une ligne contenant `<recherche>` dans `FICHIER`.
* **+txt `<file`>**: crée le fichier `<file>`.
* **+`FICHIER` `<texte>`**: ajoute une ligne `<texte>` dans le fichier `FICHIER`.
* **-`FICHIER` `<chiffre>`**: supprime la ligne `<chiffre>` du fichier `FICHIER`.
* **-txtrw, +txtrw**. op seulement. active/désactive le mode lecture seule.
* **+txtlock `<fichier>`, -txtlock `<fichier>`**. op seulement. active/désactive le verrouillage d'un fichier.
Insérez `\\\\` pour faire un saut de ligne.
"""
def short_irc_doc, do: "!txt https://sys.115ans.net/irc/txt "
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
defstruct triggers: %{}, rw: true, locks: nil, markov_handler: nil, markov: nil
def random(file) do
GenServer.call(__MODULE__, {:random, file})
end
def reply_random(message, file) do
if line = random(file) do
line
|> format_line(nil, message)
|> message.replyfun.()
line
end
end
def init([]) do
dets_locks_filename = (Nola.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist
{:ok, locks} = :dets.open_file(dets_locks_filename, [])
markov_handler = Keyword.get(Application.get_env(:nola, __MODULE__, []), :markov_handler, Nola.Plugins.Txt.Markov.Native)
{:ok, markov} = markov_handler.start_link()
{:ok, _} = Registry.register(Nola.PubSub, "triggers", [plugin: __MODULE__])
{:ok, %__MODULE__{locks: locks, markov_handler: markov_handler, markov: markov, triggers: load()}}
end
def handle_info({:received, "!reload", _, chan}, state) do
{:noreply, %__MODULE__{state | triggers: load()}}
end
#
# ADMIN: RW/RO
#
def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :plus}}}, state = %{rw: false}) do
if channel && UserTrack.operator?(msg.network, channel, msg.sender.nick) do
msg.replyfun.("txt: écriture réactivée")
{:noreply, %__MODULE__{state | rw: true}}
else
{:noreply, state}
end
end
def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :minus}}}, state = %{rw: true}) do
if channel && UserTrack.operator?(msg.network, channel, msg.sender.nick) do
msg.replyfun.("txt: écriture désactivée")
{:noreply, %__MODULE__{state | rw: false}}
else
{:noreply, state}
end
end
#
# ADMIN: LOCKS
#
def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- UserTrack.operator?(msg.network, msg.channel, msg.sender.nick)
do
:dets.insert(state.locks, {trigger})
msg.replyfun.("txt: #{trigger} verrouillé")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :minus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- UserTrack.operator?(msg.network, msg.channel, msg.sender.nick),
true <- :dets.member(state.locks, trigger)
do
:dets.delete(state.locks, trigger)
msg.replyfun.("txt: #{trigger} déverrouillé")
end
{:noreply, state}
end
#
# FILE LIST
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :dot}}}, state) do
map = Enum.map(state.triggers, fn({key, data}) ->
ignore? = String.contains?(key, ".")
locked? = case :dets.lookup(state.locks, key) do
[{trigger}] -> "*"
_ -> ""
end
unless ignore?, do: "#{key}: #{to_string(Enum.count(data))}#{locked?}"
end)
|> Enum.filter(& &1)
total = Enum.reduce(state.triggers, 0, fn({_, data}, acc) ->
acc + Enum.count(data)
end)
detail = Enum.join(map, ", ")
total = ". total: #{Enum.count(state.triggers)} fichiers, #{to_string(total)} lignes. Détail: https://sys.115ans.net/irc/txt"
ro = if !state.rw, do: " (lecture seule activée)", else: ""
(detail<>total<>ro)
|> msg.replyfun.()
{:noreply, state}
end
#
# GLOBAL: RANDOM
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: []}}}, state) do
result = Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
Enum.reduce(data, acc, fn({l, _}, acc) ->
[{trigger, l} | acc]
end)
end)
|> Enum.shuffle()
if !Enum.empty?(result) do
{source, line} = Enum.random(result)
msg.replyfun.(format_line(line, "#{source}: ", msg))
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: args}}}, state) do
grep = Enum.join(args, " ")
|> String.downcase
|> :unicode.characters_to_nfd_binary()
result = with_stateful_results(msg, {:bang,"txt",msg.network,msg.channel,grep}, fn() ->
Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
if !String.contains?(trigger, ".") do
Enum.reduce(data, acc, fn({l, _}, acc) ->
[{trigger, l} | acc]
end)
else
acc
end
end)
|> Enum.filter(fn({_, line}) ->
line
|> String.downcase()
|> :unicode.characters_to_nfd_binary()
|> String.contains?(grep)
end)
|> Enum.shuffle()
end)
if result do
{source, line} = result
msg.replyfun.(["#{source}: " | line])
end
{:noreply, state}
end
def with_stateful_results(msg, key, initfun) do
me = self()
scope = {msg.network, msg.channel || msg.sender.nick}
key = {__MODULE__, me, scope, key}
with_stateful_results(key, initfun)
end
def with_stateful_results(key, initfun) do
pid = case :global.whereis_name(key) do
:undefined ->
start_stateful_results(key, initfun.())
pid -> pid
end
if pid, do: wait_stateful_results(key, initfun, pid)
end
def start_stateful_results(key, []) do
nil
end
def start_stateful_results(key, list) do
me = self()
{pid, _} = spawn_monitor(fn() ->
Process.monitor(me)
stateful_results(me, list)
end)
:yes = :global.register_name(key, pid)
pid
end
def wait_stateful_results(key, initfun, pid) do
send(pid, :get)
receive do
{:stateful_results, line} ->
line
{:DOWN, _ref, :process, ^pid, reason} ->
with_stateful_results(key, initfun)
after
5000 ->
nil
end
end
defp stateful_results(owner, []) do
send(owner, :empty)
:ok
end
@stateful_results_expire :timer.minutes(30)
defp stateful_results(owner, [line | rest] = acc) do
receive do
:get ->
send(owner, {:stateful_results, line})
stateful_results(owner, rest)
{:DOWN, _ref, :process, ^owner, _} ->
:ok
after
@stateful_results_expire -> :ok
end
end
#
# GLOBAL: MARKOV
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :tilde, args: []}}}, state) do
case state.markov_handler.sentence(state.markov) do
{:ok, line} ->
msg.replyfun.(line)
error ->
Logger.error "Txt Markov error: "<>inspect error
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :tilde, args: complete}}}, state) do
complete = Enum.join(complete, " ")
case state.markov_handler.complete_sentence(complete, state.markov) do
{:ok, line} ->
msg.replyfun.(line)
error ->
Logger.error "Txt Markov error: "<>inspect error
end
{:noreply, state}
end
#
# TXT CREATE
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- can_write?(state, msg, trigger),
:ok <- create_file(trigger)
do
msg.replyfun.("#{trigger}.txt créé. Ajouter: `+#{trigger} …` ; Lire: `!#{trigger}`")
{:noreply, %__MODULE__{state | triggers: load()}}
else
_ -> {:noreply, state}
end
end
#
# TXT: RANDOM
#
def handle_info({:irc, :trigger, trigger, m = %{trigger: %{type: :query, args: opts}}}, state) do
{trigger, _} = clean_trigger(trigger)
if Map.get(state.triggers, trigger) do
url = if m.channel do
NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :txt, m.network, NolaWeb.format_chan(m.channel), trigger)
else
NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :txt, trigger)
end
m.replyfun.("-> #{url}")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :bang, args: opts}}}, state) do
{trigger, _} = clean_trigger(trigger)
line = get_random(msg, state.triggers, trigger, String.trim(Enum.join(opts, " ")))
if line do
msg.replyfun.(format_line(line, nil, msg))
end
{:noreply, state}
end
#
# TXT: ADD
#
def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :plus, args: content}}}, state) do
with \
true <- can_write?(state, msg, trigger),
{:ok, idx} <- add(state.triggers, msg.text)
do
msg.replyfun.("#{msg.sender.nick}: ajouté à #{trigger}. (#{idx})")
{:noreply, %__MODULE__{state | triggers: load()}}
else
{:error, {:jaro, string, idx}} ->
msg.replyfun.("#{msg.sender.nick}: doublon #{trigger}##{idx}: #{string}")
error ->
Logger.debug("txt add failed: #{inspect error}")
{:noreply, state}
end
end
#
# TXT: DELETE
#
def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :minus, args: [id]}}}, state) do
with \
true <- can_write?(state, msg, trigger),
data <- Map.get(state.triggers, trigger),
{id, ""} <- Integer.parse(id),
{text, _id} <- Enum.find(data, fn({_, idx}) -> id-1 == idx end)
do
data = data |> Enum.into(Map.new)
data = Map.delete(data, text)
msg.replyfun.("#{msg.sender.nick}: #{trigger}.txt##{id} supprimée: #{text}")
dump(trigger, data)
{:noreply, %__MODULE__{state | triggers: load()}}
else
_ ->
{:noreply, state}
end
end
def handle_info(:reload_markov, state=%__MODULE__{triggers: triggers, markov: markov}) do
state.markov_handler.reload(state.triggers, state.markov)
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
def handle_call({:random, file}, _from, state) do
random = get_random(nil, state.triggers, file, [])
{:reply, random, state}
end
def terminate(_reason, state) do
if state.locks do
:dets.sync(state.locks)
:dets.close(state.locks)
end
:ok
end
# Load/Reloads text files from disk
defp load() do
triggers = Path.wildcard(directory() <> "/*.txt")
|> Enum.reduce(%{}, fn(path, m) ->
file = Path.basename(path)
key = String.replace(file, ".txt", "")
data = directory() <> file
|> File.read!
|> String.split("\n")
|> Enum.reject(fn(line) ->
cond do
line == "" -> true
!line -> true
true -> false
end
end)
|> Enum.with_index
Map.put(m, key, data)
end)
|> Enum.sort
|> Enum.into(Map.new)
send(self(), :reload_markov)
triggers
end
defp dump(trigger, data) do
data = data
|> Enum.sort_by(fn({_, idx}) -> idx end)
|> Enum.map(fn({text, _}) -> text end)
|> Enum.join("\n")
File.write!(directory() <> "/" <> trigger <> ".txt", data<>"\n", [])
end
defp get_random(msg, triggers, trigger, []) do
if data = Map.get(triggers, trigger) do
{data, _idx} = Enum.random(data)
data
else
nil
end
end
defp get_random(msg, triggers, trigger, opt) do
arg = case Integer.parse(opt) do
{pos, ""} -> {:index, pos}
{_pos, _some_string} -> {:grep, opt}
_error -> {:grep, opt}
end
get_with_param(msg, triggers, trigger, arg)
end
defp get_with_param(msg, triggers, trigger, {:index, pos}) do
data = Map.get(triggers, trigger, %{})
case Enum.find(data, fn({_, index}) -> index+1 == pos end) do
{text, _} -> text
_ -> nil
end
end
defp get_with_param(msg, triggers, trigger, {:grep, query}) do
out = with_stateful_results(msg, {:grep, trigger, query}, fn() ->
data = Map.get(triggers, trigger, %{})
regex = Regex.compile!("#{query}", "i")
Enum.filter(data, fn({txt, _}) -> Regex.match?(regex, txt) end)
|> Enum.map(fn({txt, _}) -> txt end)
|> Enum.shuffle()
end)
if out, do: out
end
defp create_file(name) do
File.touch!(directory() <> "/" <> name <> ".txt")
:ok
end
defp add(triggers, trigger_and_content) do
case String.split(trigger_and_content, " ", parts: 2) do
[trigger, content] ->
{trigger, _} = clean_trigger(trigger)
if Map.has_key?(triggers, trigger) do
jaro = Enum.find(triggers[trigger], fn({string, idx}) -> String.jaro_distance(content, string) > 0.9 end)
if jaro do
{string, idx} = jaro
{:error, {:jaro, string, idx}}
else
File.write!(directory() <> "/" <> trigger <> ".txt", content<>"\n", [:append])
idx = Enum.count(triggers[trigger])+1
{:ok, idx}
end
else
{:error, :notxt}
end
_ -> {:error, :badarg}
end
end
# fixme: this is definitely the ugliest thing i've ever done
defp clean_trigger(trigger) do
[trigger | opts] = trigger
|> String.strip
|> String.split(" ", parts: 2)
trigger = trigger
|> String.downcase
|> :unicode.characters_to_nfd_binary()
|> String.replace(~r/[^a-z0-9._]/, "")
|> String.trim(".")
|> String.trim("_")
{trigger, opts}
end
def format_line(line, prefix, msg) do
prefix = unless(prefix, do: "", else: prefix)
prefix <> line
|> String.split("\\\\")
|> Enum.map(fn(line) ->
String.split(line, "\\\\\\\\")
end)
|> List.flatten()
|> Enum.map(fn(line) ->
String.trim(line)
|> Tmpl.render(msg)
end)
end
def directory() do
Application.get_env(:nola, :data_path) <> "/irc.txt/"
end
defp can_write?(%{rw: rw?, locks: locks}, msg = %{channel: nil, sender: sender}, trigger) do
admin? = IRC.admin?(sender)
locked? = case :dets.lookup(locks, trigger) do
[{trigger}] -> true
_ -> false
end
unlocked? = if rw? == false, do: false, else: !locked?
can? = unlocked? || admin?
if !can? do
reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé"
msg.replyfun.("#{sender.nick}: permission refusée (#{reason})")
end
can?
end
defp can_write?(state = %__MODULE__{rw: rw?, locks: locks}, msg = %{channel: channel, sender: sender}, trigger) do
admin? = IRC.admin?(sender)
- operator? = IRC.UserTrack.operator?(msg.network, channel, sender.nick)
+ operator? = Nola.UserTrack.operator?(msg.network, channel, sender.nick)
locked? = case :dets.lookup(locks, trigger) do
[{trigger}] -> true
_ -> false
end
unlocked? = if rw? == false, do: false, else: !locked?
can? = admin? || operator? || unlocked?
if !can? do
reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé"
msg.replyfun.("#{sender.nick}: permission refusée (#{reason})")
end
can?
end
end
diff --git a/lib/tmpl.ex b/lib/tmpl.ex
index f4dac02..62c2a46 100644
--- a/lib/tmpl.ex
+++ b/lib/tmpl.ex
@@ -1,124 +1,124 @@
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, %Nola.Account{id: id}) do
+ if user = Nola.UserTrack.find_by_account(network, %Nola.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} ->
Logger.debug("Liquid error: #{pos} - #{inspect template}")
"[liquid ast error (at #{pos}): #{inspect err}]"
end
end
defp do_render(template_ast, context, safe) when is_list(template_ast) do
context = Liquex.Context.new(mapify(context, safe))
|> Map.put(:filter_module, Tmpl.Filter)
{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/web/controllers/irc_controller.ex b/lib/web/controllers/irc_controller.ex
index 441cbe7..d2ba04a 100644
--- a/lib/web/controllers/irc_controller.ex
+++ b/lib/web/controllers/irc_controller.ex
@@ -1,101 +1,101 @@
defmodule NolaWeb.IrcController do
use NolaWeb, :controller
plug NolaWeb.ContextPlug
def index(conn, params) do
network = Map.get(params, "network")
channel = if c = Map.get(params, "chan"), do: NolaWeb.reformat_chan(c)
commands = for mod <- Enum.uniq([Nola.Plugins.Account] ++ Nola.Plugins.enabled()) do
if is_atom(mod) do
identifier = Module.split(mod) |> List.last |> Macro.underscore
{identifier, mod.irc_doc()}
end
end
|> Enum.filter(& &1)
|> Enum.filter(fn({_, doc}) -> doc end)
members = cond do
- network && channel -> Enum.map(IRC.UserTrack.channel(network, channel), fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple) end)
+ network && channel -> Enum.map(Nola.UserTrack.channel(network, channel), fn(tuple) -> Nola.UserTrack.User.from_tuple(tuple) end)
true ->
Nola.Membership.of_account(conn.assigns.account)
end
render conn, "index.html", network: network, commands: commands, channel: channel, members: members
end
def txt(conn, %{"name" => name}) do
if String.contains?(name, ".txt") do
name = String.replace(name, ".txt", "")
data = data()
if Map.has_key?(data, name) do
lines = Enum.join(data[name], "\n")
text(conn, lines)
else
conn
|> put_status(404)
|> text("Not found")
end
else
do_txt(conn, name)
end
end
def txt(conn, _), do: do_txt(conn, nil)
defp do_txt(conn, nil) do
doc = Nola.IRC.Txt.irc_doc()
data = data()
main = Enum.filter(data, fn({trigger, _}) -> !String.contains?(trigger, ".") end) |> Enum.into(Map.new)
system = Enum.filter(data, fn({trigger, _}) -> String.contains?(trigger, ".") end) |> Enum.into(Map.new)
lines = Enum.reduce(main, 0, fn({_, lines}, acc) -> acc + Enum.count(lines) end)
conn
|> assign(:title, "txt")
|> render("txts.html", data: main, doc: doc, files: Enum.count(main), lines: lines, system: system)
end
defp do_txt(conn, txt) do
data = data()
base_url = cond do
conn.assigns[:chan] -> "/#{conn.assigns.network}/#{NolaWeb.format_chan(conn.assigns.chan)}"
true -> "/-"
end
if lines = Map.get(data, txt) do
lines = Enum.map(lines, fn(line) ->
line
|> String.split("\\\\")
|> Enum.intersperse(Phoenix.HTML.Tag.tag(:br))
end)
conn
|> assign(:breadcrumbs, [{"txt", "#{base_url}/txt"}])
|> assign(:title, "#{txt}.txt")
|> render("txt.html", name: txt, data: lines, doc: nil)
else
conn
|> put_status(404)
|> text("Not found")
end
end
defp data() do
dir = Application.get_env(:nola, :data_path) <> "/irc.txt/"
Path.wildcard(dir <> "/*.txt")
|> Enum.reduce(%{}, fn(path, m) ->
path = String.split(path, "/")
file = List.last(path)
key = String.replace(file, ".txt", "")
data = dir <> file
|> File.read!
|> String.split("\n")
|> Enum.reject(fn(line) ->
cond do
line == "" -> true
!line -> true
true -> false
end
end)
Map.put(m, key, data)
end)
|> Enum.sort
|> Enum.into(Map.new)
end
end
diff --git a/lib/web/controllers/page_controller.ex b/lib/web/controllers/page_controller.ex
index a6b85b6..bf79413 100644
--- a/lib/web/controllers/page_controller.ex
+++ b/lib/web/controllers/page_controller.ex
@@ -1,53 +1,53 @@
defmodule NolaWeb.PageController do
use NolaWeb, :controller
plug NolaWeb.ContextPlug when action not in [:token]
plug NolaWeb.ContextPlug, [restrict: :public] when action in [:token]
def token(conn, %{"token" => token}) do
with \
{:ok, account, perks} <- Nola.AuthToken.lookup(token)
do
IO.puts("Authenticated account #{inspect account}")
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 index(conn = %{assigns: %{account: account}}, _) do
memberships = Nola.Membership.of_account(account)
- users = IRC.UserTrack.find_by_account(account)
+ users = Nola.UserTrack.find_by_account(account)
metas = Nola.Account.get_all_meta(account)
predicates = Nola.Account.get_predicates(account)
conn
|> assign(:title, account.name)
|> render("user.html", users: users, memberships: memberships, metas: metas, predicates: predicates)
end
def irc(conn, _) do
bot_helps = for mod <- Nola.IRC.env(:handlers) do
mod.irc_doc()
end
render conn, "irc.html", bot_helps: bot_helps
end
def authenticate(conn, _) do
with \
{:account, account_id} when is_binary(account_id) <- {:account, get_session(conn, :account)},
{:account, account} when not is_nil(account) <- {:account, Nola.Account.get(account_id)}
do
assign(conn, :account, account)
else
_ -> conn
end
end
end
diff --git a/lib/web/live/chat_live.ex b/lib/web/live/chat_live.ex
index 2d5e289..3f126f2 100644
--- a/lib/web/live/chat_live.ex
+++ b/lib/web/live/chat_live.ex
@@ -1,120 +1,120 @@
defmodule NolaWeb.ChatLive do
use Phoenix.LiveView
use Phoenix.HTML
require Logger
def mount(%{"network" => network, "chan" => chan}, %{"account" => account_id}, socket) do
chan = NolaWeb.reformat_chan(chan)
connection = IRC.Connection.get_network(network, chan)
account = Nola.Account.get(account_id)
membership = Nola.Membership.of_account(Nola.Account.get("DRgpD4fLf8PDJMLp8Dtb"))
if account && connection && Enum.member?(membership, {connection.network, chan}) do
{:ok, _} = Registry.register(Nola.PubSub, "#{connection.network}:events", plugin: __MODULE__)
for t <- ["messages", "triggers", "outputs", "events"] do
{:ok, _} = Registry.register(Nola.PubSub, "#{connection.network}/#{chan}:#{t}", plugin: __MODULE__)
end
IRC.PuppetConnection.start(account, connection)
- users = IRC.UserTrack.channel(connection.network, chan)
- |> Enum.map(fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple) end)
+ users = Nola.UserTrack.channel(connection.network, chan)
+ |> Enum.map(fn(tuple) -> Nola.UserTrack.User.from_tuple(tuple) end)
|> Enum.reduce(Map.new, fn(user = %{id: id}, acc) ->
Map.put(acc, id, user)
end)
backlog = case Nola.IRC.Buffer.select_buffer(connection.network, chan) do
{backlog, _} ->
{backlog, _} = Enum.reduce(backlog, {backlog, nil}, &reduce_contextual_event/2)
Enum.reverse(backlog)
_ -> []
end
socket = socket
|> assign(:connection_id, connection.id)
|> assign(:network, connection.network)
|> assign(:chan, chan)
|> assign(:title, "live")
|> assign(:channel, chan)
|> assign(:account_id, account.id)
|> assign(:backlog, backlog)
|> assign(:users, users)
|> assign(:counter, 0)
{:ok, socket}
else
{:ok, redirect(socket, to: "/")}
end
end
def handle_event("send", %{"message" => %{"text" => text}}, socket) do
account = Nola.Account.get(socket.assigns.account_id)
IRC.send_message_as(account, socket.assigns.network, socket.assigns.channel, text, true)
{:noreply, assign(socket, :counter, socket.assigns.counter + 1)}
end
def handle_info({:irc, :event, event = %{type: :join, user_id: id}}, socket) do
- if user = IRC.UserTrack.lookup(id) do
+ if user = Nola.UserTrack.lookup(id) do
socket = socket
|> assign(:users, Map.put(socket.assigns.users, id, user))
|> append_to_backlog(event)
{:noreply, socket}
else
{:noreply, socket}
end
end
def handle_info({:irc, :event, event = %{type: :nick, user_id: id, nick: nick}}, socket) do
socket = socket
|> assign(:users, update_in(socket.assigns.users, [id, :nick], nick))
|> append_to_backlog(event)
{:noreply, socket}
end
def handle_info({:irc, :event, event = %{type: :quit, user_id: id}}, socket) do
socket = socket
|> assign(:users, Map.delete(socket.assigns.users, id))
|> append_to_backlog(event)
{:noreply, socket}
end
def handle_info({:irc, :event, event = %{type: :part, user_id: id}}, socket) do
socket = socket
|> assign(:users, Map.delete(socket.assigns.users, id))
|> append_to_backlog(event)
{:noreply, socket}
end
def handle_info({:irc, :trigger, _, message}, socket) do
handle_info({:irc, nil, message}, socket)
end
def handle_info({:irc, :text, message}, socket) do
IO.inspect({:live_message, message})
socket = socket
|> append_to_backlog(message)
{:noreply, socket}
end
def handle_info(info, socket) do
Logger.debug("Unhandled info: #{inspect info}")
{:noreply, socket}
end
defp append_to_backlog(socket, line) do
{add, _} = reduce_contextual_event(line, {[], List.last(socket.assigns.backlog)})
assign(socket, :backlog, socket.assigns.backlog ++ add)
end
defp reduce_contextual_event(line, {acc, nil}) do
{[line | acc], line}
end
defp reduce_contextual_event(line, {acc, last}) do
if NaiveDateTime.to_date(last.at) != NaiveDateTime.to_date(line.at) do
{[%{type: :day_changed, date: NaiveDateTime.to_date(line.at), at: nil}, line | acc], line}
else
{[line | acc], line}
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Fri, Mar 14, 4:48 PM (16 h, 43 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
33522
Default Alt Text
(180 KB)

Event Timeline