Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F51205
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
135 KB
Subscribers
None
View Options
diff --git a/lib/irc/nola_irc.ex b/lib/irc/nola_irc.ex
index 2d355d3..7e23f50 100644
--- a/lib/irc/nola_irc.ex
+++ b/lib/irc/nola_irc.ex
@@ -1,33 +1,33 @@
defmodule Nola.IRC do
require Logger
def env(), do: Nola.env(:irc)
def env(key, default \\ nil), do: Keyword.get(env(), key, default)
def application_childs do
import Supervisor.Spec
IRC.Connection.setup()
Nola.Plugins.setup()
[
worker(Registry, [[keys: :duplicate, name: IRC.ConnectionPubSub]], id: :registr_irc_conn),
- worker(IRC.Membership, []),
+ worker(Nola.Membership, []),
worker(Nola.Account, []),
worker(IRC.UserTrack.Storage, []),
worker(Nola.Plugins.Account, []),
supervisor(Nola.Plugins.Supervisor, [], [name: Nola.Plugins.Supervisor]),
supervisor(IRC.Connection.Supervisor, [], [name: IRC.Connection.Supervisor]),
supervisor(IRC.PuppetConnection.Supervisor, [], [name: IRC.PuppetConnection.Supervisor]),
]
end
# Start plugins first to let them get on connection events.
def after_start() do
Logger.info("Starting plugins")
Nola.Plugins.start_all()
Logger.info("Starting connections")
IRC.Connection.start_all()
end
end
diff --git a/lib/irc/user_track.ex b/lib/irc/user_track.ex
index 3f144d5..56a319f 100644
--- a/lib/irc/user_track.ex
+++ b/lib/irc/user_track.ex
@@ -1,329 +1,329 @@
defmodule IRC.UserTrack do
@moduledoc """
User Track DB & Utilities
"""
@ets IRC.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}
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}
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}}
account = Nola.Account.lookup(user).id
user = %User{user | account: account}
end
user = touch_struct(user, channel)
if touch && user.account do
- IRC.Membership.touch(user.account, sender.network, channel)
+ 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: IRC.Membership.touch(account, network, chan)
+ 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
- IRC.Membership.touch(user.account, network, channel)
+ 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
- IRC.Membership.touch(user.account, sender.network, channel)
+ 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/matrix/room.ex b/lib/matrix/room.ex
index 4c5cf7e..57f35b8 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 IRC.Memberships ?
+ # 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)
accounts = IRC.UserTrack.channel(state.network, state.channel)
|> Enum.filter(& &1)
|> Enum.map(fn(tuple) -> IRC.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)
{: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})
{: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)
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 cd424ef..4f1e7ff 100644
--- a/lib/nola/account.ex
+++ b/lib/nola/account.ex
@@ -1,263 +1,263 @@
defmodule Nola.Account do
alias IRC.UserTrack.User
@moduledoc """
Account registry....
Maps a network predicate:
* `{net, {:nick, nickname}}`
* `{net, {:account, account}}`
* `{net, {:mask, user@host}}`
to an unique identifier, that can be shared over multiple networks.
If a predicate cannot be found for an existing account, a new account will be made in the database.
To link two existing accounts from different network onto a different one, a merge operation is provided.
"""
# FIXME: Ensure uniqueness of name?
@derive {Poison.Encoder, except: [:token]}
defstruct [:id, :name, :token]
@type t :: %__MODULE__{id: id(), name: String.t()}
@type id :: String.t()
defimpl Inspect, for: __MODULE__ do
import Inspect.Algebra
def inspect(%{id: id, name: name}, opts) do
concat(["#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)
- IRC.Membership.merge_account(old_id, new_id)
+ Nola.Membership.merge_account(old_id, new_id)
IRC.UserTrack.merge_account(old_id, new_id)
IRC.Connection.dispatch("account", {:account_change, old_id, new_id})
IRC.Connection.dispatch("conn", {:account_change, old_id, new_id})
end
:ok
end
@doc "Find an account by a logged in user"
def find_by_nick(network, nick) do
do_lookup(%ExIRC.SenderInfo{nick: nick, network: network}, false)
end
@doc "Always find an account by nickname, even if offline. Uses predicates and then account name."
def find_always_by_nick(network, chan, nick) do
with \
nil <- find_by_nick(network, nick),
nil <- do_lookup(%User{network: network, nick: nick}, false),
nil <- get_by_name(nick)
do
nil
else
%__MODULE__{} = account ->
- memberships = IRC.Membership.of_account(account)
+ 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
lookup(user, make_default)
else
#FIXME this will never work with continued lookup by other methods as Who isn't compatible
lookup_by_nick(sender, :dets.lookup(file("predicates"), {sender.network,{:nick, sender.nick}}), make_default)
end
end
defp do_lookup(sender = %ExIRC.SenderInfo{}, make_default) do
lookup(IRC.UserTrack.find_by_nick(sender), make_default)
end
defp do_lookup(user = %User{account: id}, make_default) when is_binary(id) do
get(id)
end
defp do_lookup(user = %User{network: server, nick: nick}, make_default) do
lookup_by_nick(user, :dets.lookup(file("predicates"), {server,{:nick, nick}}), make_default)
end
defp do_lookup(nil, _) do
nil
end
defp lookup_by_nick(_, [{_, id}], _make_default) do
get(id)
end
defp lookup_by_nick(user, _, make_default) do
#authenticate_by_host(user)
if make_default, do: new_account(user), else: nil
end
def new_account(nick) do
id = EntropyString.large_id()
:dets.insert(file("db"), {id, nick, EntropyString.token()})
get(id)
end
def new_account(%{nick: nick, network: server}) do
id = EntropyString.large_id()
:dets.insert(file("db"), {id, nick, EntropyString.token()})
:dets.insert(file("predicates"), {{server, {:nick, nick}}, id})
get(id)
end
def update_account_name(account = %__MODULE__{id: id}, name) do
account = %__MODULE__{account | name: name}
:dets.insert(file("db"), from_struct(account))
get(id)
end
def get_predicates(%__MODULE__{} = account) do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
:dets.select(file("predicates"), spec)
end
end
diff --git a/lib/irc/membership.ex b/lib/nola/membership.ex
similarity index 99%
rename from lib/irc/membership.ex
rename to lib/nola/membership.ex
index 25a0cfc..b98efd7 100644
--- a/lib/irc/membership.ex
+++ b/lib/nola/membership.ex
@@ -1,129 +1,129 @@
-defmodule IRC.Membership do
+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)
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/plugins/alcoolog.ex b/lib/plugins/alcoolog.ex
index 9958889..738be71 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 = IRC.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
+ notify = Nola.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
for {net, chan} <- notify do
user = IRC.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
extra = " " <> present_type(name, comment) <> ""
IRC.Connection.broadcast_message(net, chan, msg.(nick, extra))
end
miss = cond do
points <= 0.6 -> :small
stats.active30m >= 2.9 && stats.active30m < 3 -> :miss3
stats.active30m >= 1.9 && stats.active30m < 2 -> :miss2
stats.active30m >= 0.9 && stats.active30m < 1 -> :miss1
stats.active30m >= 0.45 && stats.active30m < 0.5 -> :miss05
stats.active30m >= 0.20 && stats.active30m < 0.20 -> :miss025
stats.active30m >= 3 && stats.active1h < 3.15 -> :small3
stats.active30m >= 2 && stats.active1h < 2.15 -> :small2
stats.active30m >= 1.5 && stats.active1h < 1.5 -> :small15
stats.active30m >= 1 && stats.active1h < 1.15 -> :small1
stats.active30m >= 0.5 && stats.active1h <= 0.51 -> :small05
stats.active30m >= 0.25 && stats.active30m <= 0.255 -> :small025
true -> nil
end
if miss do
miss = Nola.Plugins.Txt.random("alcoolog.#{to_string(miss)}")
if miss do
- for {net, chan} <- IRC.Membership.notify_channels(m.account) do
+ for {net, chan} <- Nola.Membership.notify_channels(m.account) do
user = IRC.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
IRC.Connection.broadcast_message(net, chan, "#{nick}: #{miss}")
end
end
end
end
end
def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do
m.replyfun.("!santai <cl> <degrés> [commentaire]")
{:noreply, state}
end
def get_all_stats() do
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
- IRC.Membership.expanded_members_or_friends(account, network, nil)
+ 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
- IRC.Membership.expanded_members(network, channel)
+ 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 = IRC.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
+ 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)
nick = if(user, do: user.nick, else: account.name)
stats = get_full_statistics(state, account.id)
if stats && stats.sober_in > 0 do
now = DateTime.utc_now()
sober = now |> DateTime.add(round(stats.sober_in*60), :second)
|> Timex.Timezone.convert("Europe/Paris")
at = if now.day == sober.day do
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
detail
else
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
detail
end
m.replyfun.("#{nick} sera sobre #{at} (dans #{stats.sober_in_s})!")
else
m.replyfun.("#{nick} est déjà sobre. aidez le !")
end
else
m.replyfun.("inconnu")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :dot}}}, state) do
- nicks = IRC.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
+ 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 = IRC.Membership.members_or_friends(m.account, m.network, m.channel)
+ 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)
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 = IRC.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
+ notify = Nola.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
for {net, chan} <- notify do
user = IRC.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
IRC.Connection.broadcast_message(net, chan, "#{nick} -santai #{points} #{type} #{descr}")
end
{:noreply, state}
_ ->
{:noreply, state}
end
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
{account, duration} = case args do
[nick | rest] -> {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)
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 2baa999..9f66799 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} <- IRC.Membership.notify_channels(account) do
+ for {net, chan} <- Nola.Membership.notify_channels(account) do
user = IRC.UserTrack.find_by_account(net, account)
nick = if(user, do: user.nick, else: account.name)
IRC.Connection.broadcast_message(net, chan, "#{nick}: #{message}")
end
end
end
timer()
#IO.puts "tick stats ok"
{:noreply, {stats,now,dets,ets}}
end
def handle_info(_, state) do
{:noreply, state}
end
defp now() do
DateTime.utc_now()
|> Timex.Timezone.convert("Europe/Paris")
end
defp get_stats() do
Enum.into(Nola.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 4607cbe..68c55ee 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 = IRC.Membership.members(message.network, message.channel)
+ 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)
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/say.ex b/lib/plugins/say.ex
index e036bd4..9bfe1bd 100644
--- a/lib/plugins/say.ex
+++ b/lib/plugins/say.ex
@@ -1,73 +1,73 @@
defmodule Nola.Plugins.Say do
def irc_doc do
"""
# say
Say something...
* **!say `<channel>` `<text>`** say something on `channel`
* **!asay `<channel>` `<text>`** same but anonymously
You must be a member of the channel.
"""
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [type: __MODULE__]
{:ok, _} = Registry.register(Nola.PubSub, "trigger:say", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "trigger:asay", regopts)
{:ok, _} = Registry.register(Nola.PubSub, "messages:private", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, "say", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do
text = Enum.join(text, " ")
say_for(m.account, target, text, true)
{:noreply, state}
end
def handle_info({:irc, :trigger, "asay", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do
text = Enum.join(text, " ")
say_for(m.account, target, text, false)
{:noreply, state}
end
def handle_info({:irc, :text, m = %{text: "say "<>rest}}, state) do
case String.split(rest, " ", parts: 2) do
[target, text] -> say_for(m.account, target, text, true)
_ -> nil
end
{:noreply, state}
end
def handle_info({:irc, :text, m = %{text: "asay "<>rest}}, state) do
case String.split(rest, " ", parts: 2) do
[target, text] -> say_for(m.account, target, text, false)
_ -> nil
end
{:noreply, state}
end
def handle_info(_, state) do
{:noreply, state}
end
defp say_for(account, target, text, with_nick?) do
- for {net, chan} <- IRC.Membership.of_account(account) do
+ for {net, chan} <- Nola.Membership.of_account(account) do
chan2 = String.replace(chan, "#", "")
if (target == "#{net}/#{chan}" || target == "#{net}/#{chan2}" || target == chan || target == chan2) do
if with_nick? do
IRC.send_message_as(account, net, chan, text)
else
IRC.Connection.broadcast_message(net, chan, text)
end
end
end
end
end
diff --git a/lib/telegram/telegram.ex b/lib/telegram/telegram.ex
index a93f5bb..dd23146 100644
--- a/lib/telegram/telegram.ex
+++ b/lib/telegram/telegram.ex
@@ -1,233 +1,233 @@
defmodule Nola.Telegram do
require Logger
@behaviour Telegram.ChatBot
def my_path() do
"https://t.me/beauttebot"
end
def send_message(id, text, md2 \\ false) do
md = if md2, do: "MarkdownV2", else: "Markdown"
token = Keyword.get(Application.get_env(:nola, :telegram, []), :key)
Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(Nola.Telegram, id)
Telegram.Api.request(token, "sendMessage", chat_id: id, text: text, parse_mode: "Markdown")
end
@impl Telegram.ChatBot
def init(chat_id) when chat_id < 0 do
{:ok, state} = Nola.TelegramRoom.init(chat_id)
{:ok, %{room_state: state}}
end
def init(chat_id) do
Logger.info("Telegram session starting: #{chat_id}")
account = Nola.Account.find_meta_account("telegram-id", chat_id)
account_id = if account, do: account.id
{:ok, %{account: account_id}}
end
@impl Telegram.ChatBot
def handle_update(update, token, %{room_state: room_state}) do
{:ok, room_state} = Nola.TelegramRoom.handle_update(update, token, room_state)
{:ok, %{room_state: room_state}}
end
def handle_update(%{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/start"<>_}}, _token, state) do
text = "*Welcome to beautte!*\n\nQuery the bot on IRC and say \"enable-telegram\" to continue."
send_message(m["chat"]["id"], text)
{:ok, %{account: nil}}
end
def handle_update(%{"message" => m = %{"chat" => %{"type" => "private"}, "text" => text = "/enable"<>_}}, _token, state) do
key = case String.split(text, " ") do
["/enable", key | _] -> key
_ -> "nil"
end
#Handled message "1247435154:AAGnSSCnySn0RuVxy_SUcDEoOX_rbF6vdq0" %{"message" =>
# %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
# "date" => 1591027272, "entities" =>
# [%{"length" => 7, "offset" => 0, "type" => "bot_command"}],
# "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
# "message_id" => 11, "text" => "/enable salope"}, "update_id" => 764148578}
account = Nola.Account.find_meta_account("telegram-validation-code", String.downcase(key))
text = if account do
net = Nola.Account.get_meta(account, "telegram-validation-target")
Nola.Account.put_meta(account, "telegram-id", m["chat"]["id"])
Nola.Account.put_meta(account, "telegram-username", m["chat"]["username"])
Nola.Account.put_meta(account, "telegram-username", m["chat"]["username"])
Nola.Account.delete_meta(account, "telegram-validation-code")
Nola.Account.delete_meta(account, "telegram-validation-target")
IRC.Connection.broadcast_message(net, account, "Telegram #{m["chat"]["username"]} account added!")
"Yay! Linked to account **#{account.name}**."
else
"Token invalid"
end
send_message(m["chat"]["id"], text)
{:ok, %{account: account.id}}
end
#[debug] Unhandled update: %{"message" =>
# %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
# "date" => 1591096015,
# "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
# "message_id" => 29,
# "photo" => [
# %{"file_id" => "AgACAgQAAxkBAAMdXtYyz4RQqLcpOlR6xKK3w3NayHAAAnCzMRuL4bFSgl_cTXMl4m5G_T0kXQADAQADAgADbQADZVMBAAEaBA",
# "file_size" => 9544, "file_unique_id" => "AQADRv09JF0AA2VTAQAB", "height" => 95, "width" => 320},
# %{"file_id" => "AgACAgQAAxkBAAMdXtYyz4RQqLcpOlR6xKK3w3NayHAAAnCzMRuL4bFSgl_cTXMl4m5G_T0kXQADAQADAgADeAADZFMBAAEaBA",
# "file_size" => 21420, "file_unique_id" => "AQADRv09JF0AA2RTAQAB", "height" => 148, "width" => 501}]},
# "update_id" => 218161546}
for type <- ~w(photo voice video document animation) do
def handle_update(data = %{"message" => %{unquote(type) => _}}, token, state) do
start_upload(unquote(type), data, token, state)
end
end
#[debug] Unhandled update: %{"callback_query" =>
# %{
# "chat_instance" => "-7948978714441865930", "data" => "evolu.net/#dmz",
# "from" => %{"first_name" => "J", "id" => 2075406, "is_bot" => false, "language_code" => "en", "username" => "ahref"},
# "id" => "8913804780149600",
# "message" => %{"chat" => %{"first_name" => "J", "id" => 2075406, "type" => "private", "username" => "ahref"},
# "date" => 1591098553, "from" => %{"first_name" => "devbeautte", "id" => 1293058838, "is_bot" => true, "username" => "devbeauttebot"},
# "message_id" => 62,
# "reply_markup" => %{"inline_keyboard" => [[%{"callback_data" => "random/#", "text" => "random/#"},
# %{"callback_data" => "evolu.net/#dmz", "text" => "evolu.net/#dmz"}]]},
# "text" => "Where should I send the file?"}
# }
# , "update_id" => 218161568}
#def handle_update(t, %{"callback_query" => cb = %{"data" => "resend", "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}) do
#end
def handle_update(%{"callback_query" => cb = %{"data" => "start-upload:"<>target, "id" => id, "message" => m = %{"message_id" => m_id, "chat" => %{"id" => chat_id}, "reply_to_message" => op}}}, t, state) do
account = Nola.Account.find_meta_account("telegram-id", chat_id)
if account do
target = case String.split(target, "/") do
- ["everywhere"] -> IRC.Membership.of_account(account)
+ ["everywhere"] -> Nola.Membership.of_account(account)
[net, chan] -> [{net, chan}]
end
Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Processing...", reply_markup: %{})
{content, type} = cond do
op["photo"] -> {op["photo"], ""}
op["voice"] -> {op["voice"], " a voice message"}
op["video"] -> {op["video"], ""}
op["document"] -> {op["document"], ""}
op["animation"] -> {op["animation"], ""}
end
file = if is_list(content) && Enum.count(content) > 1 do
Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2)
|> List.first()
else
content
end
file_id = file["file_id"]
file_unique_id = file["file_unique_id"]
text = if(op["caption"], do: ": "<> op["caption"] <> "", else: "")
resend = %{"inline_keyboard" => [ [%{"text" => "re-share", "callback_data" => "resend"}] ]}
spawn(fn() ->
with \
{:ok, file} <- Telegram.Api.request(t, "getFile", file_id: file_id),
path = "https://api.telegram.org/file/bot#{t}/#{file["file_path"]}",
{:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path),
<<smol_body::binary-size(20), _::binary>> = body,
{:ok, magic} <- GenMagic.Pool.perform(Nola.GenMagic, {:bytes, smol_body}),
bucket = Application.get_env(:nola, :s3, []) |> Keyword.get(:bucket),
ext = Path.extname(file["file_path"]),
s3path = "#{account.id}/#{file_unique_id}#{ext}",
Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "*Uploading...*", reply_markup: %{}, parse_mode: "MarkdownV2"),
s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type),
{:ok, _} <- ExAws.request(s3req)
do
path = NolaWeb.Router.Helpers.url(NolaWeb.Endpoint) <> "/files/#{s3path}"
sent = for {net, chan} <- target do
txt = "sent#{type}#{text} #{path}"
IRC.send_message_as(account, net, chan, txt)
"#{net}/#{chan}"
end
if caption = op["caption"], do: as_irc_message(chat_id, caption, account)
text = "Sent on " <> Enum.join(sent, ", ") <> " !"
Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "_Sent!_", reply_markup: %{}, parse_mode: "MarkdownV2")
else
error ->
Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Something failed.", reply_markup: %{}, parse_mode: "MarkdownV2")
Logger.error("Failed upload from Telegram: #{inspect error}")
end
end)
end
{:ok, state}
end
def handle_update(%{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}, "text" => text}}, _, state) do
account = Nola.Account.find_meta_account("telegram-id", id)
if account do
as_irc_message(id, text, account)
end
{:ok, state}
end
def handle_update(m, _, state) do
Logger.debug("Unhandled update: #{inspect m}")
{:ok, state}
end
@impl Telegram.ChatBot
def handle_info(info, %{room_state: room_state}) do
{:ok, room_state} = Nola.TelegramRoom.handle_info(info, room_state)
{:ok, %{room_state: room_state}}
end
def handle_info(_info, state) do
{:ok, state}
end
defp as_irc_message(id, text, account) do
reply_fun = fn(text) -> send_message(id, text) end
trigger_text = cond do
String.starts_with?(text, "/") ->
"/"<>text = text
"!"<>text
Enum.any?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(text, trigger) end) ->
text
true ->
"!"<>text
end
message = %IRC.Message{
id: FlakeId.get(),
transport: :telegram,
network: "telegram",
channel: nil,
text: text,
account: account,
sender: %ExIRC.SenderInfo{nick: account.name},
replyfun: reply_fun,
trigger: IRC.Connection.extract_trigger(trigger_text),
at: nil
}
IRC.Connection.publish(message, ["messages:private", "messages:telegram", "telegram/#{account.id}:messages"])
message
end
defp start_upload(_type, %{"message" => m = %{"chat" => %{"id" => id, "type" => "private"}}}, token, state) do
account = Nola.Account.find_meta_account("telegram-id", id)
if account do
text = if(m["text"], do: m["text"], else: nil)
- targets = IRC.Membership.of_account(account)
+ targets = Nola.Membership.of_account(account)
|> Enum.map(fn({net, chan}) -> "#{net}/#{chan}" end)
|> Enum.map(fn(i) -> %{"text" => i, "callback_data" => "start-upload:#{i}"} end)
kb = if Enum.count(targets) > 1 do
[%{"text" => "everywhere", "callback_data" => "start-upload:everywhere"}] ++ targets
else
targets
end
|> Enum.chunk_every(2)
keyboard = %{"inline_keyboard" => kb}
Telegram.Api.request(token, "sendMessage", chat_id: id, text: "Where should I send this file?", reply_markup: keyboard, reply_to_message_id: m["message_id"], parse_mode: "MarkdownV2")
end
{:ok, state}
end
end
diff --git a/lib/web/context_plug.ex b/lib/web/context_plug.ex
index 7289763..fcdf42f 100644
--- a/lib/web/context_plug.ex
+++ b/lib/web/context_plug.ex
@@ -1,92 +1,92 @@
defmodule NolaWeb.ContextPlug do
import Plug.Conn
import Phoenix.Controller
def init(opts \\ []) do
opts || []
end
def get_account(conn) do
cond do
get_session(conn, :account) -> get_session(conn, :account)
get_session(conn, :oidc_id) -> if account = Nola.Account.find_meta_account("identity-id", get_session(conn, :oidc_id)), do: account.id
true -> nil
end
end
def call(conn, opts) do
account = with \
{:account, account_id} when is_binary(account_id) <- {:account, get_account(conn)},
{:account, account} when not is_nil(account) <- {:account, Nola.Account.get(account_id)}
do
account
else
_ -> nil
end
network = Map.get(conn.params, "network")
network = if network == "-", do: nil, else: network
oidc_account = Nola.Account.find_meta_account("identity-id", get_session(conn, :oidc_id))
conns = IRC.Connection.get_network(network)
chan = if c = Map.get(conn.params, "chan") do
NolaWeb.reformat_chan(c)
end
chan_conn = IRC.Connection.get_network(network, chan)
memberships = if account do
- IRC.Membership.of_account(account)
+ Nola.Membership.of_account(account)
end
auth_required = cond do
Keyword.get(opts, :restrict) == :public -> false
account == nil -> true
network == nil -> false
Keyword.get(opts, :restrict) == :logged_in -> false
network && chan ->
!Enum.member?(memberships, {network, chan})
network ->
!Enum.any?(memberships, fn({n, _}) -> n == network end)
end
bot = cond do
network && chan && chan_conn -> chan_conn.nick
network && conns -> conns.nick
true -> nil
end
cond do
account && auth_required ->
conn
|> put_status(404)
|> text("Page not found")
|> halt()
auth_required ->
conn
|> put_status(403)
|> render(NolaWeb.AlcoologView, "auth.html", bot: bot, no_header: true, network: network)
|> halt()
(network && !conns) ->
conn
|> put_status(404)
|> text("Page not found")
|> halt()
(chan && !chan_conn) ->
conn
|> put_status(404)
|> text("Page not found")
|> halt()
true ->
conn = conn
|> assign(:network, network)
|> assign(:chan, chan)
|> assign(:bot, bot)
|> assign(:account, account)
|> assign(:oidc_account, oidc_account)
|> assign(:memberships, memberships)
end
end
end
diff --git a/lib/web/controllers/alcoolog_controller.ex b/lib/web/controllers/alcoolog_controller.ex
index dc09517..6337da5 100644
--- a/lib/web/controllers/alcoolog_controller.ex
+++ b/lib/web/controllers/alcoolog_controller.ex
@@ -1,323 +1,323 @@
defmodule NolaWeb.AlcoologController do
use NolaWeb, :controller
require Logger
plug NolaWeb.ContextPlug when action not in [:token]
plug NolaWeb.ContextPlug, [restrict: :public] when action in [:token]
def token(conn, %{"token" => token}) do
case Nola.Token.lookup(token) do
{:ok, {:alcoolog, :index, network, channel}} -> index(conn, nil, network, channel)
err ->
Logger.debug("AlcoologControler: token #{inspect err} invalid")
conn
|> put_status(404)
|> text("Page not found")
end
end
def nick(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
days = String.to_integer(Map.get(params, "days", "180"))
- friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
+ friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
if friend? do
stats = Nola.Plugins.Alcoolog.get_full_statistics(profile_account.id)
history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- Nola.Plugins.Alcoolog.nick_history(profile_account) do
%{
at: ts |> DateTime.from_unix!(:millisecond),
points: points,
active: active,
cl: cl,
deg: deg,
type: type,
description: descr,
meta: meta
}
end
history = Enum.sort(history, &(DateTime.compare(&1.at, &2.at) != :lt))
|> IO.inspect()
conn
|> assign(:title, "alcoolog #{nick}")
|> render("user.html", network: network, profile: profile_account, days: days, nick: nick, history: history, stats: stats)
else
conn
|> put_status(404)
|> text("Page not found")
end
end
def nick_stats_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
- friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
+ friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
if friend? do
stats = Nola.Plugins.Alcoolog.get_full_statistics(profile_account.id)
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(stats))
else
conn
|> put_status(404)
|> json([])
end
end
def nick_gls_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
- friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
+ friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
count = String.to_integer(Map.get(params, "days", "180"))
if friend? do
data = Nola.Plugins.Alcoolog.user_over_time_gl(profile_account, count)
delay = count*((24 * 60)*60)
now = DateTime.utc_now()
start_date = DateTime.utc_now()
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
|> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
|> DateTime.to_date()
|> Date.to_erl()
filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
|> Enum.to_list
|> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
|> Enum.map(&Date.from_erl!(&1))
|> Enum.map(fn(date) ->
%{date: date, gls: Map.get(data, date, 0)}
end)
|> Enum.sort(&(Date.compare(&1.date, &2.date) != :gt))
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(filled))
else
conn
|> put_status(404)
|> json([])
end
end
def nick_volumes_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
- friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
+ friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
count = String.to_integer(Map.get(params, "days", "180"))
if friend? do
data = Nola.Plugins.Alcoolog.user_over_time(profile_account, count)
delay = count*((24 * 60)*60)
now = DateTime.utc_now()
start_date = DateTime.utc_now()
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
|> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
|> DateTime.to_date()
|> Date.to_erl()
filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
|> Enum.to_list
|> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
|> Enum.map(&Date.from_erl!(&1))
|> Enum.map(fn(date) ->
%{date: date, volumes: Map.get(data, date, 0)}
end)
|> Enum.sort(&(Date.compare(&1.date, &2.date) != :gt))
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(filled))
else
conn
|> put_status(404)
|> json([])
end
end
def nick_log_json(conn = %{assigns: %{account: account}}, %{"network" => network, "nick" => nick}) do
profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
- friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
+ friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
if friend? do
history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- Nola.Plugins.Alcoolog.nick_history(profile_account) do
%{
at: ts |> DateTime.from_unix!(:millisecond) |> DateTime.to_iso8601(),
points: points,
active: active,
cl: cl,
deg: deg,
type: type,
description: descr,
meta: meta
}
end
last = List.last(history)
{_, active} = Nola.Plugins.Alcoolog.user_stats(profile_account)
last = %{last | active: active, at: DateTime.utc_now() |> DateTime.to_iso8601()}
history = history ++ [last]
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(history))
else
conn
|> put_status(404)
|> json([])
end
end
def nick_history_json(conn = %{assigns: %{account: account}}, %{"network" => network, "nick" => nick}) do
profile_account = Nola.Account.find_always_by_nick(network, nick, nick)
- friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
+ friend? = Enum.member?(Nola.Membership.friends(account), profile_account.id)
if friend? do
history = for {_, date, value} <- Nola.Plugs.AlcoologAnnouncer.log(profile_account) do
%{date: DateTime.to_iso8601(date), value: value}
end
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(history))
else
conn
|> put_status(404)
|> json([])
end
end
def index(conn = %{assigns: %{account: account}}, %{"network" => network, "chan" => channel}) do
index(conn, account, network, NolaWeb.reformat_chan(channel))
end
def index(conn = %{assigns: %{account: account}}, _) do
index(conn, account, nil, nil)
end
#def index(conn, params) do
# network = Map.get(params, "network")
# chan = if c = Map.get(params, "chan") do
# NolaWeb.reformat_chan(c)
# end
# irc_conn = if network do
# IRC.Connection.get_network(network, chan)
# end
# bot = if(irc_conn, do: irc_conn.nick)#
#
# conn
# |> put_status(403)
# |> render("auth.html", network: network, channel: chan, irc_conn: conn, bot: bot)
#end
def index(conn, account, network, channel) do
aday = ((24 * 60)*60)
now = DateTime.utc_now()
before7 = now
|> DateTime.add(-(7*aday), :second)
|> DateTime.to_unix(:millisecond)
before15 = now
|> DateTime.add(-(15*aday), :second)
|> DateTime.to_unix(:millisecond)
before31 = now
|> DateTime.add(-(31*aday), :second)
|> DateTime.to_unix(:millisecond)
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
match = [
{{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_},
[
{:>, :"$1", {:const, before15}},
], [:"$_"]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
- members = IRC.Membership.expanded_members_or_friends(account, network, channel)
+ members = Nola.Membership.expanded_members_or_friends(account, network, channel)
members_ids = Enum.map(members, fn({account, _, nick}) -> account.id end)
member_names = Enum.reduce(members, %{}, fn({account, _, nick}, acc) -> Map.put(acc, account.id, nick) end)
drinks = :ets.select(Nola.Plugins.Alcoolog.ETS, match)
|> Enum.filter(fn({{account, _}, _vol, _cur, _cl, _deg, _name, _cmt, _meta}) -> Enum.member?(members_ids, account) end)
|> Enum.map(fn({{account, _}, _, _, _, _, _, _, _} = object) -> {object, Map.get(member_names, account)} end)
|> Enum.sort_by(fn({{{_, ts}, _, _, _, _, _, _, _}, _}) -> ts end, &>/2)
stats = Nola.Plugins.Alcoolog.get_channel_statistics(account, network, channel)
top = Enum.reduce(drinks, %{}, fn({{{account_id, _}, vol, _, _, _, _, _, _}, _}, acc) ->
nick = Map.get(member_names, account_id)
all = Map.get(acc, nick, 0)
Map.put(acc, nick, all + vol)
end)
|> Enum.sort_by(fn({_nick, count}) -> count end, &>/2)
# {date, single_peak}
#
conn
|> assign(:title, "alcoolog")
|> render("index.html", network: network, channel: channel, drinks: drinks, top: top, stats: stats)
end
def index_gls_json(conn = %{assigns: %{account: account}}, %{"network" => network, "chan" => channel}) do
count = 30
channel = NolaWeb.reformat_chan(channel)
- members = IRC.Membership.expanded_members_or_friends(account, network, channel)
+ members = Nola.Membership.expanded_members_or_friends(account, network, channel)
members_ids = Enum.map(members, fn({account, _, nick}) -> account.id end)
member_names = Enum.reduce(members, %{}, fn({account, _, nick}, acc) -> Map.put(acc, account.id, nick) end)
delay = count*((24 * 60)*60)
now = DateTime.utc_now()
start_date = DateTime.utc_now()
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
|> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
|> DateTime.to_date()
|> Date.to_erl()
filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
|> Enum.to_list
|> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
|> Enum.map(&Date.from_erl!(&1))
|> Enum.map(fn(date) ->
{date, (for {a, _, _} <- members, into: Map.new, do: {Map.get(member_names, a.id, a.id), 0})}
end)
|> Enum.into(Map.new)
gls = Enum.reduce(members, filled, fn({account, _, _}, gls) ->
Enum.reduce(Nola.Plugins.Alcoolog.user_over_time_gl(account, count), gls, fn({date, gl}, gls) ->
u = Map.get(gls, date, %{})
|> Map.put(Map.get(member_names, account.id, account.id), gl)
Map.put(gls, date, u)
end)
end)
dates = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
|> Enum.to_list
|> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
|> Enum.map(&Date.from_erl!(&1))
filled2 = Enum.map(member_names, fn({_, name}) ->
history = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
|> Enum.to_list
|> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
|> Enum.map(&Date.from_erl!(&1))
|> Enum.map(fn(date) ->
get_in(gls, [date, name]) #%{date: date, gl: get_in(gls, [date, name])}
end)
if Enum.all?(history, fn(x) -> x == 0 end) do
nil
else
%{name: name, history: history}
end
end)
|> Enum.filter(fn(x) -> x end)
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(%{labels: dates, data: filled2}))
end
def minisync(conn, %{"user_id" => user_id, "key" => key, "value" => value}) do
account = Nola.Account.get(user_id)
if account do
ds = Nola.Plugins.Alcoolog.data_state()
meta = Nola.Plugins.Alcoolog.get_user_meta(ds, account.id)
case Float.parse(value) do
{val, _} ->
new_meta = Map.put(meta, String.to_existing_atom(key), val)
Nola.Plugins.Alcoolog.put_user_meta(ds, account.id, new_meta)
_ ->
conn
|> put_status(:unprocessable_entity)
|> text("invalid value")
end
else
conn
|> put_status(:not_found)
|> text("not found")
end
end
end
diff --git a/lib/web/controllers/irc_controller.ex b/lib/web/controllers/irc_controller.ex
index d6114e6..441cbe7 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)
true ->
- IRC.Membership.of_account(conn.assigns.account)
+ 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 c5d0a57..a6b85b6 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 = IRC.Membership.of_account(account)
+ memberships = Nola.Membership.of_account(account)
users = IRC.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 0513cc2..2d5e289 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 = IRC.Membership.of_account(Nola.Account.get("DRgpD4fLf8PDJMLp8Dtb"))
+ 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)
|> 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
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
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Mar 14, 10:56 AM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
33572
Default Alt Text
(135 KB)
Attached To
rNOLA Nola
Event Timeline
Log In to Comment