Page MenuHomePhabricator

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/lib/irc/account.ex b/lib/irc/account.ex
index 46c7f6e..45680f8 100644
--- a/lib/irc/account.ex
+++ b/lib/irc/account.ex
@@ -1,451 +1,451 @@
defmodule IRC.Account do
alias IRC.UserTrack.User
@moduledoc """
Account registry....
Maps a network predicate:
* `{net, {:nick, nickname}}`
* `{net, {:account, account}}`
* `{net, {:mask, user@host}}`
to an unique identifier, that can be shared over multiple networks.
If a predicate cannot be found for an existing account, a new account will be made in the database.
To link two existing accounts from different network onto a different one, a merge operation is provided.
"""
# FIXME: Ensure uniqueness of name?
@derive {Poison.Encoder, except: [:token]}
defstruct [:id, :name, :token]
@type t :: %__MODULE__{id: id(), name: String.t()}
@type id :: String.t()
defimpl Inspect, for: __MODULE__ do
import Inspect.Algebra
def inspect(%{id: id, name: name}, opts) do
concat(["#IRC.Account[", id, " ", name, "]"])
end
end
def file(base) do
- to_charlist(LSG.data_path() <> "/account_#{base}.dets")
+ to_charlist(Nola.data_path() <> "/account_#{base}.dets")
end
defp from_struct(%__MODULE__{id: id, name: name, token: token}) do
{id, name, token}
end
defp from_tuple({id, name, token}) do
%__MODULE__{id: id, name: name, token: token}
end
def start_link() do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def init(_) do
{:ok, accounts} = :dets.open_file(file("db"), [])
{:ok, meta} = :dets.open_file(file("meta"), [])
{:ok, predicates} = :dets.open_file(file("predicates"), [{:type, :set}])
{:ok, %{accounts: accounts, meta: meta, predicates: predicates}}
end
def get(id) do
case :dets.lookup(file("db"), id) do
[account] -> from_tuple(account)
_ -> nil
end
end
def get_by_name(name) do
spec = [{{:_, :"$1", :_}, [{:==, :"$1", {:const, name}}], [:"$_"]}]
case :dets.select(file("db"), spec) do
[account] -> from_tuple(account)
_ -> nil
end
end
def get_meta(%__MODULE__{id: id}, key, default \\ nil) do
case :dets.lookup(file("meta"), {id, key}) do
[{_, value}] -> (value || default)
_ -> default
end
end
@spec find_meta_accounts(String.t()) :: [{account :: t(), value :: String.t()}, ...]
@doc "Find all accounts that have a meta of `key`."
def find_meta_accounts(key) do
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$2", {:const, key}}], [{{:"$1", :"$3"}}]}]
for {id, val} <- :dets.select(file("meta"), spec), do: {get(id), val}
end
@doc "Find an account given a specific meta `key` and `value`."
@spec find_meta_account(String.t(), String.t()) :: t() | nil
def find_meta_account(key, value) do
#spec = [{{{:"$1", :"$2"}, :"$3"}, [:andalso, {:==, :"$2", {:const, key}}, {:==, :"$3", {:const, value}}], [:"$1"]}]
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:andalso, {:==, :"$2", {:const, key}}, {:==, {:const, value}, :"$3"}}], [:"$1"]}]
case :dets.select(file("meta"), spec) do
[id] -> get(id)
_ -> nil
end
end
def get_all_meta(%__MODULE__{id: id}) do
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$1", {:const, id}}], [{{:"$2", :"$3"}}]}]
:dets.select(file("meta"), spec)
end
def put_user_meta(account = %__MODULE__{}, key, value) do
put_meta(account, "u:"<>key, value)
end
def put_meta(%__MODULE__{id: id}, key, value) do
:dets.insert(file("meta"), {{id, key}, value})
end
def delete_meta(%__MODULE__{id: id}, key) do
:dets.delete(file("meta"), {id, key})
end
def all_accounts() do
:dets.traverse(file("db"), fn(obj) -> {:continue, from_tuple(obj)} end)
end
def all_predicates() do
:dets.traverse(file("predicates"), fn(obj) -> {:continue, obj} end)
end
def all_meta() do
:dets.traverse(file("meta"), fn(obj) -> {:continue, obj} end)
end
def merge_account(old_id, new_id) do
if old_id != new_id do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, old_id}}], [:"$1"]}]
predicates = :dets.select(file("predicates"), spec)
for pred <- predicates, do: :ok = :dets.insert(file("predicates"), {pred, new_id})
spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$1", {:const, old_id}}], [{{:"$2", :"$3"}}]}]
metas = :dets.select(file("meta"), spec)
for {k,v} <- metas do
:dets.delete(file("meta"), {{old_id, k}})
:ok = :dets.insert(file("meta"), {{new_id, k}, v})
end
:dets.delete(file("db"), old_id)
IRC.Membership.merge_account(old_id, new_id)
IRC.UserTrack.merge_account(old_id, new_id)
IRC.Connection.dispatch("account", {:account_change, old_id, new_id})
IRC.Connection.dispatch("conn", {:account_change, old_id, new_id})
end
:ok
end
@doc "Find an account by a logged in user"
def find_by_nick(network, nick) do
do_lookup(%ExIRC.SenderInfo{nick: nick, network: network}, false)
end
@doc "Always find an account by nickname, even if offline. Uses predicates and then account name."
def find_always_by_nick(network, chan, nick) do
with \
nil <- find_by_nick(network, nick),
nil <- do_lookup(%User{network: network, nick: nick}, false),
nil <- get_by_name(nick)
do
nil
else
%__MODULE__{} = account ->
memberships = IRC.Membership.of_account(account)
if Enum.any?(memberships, fn({net, ch}) -> (net == network) or (chan && chan == ch) end) do
account
else
nil
end
end
end
def find(something) do
do_lookup(something, false)
end
def lookup(something, make_default \\ true) do
account = do_lookup(something, make_default)
if account && Map.get(something, :nick) do
IRC.Connection.dispatch("account", {:account_auth, Map.get(something, :nick), account.id})
end
account
end
def handle_info(_, state) do
{:noreply, state}
end
def handle_cast(_, state) do
{:noreply, state}
end
def handle_call(_, _, state) do
{:noreply, state}
end
def terminate(_, state) do
for {_, dets} <- state do
:dets.sync(dets)
:dets.close(dets)
end
end
defp do_lookup(message = %IRC.Message{account: account_id}, make_default) when is_binary(account_id) do
get(account_id)
end
defp do_lookup(sender = %ExIRC.Who{}, make_default) do
if user = IRC.UserTrack.find_by_nick(sender) do
lookup(user, make_default)
else
#FIXME this will never work with continued lookup by other methods as Who isn't compatible
lookup_by_nick(sender, :dets.lookup(file("predicates"), {sender.network,{:nick, sender.nick}}), make_default)
end
end
defp do_lookup(sender = %ExIRC.SenderInfo{}, make_default) do
lookup(IRC.UserTrack.find_by_nick(sender), make_default)
end
defp do_lookup(user = %User{account: id}, make_default) when is_binary(id) do
get(id)
end
defp do_lookup(user = %User{network: server, nick: nick}, make_default) do
lookup_by_nick(user, :dets.lookup(file("predicates"), {server,{:nick, nick}}), make_default)
end
defp do_lookup(nil, _) do
nil
end
defp lookup_by_nick(_, [{_, id}], _make_default) do
get(id)
end
defp lookup_by_nick(user, _, make_default) do
#authenticate_by_host(user)
if make_default, do: new_account(user), else: nil
end
def new_account(nick) do
id = EntropyString.large_id()
:dets.insert(file("db"), {id, nick, EntropyString.token()})
get(id)
end
def new_account(%{nick: nick, network: server}) do
id = EntropyString.large_id()
:dets.insert(file("db"), {id, nick, EntropyString.token()})
:dets.insert(file("predicates"), {{server, {:nick, nick}}, id})
get(id)
end
def update_account_name(account = %__MODULE__{id: id}, name) do
account = %__MODULE__{account | name: name}
:dets.insert(file("db"), from_struct(account))
get(id)
end
def get_predicates(%__MODULE__{} = account) do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
:dets.select(file("predicates"), spec)
end
defmodule AccountPlugin do
@moduledoc """
# Account
* **account** Get current account id and token
* **auth `<account-id>` `<token>`** Authenticate and link the current nickname to an account
* **auth** list authentications methods
* **whoami** list currently authenticated users
* **web** get a one-time login link to web
* **enable-telegram** Link a Telegram account
* **enable-sms** Link a SMS number
* **enable-untappd** Link a Untappd account
* **set-name** set account name
* **setusermeta puppet-nick `<nick>`** Set puppet IRC nickname
"""
def irc_doc, do: @moduledoc
def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "messages:private", [])
{:ok, nil}
end
def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "help"}}, state) do
text = [
"account: show current account and auth token",
"auth: show authentications methods",
"whoami: list authenticated users",
"set-name <name>: set account name",
"web: login to web",
"enable-sms | disable-sms: enable/change or disable sms",
"enable-telegram: link/change telegram",
"enable-untappd: link untappd account",
"getmeta: show meta datas",
"setusermeta: set user meta",
]
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "auth"}}, state) do
spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}]
predicates = :dets.select(IRC.Account.file("predicates"), spec)
text = for {net, {key, value}} <- predicates, do: "#{net}: #{to_string(key)}: #{value}"
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "whoami"}}, state) do
users = for user <- IRC.UserTrack.find_by_account(m.account) do
chans = Enum.map(user.privileges, fn({chan, _}) -> chan end)
|> Enum.join(" ")
"#{user.network} - #{user.nick}!#{user.username}@#{user.host} - #{chans}"
end
m.replyfun.(users)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "account"}}, state) do
account = IRC.Account.lookup(m.sender)
text = ["Account Id: #{account.id}",
"Authenticate to this account from another network: \"auth #{account.id} #{account.token}\" to the other bot!"]
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{sender: sender, text: "auth"<>_}}, state) do
#account = IRC.Account.lookup(m.sender)
case String.split(m.text, " ") do
["auth", id, token] ->
join_account(m, id, token)
_ ->
m.replyfun.("Invalid parameters")
end
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "set-name "<>name}}, state) do
IRC.Account.update_account_name(account, name)
m.replyfun.("Name changed: #{name}")
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "disable-sms"}}, state) do
if IRC.Account.get_meta(m.account, "sms-number") do
IRC.Account.delete_meta(m.account, "sms-number")
m.replfyun.("SMS disabled.")
else
m.replyfun.("SMS already disabled.")
end
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "web"}}, state) do
auth_url = Untappd.auth_url()
- login_url = LSG.AuthToken.new_url(m.account.id, nil)
+ login_url = Nola.AuthToken.new_url(m.account.id, nil)
m.replyfun.("-> " <> login_url)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "enable-sms"}}, state) do
code = String.downcase(EntropyString.small_id())
IRC.Account.put_meta(m.account, "sms-validation-code", code)
IRC.Account.put_meta(m.account, "sms-validation-target", m.network)
- number = LSG.IRC.SmsPlugin.my_number()
+ number = Nola.IRC.SmsPlugin.my_number()
text = "To enable or change your number for SMS messaging, please send:"
<> " \"enable #{code}\" to #{number}"
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "enable-telegram"}}, state) do
code = String.downcase(EntropyString.small_id())
IRC.Account.delete_meta(m.account, "telegram-id")
IRC.Account.put_meta(m.account, "telegram-validation-code", code)
IRC.Account.put_meta(m.account, "telegram-validation-target", m.network)
- text = "To enable or change your number for telegram messaging, please open #{LSG.Telegram.my_path()} and send:"
+ text = "To enable or change your number for telegram messaging, please open #{Nola.Telegram.my_path()} and send:"
<> " \"/enable #{code}\""
m.replyfun.(text)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "enable-untappd"}}, state) do
auth_url = Untappd.auth_url()
- login_url = LSG.AuthToken.new_url(m.account.id, {:external_redirect, auth_url})
+ login_url = Nola.AuthToken.new_url(m.account.id, {:external_redirect, auth_url})
m.replyfun.(["To link your Untappd account, open this URL:", login_url])
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "getmeta"<>_}}, state) do
result = case String.split(m.text, " ") do
["getmeta"] ->
for {k, v} <- IRC.Account.get_all_meta(m.account) do
case k do
"u:"<>key -> "(user) #{key}: #{v}"
key -> "#{key}: #{v}"
end
end
["getmeta", key] ->
value = IRC.Account.get_meta(m.account, key)
text = if value do
"#{key}: #{value}"
else
"#{key} is not defined"
end
_ ->
"usage: getmeta [key]"
end
m.replyfun.(result)
{:noreply, state}
end
def handle_info({:irc, :text, m = %IRC.Message{text: "setusermet"<>_}}, state) do
result = case String.split(m.text, " ") do
["setusermeta", key, value] ->
IRC.Account.put_user_meta(m.account, key, value)
"ok"
_ ->
"usage: setusermeta <key> <value>"
end
m.replyfun.(result)
{:noreply, state}
end
def handle_info(_, state) do
{:noreply, state}
end
defp join_account(m, id, token) do
old_account = IRC.Account.lookup(m.sender)
new_account = IRC.Account.get(id)
if new_account && token == new_account.token do
case IRC.Account.merge_account(old_account.id, new_account.id) do
:ok ->
if old_account.id == new_account.id do
m.replyfun.("Already authenticated, but hello")
else
m.replyfun.("Accounts merged!")
end
_ -> m.replyfun.("Something failed :(")
end
else
m.replyfun.("Invalid token")
end
end
end
end
diff --git a/lib/irc/connection.ex b/lib/irc/connection.ex
index 9382714..86d8279 100644
--- a/lib/irc/connection.ex
+++ b/lib/irc/connection.ex
@@ -1,521 +1,521 @@
defmodule IRC.Connection do
require Logger
use Ecto.Schema
@moduledoc """
# IRC Connection
Provides a nicer abstraction over ExIRC's handlers.
## Start connections
```
IRC.Connection.start_link(host: "irc.random.sh", port: 6697, nick: "pouetbot", channels: ["#dev"])
## PubSub topics
* `account` -- accounts change
* {:account_change, old_account_id, new_account_id} # Sent when account merged
* {:accounts, [{:account, network, channel, nick, account_id}] # Sent on bot join
* {:account, network, nick, account_id} # Sent on user join
* `message` -- aill messages (without triggers)
* `message:private` -- all messages without a channel
* `message:#CHANNEL` -- all messages within `#CHANNEL`
* `triggers` -- all triggers
* `trigger:TRIGGER` -- any message with a trigger `TRIGGER`
## Replying to %IRC.Message{}
Each `IRC.Message` comes with a dedicated `replyfun`, to which you only have to pass either:
"""
def irc_doc, do: nil
@min_backoff :timer.seconds(5)
@max_backoff :timer.seconds(2*60)
embedded_schema do
field :network, :string
field :host, :string
field :port, :integer
field :nick, :string
field :user, :string
field :name, :string
field :pass, :string
field :tls, :boolean, default: false
field :channels, {:array, :string}, default: []
end
defmodule Supervisor do
use DynamicSupervisor
def start_link() do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def start_child(%IRC.Connection{} = conn) do
spec = %{id: conn.id, start: {IRC.Connection, :start_link, [conn]}, restart: :transient}
DynamicSupervisor.start_child(__MODULE__, spec)
end
@impl true
def init(_init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
max_restarts: 10,
max_seconds: 1
)
end
end
def changeset(params) do
import Ecto.Changeset
%__MODULE__{id: EntropyString.large_id()}
|> cast(params, [:network, :host, :port, :nick, :user, :name, :pass, :channels, :tls])
|> validate_required([:host, :port, :nick, :user, :name])
|> apply_action(:insert)
end
def to_tuple(%__MODULE__{} = conn) do
{conn.id, conn.network, conn.host, conn.port, conn.nick, conn.user, conn.name, conn.pass, conn.tls, conn.channels, nil}
end
def from_tuple({id, network, host, port, nick, user, name, pass, tls, channels, _}) do
%__MODULE__{id: id, network: network, host: host, port: port, nick: nick, user: user, name: name, pass: pass, tls: tls, channels: channels}
end
## -- MANAGER API
def setup() do
:dets.open_file(dets(), [])
end
- def dets(), do: to_charlist(LSG.data_path("/connections.dets"))
+ def dets(), do: to_charlist(Nola.data_path("/connections.dets"))
def lookup(id) do
case :dets.lookup(dets(), id) do
[object | _] -> from_tuple(object)
_ -> nil
end
end
def connections() do
:dets.foldl(fn(object, acc) -> [from_tuple(object) | acc] end, [], dets())
end
def start_all() do
for conn <- connections(), do: {conn, IRC.Connection.Supervisor.start_child(conn)}
end
def get_network(network, channel \\ nil) do
spec = [{{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[{:==, :"$1", {:const, network}}], [:"$_"]}]
results = Enum.map(:dets.select(dets(), spec), fn(object) -> from_tuple(object) end)
if channel do
Enum.find(results, fn(conn) -> Enum.member?(conn.channels, channel) end)
else
List.first(results)
end
end
def get_host_nick(host, port, nick) do
spec = [{{:_, :_, :"$1", :"$2", :"$3", :_, :_, :_, :_, :_, :_},
[{:andalso,
{:andalso, {:==, :"$1", {:const, host}}, {:==, :"$2", {:const, port}}},
{:==, :"$3", {:const, nick}}}],
[:"$_"]}
]
case :dets.select(dets(), spec) do
[object] -> from_tuple(object)
[] -> nil
end
end
def delete_connection(%__MODULE__{id: id} = conn) do
:dets.delete(dets(), id)
stop_connection(conn)
:ok
end
def start_connection(%__MODULE__{} = conn) do
IRC.Connection.Supervisor.start_child(conn)
end
def stop_connection(%__MODULE__{id: id}) do
case :global.whereis_name(id) do
pid when is_pid(pid) ->
GenServer.stop(pid, :normal)
_ -> :error
end
end
def add_connection(opts) do
case changeset(opts) do
{:ok, conn} ->
if existing = get_host_nick(conn.host, conn.port, conn.nick) do
{:error, {:existing, conn}}
else
:dets.insert(dets(), to_tuple(conn))
IRC.Connection.Supervisor.start_child(conn)
end
error -> error
end
end
def update_connection(connection) do
:dets.insert(dets(), to_tuple(connection))
end
def start_link(conn) do
GenServer.start_link(__MODULE__, [conn], name: {:global, conn.id})
end
def broadcast_message(net, chan, message) do
dispatch("conn", {:broadcast, net, chan, message}, IRC.ConnectionPubSub)
end
def broadcast_message(list, message) when is_list(list) do
for {net, chan} <- list do
broadcast_message(net, chan, message)
end
end
def privmsg(channel, line) do
GenServer.cast(__MODULE__, {:privmsg, channel, line})
end
def init([conn]) do
Logger.metadata(conn: conn.id)
backoff = :backoff.init(@min_backoff, @max_backoff)
|> :backoff.type(:jitter)
{:ok, %{client: nil, backoff: backoff, conn: conn, connected_server: nil, connected_port: nil, network: conn.network}, {:continue, :connect}}
end
@triggers %{
"!" => :bang,
"+" => :plus,
"-" => :minus,
"?" => :query,
"." => :dot,
"~" => :tilde,
"@" => :at,
"++" => :plus_plus,
"--" => :minus_minus,
"!!" => :bang_bang,
"??" => :query_query,
".." => :dot_dot,
"~~" => :tilde_tilde,
"@@" => :at_at
}
def handle_continue(:connect, state) do
client_opts = []
|> Keyword.put(:network, state.conn.network)
{:ok, _} = Registry.register(IRC.ConnectionPubSub, "conn", [])
client = if state.client && Process.alive?(state.client) do
Logger.info("Reconnecting client")
state.client
else
Logger.info("Connecting")
{:ok, client} = ExIRC.Client.start_link(debug: false)
ExIRC.Client.add_handler(client, self())
client
end
opts = [{:nodelay, true}]
conn_fun = if state.conn.tls, do: :connect_ssl!, else: :connect!
apply(ExIRC.Client, conn_fun, [client, to_charlist(state.conn.host), state.conn.port, opts])
{:noreply, %{state | client: client}}
end
def handle_info(:disconnected, state) do
{delay, backoff} = :backoff.fail(state.backoff)
Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect delay}ms")
Process.send_after(self(), :connect, delay)
{:noreply, %{state | backoff: backoff}}
end
def handle_info(:connect, state) do
{:noreply, state, {:continue, :connect}}
end
def handle_cast({:privmsg, channel, line}, state) do
irc_reply(state, {channel, nil}, line)
{:noreply, state}
end
# Connection successful
def handle_info({:connected, server, port}, state) do
Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect state}")
{_, backoff} = :backoff.succeed(state.backoff)
ExIRC.Client.logon(state.client, state.conn.pass || "", state.conn.nick, state.conn.user, state.conn.name)
{:noreply, %{state | backoff: backoff, connected_server: server, connected_port: port}}
end
# Logon successful
def handle_info(:logged_in, state) do
Logger.info("#{inspect(self())} Logged in")
{_, backoff} = :backoff.succeed(state.backoff)
Enum.map(state.conn.channels, &ExIRC.Client.join(state.client, &1))
{:noreply, %{state | backoff: backoff}}
end
# ISUP
def handle_info({:isup, network}, state) when is_binary(network) do
IRC.UserTrack.clear_network(state.network)
if network != state.network do
Logger.warn("Possibly misconfigured network: #{network} != #{state.network}")
end
{:noreply, state}
end
# Been kicked
def handle_info({:kicked, _sender, chan, _reason}, state) do
ExIRC.Client.join(state.client, chan)
{:noreply, state}
end
# Received something in a channel
def handle_info({:received, text, sender, chan}, state) do
user = if user = IRC.UserTrack.find_by_nick(state.network, sender.nick) do
user
else
Logger.error("Could not lookup user for message: #{inspect {state.network, chan, sender.nick}}")
user = IRC.UserTrack.joined(chan, sender, [])
ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user..
user
end
if !user do
ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user..
Logger.error("Could not lookup user nor create it for message: #{inspect {state.network, chan, sender.nick}}")
else
if !Map.get(user.options, :puppet) do
reply_fun = fn(text) -> irc_reply(state, {chan, sender}, text) end
account = IRC.Account.lookup(sender)
message = %IRC.Message{id: FlakeId.get(), transport: :irc, at: NaiveDateTime.utc_now(), text: text, network: state.network,
account: account, sender: sender, channel: chan, replyfun: reply_fun,
trigger: extract_trigger(text)}
message = case IRC.UserTrack.messaged(message) do
:ok -> message
{:ok, message} -> message
end
publish(message, ["#{message.network}/#{chan}:messages"])
end
end
{:noreply, state}
end
# Received a private message
def handle_info({:received, text, sender}, state) do
reply_fun = fn(text) -> irc_reply(state, {sender.nick, sender}, text) end
account = IRC.Account.lookup(sender)
message = %IRC.Message{id: FlakeId.get(), transport: :irc, text: text, network: state.network, at: NaiveDateTime.utc_now(),
account: account, sender: sender, replyfun: reply_fun, trigger: extract_trigger(text)}
message = case IRC.UserTrack.messaged(message) do
:ok -> message
{:ok, message} -> message
end
publish(message, ["messages:private", "#{message.network}/#{account.id}:messages"])
{:noreply, state}
end
## -- Broadcast
def handle_info({:broadcast, net, account = %IRC.Account{}, message}, state) do
if net == state.conn.network do
user = IRC.UserTrack.find_by_account(net, account)
if user do
irc_reply(state, {user.nick, nil}, message)
end
end
{:noreply, state}
end
def handle_info({:broadcast, net, chan, message}, state) do
if net == state.conn.network && Enum.member?(state.conn.channels, chan) do
irc_reply(state, {chan, nil}, message)
end
{:noreply, state}
end
## -- UserTrack
def handle_info({:joined, channel}, state) do
ExIRC.Client.who(state.client, channel)
{:noreply, state}
end
def handle_info({:who, channel, whos}, state) do
accounts = Enum.map(whos, fn(who = %ExIRC.Who{nick: nick, operator?: operator}) ->
priv = if operator, do: [:operator], else: []
# Don't touch -- on WHO the bot joined, not the users.
IRC.UserTrack.joined(channel, who, priv, false)
account = IRC.Account.lookup(who)
if account do
{:account, who.network, channel, who.nick, account.id}
end
end)
|> Enum.filter(fn(x) -> x end)
dispatch("account", {:accounts, accounts})
{:noreply, state}
end
def handle_info({:quit, reason, sender}, state) do
IRC.UserTrack.quitted(sender, reason)
{:noreply, state}
end
def handle_info({:joined, channel, sender}, state) do
IRC.UserTrack.joined(channel, sender, [])
account = IRC.Account.lookup(sender)
if account do
dispatch("account", {:account, sender.network, channel, sender.nick, account.id})
end
{:noreply, state}
end
def handle_info({:kicked, nick, _by, channel, _reason}, state) do
IRC.UserTrack.parted(state.network, channel, nick)
{:noreply, state}
end
def handle_info({:parted, channel, %ExIRC.SenderInfo{nick: nick}}, state) do
IRC.UserTrack.parted(state.network, channel, nick)
{:noreply, state}
end
def handle_info({:mode, [channel, mode, nick]}, state) do
track_mode(state.network, channel, nick, mode)
{:noreply, state}
end
def handle_info({:nick_changed, old_nick, new_nick}, state) do
IRC.UserTrack.renamed(state.network, old_nick, new_nick)
{:noreply, state}
end
def handle_info(unhandled, client) do
Logger.debug("unhandled: #{inspect unhandled}")
{:noreply, client}
end
def publish(pub), do: publish(pub, [])
def publish(m = %IRC.Message{trigger: nil}, keys) do
dispatch(["messages"] ++ keys, {:irc, :text, m})
end
def publish(m = %IRC.Message{trigger: t = %IRC.Trigger{trigger: trigger}}, keys) do
dispatch(["triggers", "#{m.network}/#{m.channel}:triggers", "trigger:"<>trigger], {:irc, :trigger, trigger, m})
end
def publish_event(net, event = %{type: _}) when is_binary(net) do
event = event
|> Map.put(:at, NaiveDateTime.utc_now())
|> Map.put(:network, net)
dispatch("#{net}:events", {:irc, :event, event})
end
def publish_event({net, chan}, event = %{type: type}) do
event = event
|> Map.put(:at, NaiveDateTime.utc_now())
|> Map.put(:network, net)
|> Map.put(:channel, chan)
dispatch("#{net}/#{chan}:events", {:irc, :event, event})
end
def dispatch(keys, content, sub \\ IRC.PubSub)
def dispatch(key, content, sub) when is_binary(key), do: dispatch([key], content, sub)
def dispatch(keys, content, sub) when is_list(keys) do
Logger.debug("dispatch #{inspect keys} = #{inspect content}")
for key <- keys do
spawn(fn() -> Registry.dispatch(sub, key, fn h ->
for {pid, _} <- h, do: send(pid, content)
end) end)
end
end
#
# Triggers
#
def triggers, do: @triggers
for {trigger, name} <- @triggers do
def extract_trigger(unquote(trigger)<>text) do
text = String.strip(text)
[trigger | args] = String.split(text, " ")
%IRC.Trigger{type: unquote(name), trigger: String.downcase(trigger), args: args}
end
end
def extract_trigger(_), do: nil
#
# IRC Replies
#
# irc_reply(ExIRC.Client pid, {channel or nick, ExIRC.Sender}, binary | replies
# replies :: {:kick, reason} | {:kick, nick, reason} | {:mode, mode, nick}
defp irc_reply(state = %{client: client, network: network}, {target, _}, text) when is_binary(text) or is_list(text) do
lines = IRC.splitlong(text)
|> Enum.map(fn(x) -> if(is_list(x), do: x, else: String.split(x, "\n")) end)
|> List.flatten()
outputs = for line <- lines do
ExIRC.Client.msg(client, :privmsg, target, line)
{:irc, :out, %IRC.Message{id: FlakeId.get(), transport: :irc, network: network,
channel: target, text: line, sender: %ExIRC.SenderInfo{nick: state.conn.nick}, at: NaiveDateTime.utc_now(), meta: %{self: true}}}
end
for f <- outputs, do: dispatch(["irc:outputs", "#{network}/#{target}:outputs"], f)
end
defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:kick, reason}) do
ExIRC.Client.kick(client, target, nick, reason)
end
defp irc_reply(%{client: client}, {target, _}, {:kick, nick, reason}) do
ExIRC.Client.kick(client, target, nick, reason)
end
defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:mode, mode}) do
ExIRC.Client.mode(%{client: client}, target, mode, nick)
end
defp irc_reply(%{client: client}, target, {:mode, mode, nick}) do
ExIRC.Client.mode(client, target, mode, nick)
end
defp irc_reply(%{client: client}, target, {:channel_mode, mode}) do
ExIRC.Client.mode(client, target, mode)
end
defp track_mode(network, channel, nick, "+o") do
IRC.UserTrack.change_privileges(network, channel, nick, {[:operator], []})
:ok
end
defp track_mode(network, channel, nick, "-o") do
IRC.UserTrack.change_privileges(network, channel, nick, {[], [:operator]})
:ok
end
defp track_mode(network, channel, nick, "+v") do
IRC.UserTrack.change_privileges(network, channel, nick, {[:voice], []})
:ok
end
defp track_mode(network, channel, nick, "-v") do
IRC.UserTrack.change_privileges(network, channel, nick, {[], [:voice]})
:ok
end
defp track_mode(network, channel, nick, mode) do
Logger.warn("Unhandled track_mode: #{inspect {nick, mode}}")
:ok
end
defp server(%{conn: %{host: host, port: port}}) do
host <> ":" <> to_string(port)
end
end
diff --git a/lib/irc/irc.ex b/lib/irc/irc.ex
index d9fdfb5..71d6d93 100644
--- a/lib/irc/irc.ex
+++ b/lib/irc/irc.ex
@@ -1,79 +1,79 @@
defmodule IRC do
defmodule Message do
@derive {Poison.Encoder, except: [:replyfun]}
defstruct [:id,
:text,
{:transport, :irc},
:network,
:account,
:sender,
:channel,
:trigger,
:replyfun,
:at,
{:meta, %{}}
]
end
defmodule Trigger do
@derive Poison.Encoder
defstruct [:type, :trigger, :args]
end
def send_message_as(account, network, channel, text, force_puppet \\ false) do
connection = IRC.Connection.get_network(network)
if connection && (force_puppet || IRC.PuppetConnection.whereis(account, connection)) do
IRC.PuppetConnection.start_and_send_message(account, connection, channel, text)
else
user = IRC.UserTrack.find_by_account(network, account)
nick = if(user, do: user.nick, else: account.name)
IRC.Connection.broadcast_message(network, channel, "<#{nick}> #{text}")
end
end
def register(key) do
case Registry.register(IRC.PubSub, key, []) do
{:ok, _} -> :ok
error -> error
end
end
def admin?(%Message{sender: sender}), do: admin?(sender)
def admin?(%{nick: nick, user: user, host: host}) do
- for {n, u, h} <- LSG.IRC.env(:admins, []) do
+ for {n, u, h} <- Nola.IRC.env(:admins, []) do
admin_part_match?(n, nick) && admin_part_match?(u, user) && admin_part_match?(h, host)
end
|> Enum.any?
end
defp admin_part_match?(:_, _), do: true
defp admin_part_match?(a, a), do: true
defp admin_part_match?(_, _), do: false
@max_chars 440
def splitlong(string, max_chars \\ 440)
def splitlong(string, max_chars) when is_list(string) do
Enum.map(string, fn(s) -> splitlong(s, max_chars) end)
|> List.flatten()
end
def splitlong(string, max_chars) do
string
|> String.codepoints
|> Enum.chunk_every(max_chars)
|> Enum.map(&Enum.join/1)
end
def splitlong_with_prefix(string, prefix, max_chars \\ 440) do
prefix = "#{prefix} "
max_chars = max_chars - (length(String.codepoints(prefix)))
string
|> String.codepoints
|> Enum.chunk_every(max_chars)
|> Enum.map(fn(line) -> prefix <> Enum.join(line) end)
end
end
diff --git a/lib/irc/membership.ex b/lib/irc/membership.ex
index 74ed1b4..b727dfd 100644
--- a/lib/irc/membership.ex
+++ b/lib/irc/membership.ex
@@ -1,129 +1,129 @@
defmodule IRC.Membership do
@moduledoc """
Memberships (users in channels)
"""
# Key: {account, net, channel}
# Format: {key, last_seen}
defp dets() do
- to_charlist(LSG.data_path <> "/memberships.dets")
+ 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(%IRC.Account{id: id}) do
spec = [{{{:"$1", :"$2", :"$3"}, :_}, [{:==, :"$1", {:const, id}}], [{{:"$2", :"$3"}}]}]
:dets.select(dets(), spec)
end
def merge_account(old_id, new_id) do
#iex(37)> :ets.fun2ms(fn({{old_id, _, _}, _}=obj) when old_id == "42" -> obj end)
spec = [{{{:"$1", :_, :_}, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, dets(), spec, fn(table, obj = {{_old, net, chan}, ts}) ->
:dets.delete_object(table, obj)
:dets.insert(table, {{new_id, net, chan}, ts})
end)
end
def touch(%IRC.Account{id: id}, network, channel) do
:dets.insert(dets(), {{id, network, channel}, NaiveDateTime.utc_now()})
end
def touch(account_id, network, channel) do
if account = IRC.Account.get(account_id) do
touch(account, network, channel)
end
end
def notify_channels(account, minutes \\ 30, last_active \\ true) do
not_before = NaiveDateTime.add(NaiveDateTime.utc_now(), (minutes*-60), :second)
spec = [{{{:"$1", :_, :_}, :_}, [{:==, :"$1", {:const, account.id}}], [:"$_"]}]
memberships = :dets.select(dets(), spec)
|> Enum.sort_by(fn({_, ts}) -> ts end, {:desc, NaiveDateTime})
active_memberships = Enum.filter(memberships, fn({_, ts}) -> NaiveDateTime.compare(ts, not_before) == :gt end)
cond do
active_memberships == [] && last_active ->
case memberships do
[{{_, net, chan}, _}|_] -> [{net, chan}]
_ -> []
end
active_memberships == [] ->
[]
true ->
Enum.map(active_memberships, fn({{_, net, chan}, _}) -> {net,chan} end)
end
end
def members_or_friends(account, _network, nil) do
friends(account)
end
def members_or_friends(_, network, channel) do
members(network, channel)
end
def expanded_members_or_friends(account, network, channel) do
expand(network, members_or_friends(account, network, channel))
end
def expanded_members(network, channel) do
expand(network, members(network, channel))
end
def members(network, channel) do
#iex(19)> :ets.fun2ms(fn({{id, net, chan}, ts}) when net == network and chan == channel and ts > min_seen -> id end)
limit = 0 # NaiveDateTime.add(NaiveDateTime.utc_now, 30*((24*-60)*60), :second)
spec = [
{{{:"$1", :"$2", :"$3"}, :"$4"},
[
{:andalso,
{:andalso, {:==, :"$2", {:const, network}}, {:==, :"$3", {:const, channel}}},
{:>, :"$4", {:const, limit}}}
], [:"$1"]}
]
:dets.select(dets(), spec)
end
def friends(account = %IRC.Account{id: id}) do
for({net, chan} <- of_account(account), do: members(net, chan))
|> List.flatten()
|> Enum.uniq()
end
def handle_info(_, dets) do
{:noreply, dets}
end
def handle_cast(_, dets) do
{:noreply, dets}
end
def handle_call(_, _, dets) do
{:noreply, dets}
end
def terminate(_, dets) do
:dets.sync(dets)
:dets.close(dets)
end
defp expand(network, list) do
for id <- list do
if account = IRC.Account.get(id) do
user = IRC.UserTrack.find_by_account(network, account)
nick = if(user, do: user.nick, else: account.name)
{account, user, nick}
end
end
|> Enum.filter(fn(x) -> x end)
end
end
diff --git a/lib/irc/plugin_supervisor.ex b/lib/irc/plugin_supervisor.ex
index 24ac683..a65ad09 100644
--- a/lib/irc/plugin_supervisor.ex
+++ b/lib/irc/plugin_supervisor.ex
@@ -1,99 +1,99 @@
defmodule IRC.Plugin do
require Logger
defmodule Supervisor do
use DynamicSupervisor
require Logger
def start_link() do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def start_child(module, opts \\ []) do
Logger.info("Starting #{module}")
spec = %{id: {IRC.Plugin,module}, start: {IRC.Plugin, :start_link, [module, opts]}, name: module, restart: :transient}
case DynamicSupervisor.start_child(__MODULE__, spec) do
{:ok, _} = res -> res
:ignore ->
Logger.warn("Ignored #{module}")
:ignore
{:error,_} = res ->
Logger.error("Could not start #{module}: #{inspect(res, pretty: true)}")
res
end
end
@impl true
def init(_init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
max_restarts: 10,
max_seconds: 1
)
end
end
- def dets(), do: to_charlist(LSG.data_path("/plugins.dets"))
+ def dets(), do: to_charlist(Nola.data_path("/plugins.dets"))
def setup() do
:dets.open_file(dets(), [])
end
def enabled() do
:dets.foldl(fn
{name, true, _}, acc -> [name | acc]
_, acc -> acc
end, [], dets())
end
def start_all() do
for mod <- enabled(), do: {mod, IRC.Plugin.Supervisor.start_child(mod)}
end
def declare(module) do
case get(module) do
:disabled -> :dets.insert(dets(), {module, true, nil})
_ -> nil
end
end
def start(module, opts \\ []) do
IRC.Plugin.Supervisor.start_child(module)
end
@doc "Enables a plugin"
def enable(name), do: switch(name, true)
@doc "Disables a plugin"
def disable(name), do: switch(name, false)
@doc "Enables or disables a plugin"
def switch(name, value) when is_boolean(value) do
last = case get(name) do
{:ok, last} -> last
_ -> nil
end
:dets.insert(dets(), {name, value, last})
end
@spec get(module()) :: {:ok, last_start :: nil | non_neg_integer()} | :disabled
def get(name) do
case :dets.lookup(dets(), name) do
[{name, enabled, last_start}] -> {:ok, enabled, last_start}
_ -> :disabled
end
end
def start_link(module, options \\ []) do
with {:disabled, {_, true, last}} <- {:disabled, get(module)},
{:throttled, false} <- {:throttled, false}
do
module.start_link()
else
{error, _} ->
Logger.info("Plugin: #{to_string(module)} ignored start: #{to_string(error)}")
:ignore
end
end
end
diff --git a/lib/irc/puppet_connection.ex b/lib/irc/puppet_connection.ex
index f12cbf7..91a26b3 100644
--- a/lib/irc/puppet_connection.ex
+++ b/lib/irc/puppet_connection.ex
@@ -1,238 +1,238 @@
defmodule IRC.PuppetConnection do
require Logger
@min_backoff :timer.seconds(5)
@max_backoff :timer.seconds(2*60)
@max_idle :timer.hours(12)
@env Mix.env
defmodule Supervisor do
use DynamicSupervisor
def start_link() do
DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def start_child(%IRC.Account{id: account_id}, %IRC.Connection{id: connection_id}) do
spec = %{id: {account_id, connection_id}, start: {IRC.PuppetConnection, :start_link, [account_id, connection_id]}, restart: :transient}
DynamicSupervisor.start_child(__MODULE__, spec)
end
@impl true
def init(_init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
max_restarts: 10,
max_seconds: 1
)
end
end
def whereis(account = %IRC.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}) do
{:global, name} = name(account_id, connection_id)
case :global.whereis_name(name) do
:undefined -> nil
pid -> pid
end
end
def send_message(account = %IRC.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}, channel, text) do
GenServer.cast(name(account_id, connection_id), {:send_message, self(), channel, text})
end
def start_and_send_message(account = %IRC.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}, channel, text) do
{:global, name} = name(account_id, connection_id)
pid = whereis(account, connection)
pid = if !pid do
case IRC.PuppetConnection.Supervisor.start_child(account, connection) do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
end
else
pid
end
GenServer.cast(pid, {:send_message, self(), channel, text})
end
def start(account = %IRC.Account{}, connection = %IRC.Connection{}) do
IRC.PuppetConnection.Supervisor.start_child(account, connection)
end
def start_link(account_id, connection_id) do
GenServer.start_link(__MODULE__, [account_id, connection_id], name: name(account_id, connection_id))
end
def name(account_id, connection_id) do
{:global, {PuppetConnection, account_id, connection_id}}
end
def init([account_id, connection_id]) do
account = %IRC.Account{} = IRC.Account.get(account_id)
connection = %IRC.Connection{} = IRC.Connection.lookup(connection_id)
Logger.metadata(puppet_conn: account.id <> "@" <> connection.id)
backoff = :backoff.init(@min_backoff, @max_backoff)
|> :backoff.type(:jitter)
idle = :erlang.send_after(@max_idle, self, :idle)
{:ok, %{client: nil, backoff: backoff, idle: idle, connected: false, buffer: [], channels: [], connection_id: connection_id, account_id: account_id, connected_server: nil, connected_port: nil, network: connection.network}, {:continue, :connect}}
end
def handle_continue(:connect, state) do
#ipv6 = if @env == :prod do
- # subnet = LSG.Subnet.assign(state.account_id)
+ # subnet = Nola.Subnet.assign(state.account_id)
# IRC.Account.put_meta(IRC.Account.get(state.account_id), "subnet", subnet)
# ip = Pfx.host(subnet, 1)
# {:ok, ipv6} = :inet_parse.ipv6_address(to_charlist(ip))
# System.cmd("add-ip6", [ip])
# ipv6
#end
conn = IRC.Connection.lookup(state.connection_id)
client_opts = []
|> Keyword.put(:network, conn.network)
client = if state.client && Process.alive?(state.client) do
Logger.info("Reconnecting client")
state.client
else
Logger.info("Connecting")
{:ok, client} = ExIRC.Client.start_link(debug: false)
ExIRC.Client.add_handler(client, self())
client
end
base_opts = [
{:nodelay, true}
]
#{ip, opts} = case {ipv6, :inet_res.resolve(to_charlist(conn.host), :in, :aaaa)} do
# {ipv6, {:ok, {:dns_rec, _dns_header, _query, rrs = [{:dns_rr, _, _, _, _, _, _, _, _, _} | _], _, _}}} ->
# ip = rrs
# |> Enum.map(fn({:dns_rr, _, :aaaa, :in, _, _, ipv6, _, _, _}) -> ipv6 end)
# |> Enum.shuffle()
# |> List.first()
# opts = [
# :inet6,
# {:ifaddr, ipv6}
# ]
# {ip, opts}
# _ ->
{ip, opts} = {to_charlist(conn.host), []}
#end
conn_fun = if conn.tls, do: :connect_ssl!, else: :connect!
apply(ExIRC.Client, conn_fun, [client, ip, conn.port, base_opts ++ opts])
{:noreply, %{state | client: client}}
end
def handle_continue(:connected, state) do
state = Enum.reduce(Enum.reverse(state.buffer), state, fn(b, state) ->
{:noreply, state} = handle_cast(b, state)
state
end)
{:noreply, %{state | buffer: []}}
end
def handle_cast(cast = {:send_message, _pid, _channel, _text}, state = %{connected: false, buffer: buffer}) do
{:noreply, %{state | buffer: [cast | buffer]}}
end
def handle_cast({:send_message, pid, channel, text}, state = %{connected: true}) do
channels = if !Enum.member?(state.channels, channel) do
ExIRC.Client.join(state.client, channel)
[channel | state.channels]
else
state.channels
end
ExIRC.Client.msg(state.client, :privmsg, channel, text)
meta = %{puppet: true, from: pid}
account = IRC.Account.get(state.account_id)
nick = make_nick(state)
sender = %ExIRC.SenderInfo{network: state.network, nick: suffix_nick(nick), user: nick, host: "puppet."}
reply_fun = fn(text) ->
IRC.Connection.broadcast_message(state.network, channel, text)
end
message = %IRC.Message{id: FlakeId.get(), at: NaiveDateTime.utc_now(), text: text, network: state.network, account: account, sender: sender, channel: channel, replyfun: reply_fun, trigger: IRC.Connection.extract_trigger(text), meta: meta}
message = case IRC.UserTrack.messaged(message) do
:ok -> message
{:ok, message} -> message
end
IRC.Connection.publish(message, ["#{message.network}/#{channel}:messages"])
idle = if length(state.buffer) == 0 do
:erlang.cancel_timer(state.idle)
:erlang.send_after(@max_idle, self(), :idle)
else
state.idle
end
{:noreply, %{state | idle: idle, channels: channels}}
end
def handle_info(:idle, state) do
ExIRC.Client.quit(state.client, "Puppet was idle for too long")
ExIRC.Client.stop!(state.client)
{:stop, :normal, state}
end
def handle_info(:disconnected, state) do
{delay, backoff} = :backoff.fail(state.backoff)
Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect delay}ms")
Process.send_after(self(), :connect, delay)
{:noreply, %{state | connected: false, backoff: backoff}}
end
def handle_info(:connect, state) do
{:noreply, state, {:continue, :connect}}
end
# Connection successful
def handle_info({:connected, server, port}, state) do
Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect state}")
{_, backoff} = :backoff.succeed(state.backoff)
base_nick = make_nick(state)
ExIRC.Client.logon(state.client, "", suffix_nick(base_nick), base_nick, "#{base_nick}'s puppet")
{:noreply, %{state | backoff: backoff, connected_server: server, connected_port: port}}
end
# Logon successful
def handle_info(:logged_in, state) do
Logger.info("#{inspect(self())} Logged in")
{_, backoff} = :backoff.succeed(state.backoff)
# Create an UserTrack entry for the client so it's authenticated to the right account_id already.
IRC.UserTrack.connected(state.network, suffix_nick(make_nick(state)), make_nick(state), "puppet.", state.account_id, %{puppet: true})
{:noreply, %{state | backoff: backoff}}
end
# ISUP
def handle_info({:isup, network}, state) do
{:noreply, %{state | network: network, connected: true}, {:continue, :connected}}
end
# Been kicked
def handle_info({:kicked, _sender, chan, _reason}, state) do
{:noreply, %{state | channels: state.channels -- [chan]}}
end
def handle_info(_info, state) do
{:noreply, state}
end
def make_nick(state) do
account = IRC.Account.get(state.account_id)
user = IRC.UserTrack.find_by_account(state.network, account)
base_nick = if(user, do: user.nick, else: account.name)
clean_nick = case String.split(base_nick, ":", parts: 2) do
["@"<>nick, _] -> nick
[nick] -> nick
end
clean_nick
end
if Mix.env == :dev do
def suffix_nick(nick), do: "#{nick}[d]"
else
def suffix_nick(nick), do: "#{nick}[p]"
end
end
diff --git a/lib/lsg/application.ex b/lib/lsg/application.ex
index 0d29668..1782053 100644
--- a/lib/lsg/application.ex
+++ b/lib/lsg/application.ex
@@ -1,56 +1,56 @@
-defmodule LSG.Application do
+defmodule Nola.Application do
use Application
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec
Logger.add_backend(Sentry.LoggerBackend)
- :ok = LSG.Matrix.setup()
- :ok = LSG.TelegramRoom.setup()
+ :ok = Nola.Matrix.setup()
+ :ok = Nola.TelegramRoom.setup()
# Define workers and child supervisors to be supervised
children = [
# Start the endpoint when the application starts
- supervisor(LSGWeb.Endpoint, []),
- # Start your own worker by calling: LSG.Worker.start_link(arg1, arg2, arg3)
- # worker(LSG.Worker, [arg1, arg2, arg3]),
- worker(Registry, [[keys: :duplicate, name: LSG.BroadcastRegistry]], id: :registry_broadcast),
- worker(LSG.IcecastAgent, []),
- worker(LSG.Token, []),
- worker(LSG.AuthToken, []),
- LSG.Subnet,
- {GenMagic.Pool, [name: LSG.GenMagic, pool_size: 2]},
- #worker(LSG.Icecast, []),
- ] ++ LSG.IRC.application_childs
- ++ LSG.Matrix.application_childs
+ supervisor(NolaWeb.Endpoint, []),
+ # Start your own worker by calling: Nola.Worker.start_link(arg1, arg2, arg3)
+ # worker(Nola.Worker, [arg1, arg2, arg3]),
+ worker(Registry, [[keys: :duplicate, name: Nola.BroadcastRegistry]], id: :registry_broadcast),
+ worker(Nola.IcecastAgent, []),
+ worker(Nola.Token, []),
+ worker(Nola.AuthToken, []),
+ Nola.Subnet,
+ {GenMagic.Pool, [name: Nola.GenMagic, pool_size: 2]},
+ #worker(Nola.Icecast, []),
+ ] ++ Nola.IRC.application_childs
+ ++ Nola.Matrix.application_childs
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
- opts = [strategy: :one_for_one, name: LSG.Supervisor]
+ opts = [strategy: :one_for_one, name: Nola.Supervisor]
sup = Supervisor.start_link(children, opts)
start_telegram()
- spawn_link(fn() -> LSG.IRC.after_start() end)
- spawn_link(fn() -> LSG.Matrix.after_start() end)
- spawn_link(fn() -> LSG.TelegramRoom.after_start() end)
+ spawn_link(fn() -> Nola.IRC.after_start() end)
+ spawn_link(fn() -> Nola.Matrix.after_start() end)
+ spawn_link(fn() -> Nola.TelegramRoom.after_start() end)
sup
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
- LSGWeb.Endpoint.config_change(changed, removed)
+ NolaWeb.Endpoint.config_change(changed, removed)
:ok
end
defp start_telegram() do
token = Keyword.get(Application.get_env(:lsg, :telegram, []), :key)
options = [
username: Keyword.get(Application.get_env(:lsg, :telegram, []), :nick, "beauttebot"),
purge: false
]
- telegram = Telegram.Bot.ChatBot.Supervisor.start_link({LSG.Telegram, token, options})
+ telegram = Telegram.Bot.ChatBot.Supervisor.start_link({Nola.Telegram, token, options})
end
end
diff --git a/lib/lsg/auth_token.ex b/lib/lsg/auth_token.ex
index 0c5ba58..d125ea4 100644
--- a/lib/lsg/auth_token.ex
+++ b/lib/lsg/auth_token.ex
@@ -1,59 +1,59 @@
-defmodule LSG.AuthToken do
+defmodule Nola.AuthToken do
use GenServer
def start_link() do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def lookup(id) do
GenServer.call(__MODULE__, {:lookup, id})
end
def new_path(account, perks \\ nil) do
case new(account, perks) do
{:ok, id} ->
- LSGWeb.Router.Helpers.login_path(LSGWeb.Endpoint, :token, id)
+ NolaWeb.Router.Helpers.login_path(NolaWeb.Endpoint, :token, id)
error ->
error
end
end
def new_url(account, perks \\ nil) do
case new(account, perks) do
{:ok, id} ->
- LSGWeb.Router.Helpers.login_url(LSGWeb.Endpoint, :token, id)
+ NolaWeb.Router.Helpers.login_url(NolaWeb.Endpoint, :token, id)
error ->
error
end
end
def new(account, perks \\ nil) do
GenServer.call(__MODULE__, {:new, account, perks})
end
def init(_) do
{:ok, Map.new}
end
def handle_call({:lookup, id}, _, state) do
IO.inspect(state)
with \
{account, date, perks} <- Map.get(state, id),
d when d > 0 <- DateTime.diff(date, DateTime.utc_now())
do
{:reply, {:ok, account, perks}, Map.delete(state, id)}
else
x ->
IO.inspect(x)
{:reply, {:error, :invalid_token}, state}
end
end
def handle_call({:new, account, perks}, _, state) do
id = IRC.UserTrack.Id.token()
expire = DateTime.utc_now()
|> DateTime.add(15*60, :second)
{:reply, {:ok, id}, Map.put(state, id, {account, expire, perks})}
end
end
diff --git a/lib/lsg/icecast.ex b/lib/lsg/icecast.ex
index 07dd4fc..60fb45a 100644
--- a/lib/lsg/icecast.ex
+++ b/lib/lsg/icecast.ex
@@ -1,117 +1,117 @@
-defmodule LSG.Icecast do
+defmodule Nola.Icecast do
use GenServer
require Logger
@hackney_pool :default
@httpoison_opts [hackney: [pool: @hackney_pool]]
@fuse __MODULE__
def start_link, do: GenServer.start_link(__MODULE__, [], [])
def init(_) do
GenServer.cast(self(), :poll)
{:ok, nil}
end
def handle_cast(:poll, state) do
state = poll(state)
{:noreply, state}
end
def handle_info(:poll, state) do
state = poll(state)
{:noreply, state}
end
defp poll(state) do
state = case request(base_url(), :get) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
#update_json_stats(Jason.decode(body))
stats = update_stats(body)
if state != stats do
Logger.info "Icecast Update: " <> inspect(stats)
- LSG.IcecastAgent.update(stats)
- Registry.dispatch(LSG.BroadcastRegistry, "icecast", fn ws ->
+ Nola.IcecastAgent.update(stats)
+ Registry.dispatch(Nola.BroadcastRegistry, "icecast", fn ws ->
for {pid, _} <- ws, do: send(pid, {:icecast, stats})
end)
stats
else
state
end
error ->
Logger.error "Icecast HTTP Error: #{inspect error}"
state
end
interval = Application.get_env(:lsg, :icecast_poll_interval, 60_000)
:timer.send_after(interval, :poll)
state
end
defp update_stats(html) do
raw = Floki.find(html, "div.roundbox")
|> Enum.map(fn(html) ->
html = Floki.raw_html(html)
[{"h3", _, ["Mount Point /"<>mount]}] = Floki.find(html, "h3.mount")
stats = Floki.find(html, "tr")
|> Enum.map(fn({"tr", _, tds}) ->
[{"td", _, keys}, {"td", _, values}] = tds
key = List.first(keys)
value = List.first(values)
{key, value}
end)
|> Enum.into(Map.new)
{mount, stats}
end)
|> Enum.into(Map.new)
live? = if Map.get(raw["live"], "Content Type:", false), do: true, else: false
np = if live? do
raw["live"]["Currently playing:"]
else
raw["autodj"]["Currently playing:"]
end
genre = raw["live"]["Genre:"] || nil
%{np: np || "", live: live? || false, genre: genre}
end
defp update_json_stats({:ok, body}) do
Logger.debug "JSON STATS: #{inspect body}"
end
defp update_json_stats(error) do
Logger.error "Failed to decode JSON Stats: #{inspect error}"
end
defp request(uri, method, body \\ [], headers \\ []) do
- headers = [{"user-agent", "LSG-API[115ans.net, sys.115ans.net] href@random.sh"}] ++ headers
+ headers = [{"user-agent", "Nola-API[115ans.net, sys.115ans.net] href@random.sh"}] ++ headers
options = @httpoison_opts
case :ok do #:fuse.ask(@fuse, :sync) do
:ok -> run_request(method, uri, body, headers, options)
:blown -> :blown
end
end
# This is to work around hackney's behaviour of returning `{:error, :closed}` when a pool connection has been closed
# (keep-alive expired). We just retry the request immediatly up to five times.
defp run_request(method, uri, body, headers, options), do: run_request(method, uri, body, headers, options, 0)
defp run_request(method, uri, body, headers, options, retries) when retries < 4 do
case HTTPoison.request(method, uri, body, headers, options) do
{:error, :closed} -> run_request(method, uri, body, headers, options, retries + 1)
other -> other
end
end
defp run_request(method, uri, body, headers, options, _exceeded_retries), do: {:error, :unavailable}
#
# -- URIs
#
defp stats_json_url do
base_url() <> "/status-json.xsl"
end
defp base_url do
"http://91.121.59.45:8089"
end
end
diff --git a/lib/lsg/icecast_agent.ex b/lib/lsg/icecast_agent.ex
index 8f8a86a..8a3a72b 100644
--- a/lib/lsg/icecast_agent.ex
+++ b/lib/lsg/icecast_agent.ex
@@ -1,17 +1,17 @@
-defmodule LSG.IcecastAgent do
+defmodule Nola.IcecastAgent do
use Agent
def start_link() do
Agent.start_link(fn -> nil end, name: __MODULE__)
end
def update(stats) do
Agent.update(__MODULE__, fn(_old) -> stats end)
end
def get do
Agent.get(__MODULE__, fn(stats) -> stats end)
end
end
diff --git a/lib/lsg/lsg.ex b/lib/lsg/lsg.ex
index b5da5e0..11d0e24 100644
--- a/lib/lsg/lsg.ex
+++ b/lib/lsg/lsg.ex
@@ -1,30 +1,30 @@
-defmodule LSG do
+defmodule Nola do
@default_brand [
name: "Nola,
source_url: "https://phab.random.sh/source/Bot/",
owner: "Ashamed owner",
owner_email: "contact@my.nola.bot"
]
def env(), do: Application.get_env(:lsg)
def env(key, default \\ nil), do: Application.get_env(:lsg, key, default)
def brand(), do: env(:brand, @default_brand)
def brand(key), do: Keyword.get(brand(), key)
def name(), do: brand(:name)
def source_url(), do: brand(:source_url)
def data_path(suffix) do
Path.join(data_path(), suffix)
end
def data_path do
Application.get_env(:lsg, :data_path)
end
def version do
Application.spec(:lsg)[:vsn]
end
end
diff --git a/lib/lsg/subnet.ex b/lib/lsg/subnet.ex
index 81bd862..ac9d8e6 100644
--- a/lib/lsg/subnet.ex
+++ b/lib/lsg/subnet.ex
@@ -1,84 +1,84 @@
-defmodule LSG.Subnet do
+defmodule Nola.Subnet do
use Agent
def start_link(_) do
Agent.start_link(&setup/0, name: __MODULE__)
end
def assignations() do
:dets.select(dets(), [{{:"$1", :"$2"}, [is_binary: :"$2"], [{{:"$1", :"$2"}}]}])
end
def find_subnet_for(binary) when is_binary(binary) do
case :dets.select(dets(), [{{:"$1", :"$2"}, [{:==, :"$2", binary}], [{{:"$1", :"$2"}}]}]) do
[{subnet, _}] -> subnet
_ -> nil
end
end
def assign(binary) when is_binary(binary) do
result = if subnet = find_subnet_for(binary) do
{:ok, subnet}
else
Agent.get_and_update(__MODULE__, fn(dets) ->
{subnet, _} = available_select(dets)
:dets.insert(dets, {subnet, binary})
:dets.sync(dets)
{{:new, subnet}, dets}
end)
end
case result do
{:new, subnet} ->
ip = Pfx.host(subnet, 1)
set_reverse(binary, ip)
subnet
{:ok, subnet} ->
subnet
end
end
def set_reverse(name, ip, value \\ nil)
def set_reverse(name, ip, nil) do
set_reverse(name, ip, "#{name}.users.goulag.org")
end
def set_reverse(_, ip, value) do
ptr_zone = "3.0.0.2.d.f.0.a.2.ip6.arpa"
ip_fqdn = Pfx.dns_ptr(ip)
ip_local = String.replace(ip_fqdn, ".#{ptr_zone}", "")
rev? = String.ends_with?(value, ".users.goulag.org")
if rev? do
{:ok, rev_zone} = PowerDNSex.show_zone("users.goulag.org")
rev_update? = Enum.any?(rev_zone.rrsets, fn(rr) -> rr.name == "#{ip_fqdn}." end)
record = %{name: "#{value}.", type: "AAAA", ttl: 8600, records: [%{content: ip, disabled: false}]}
if(rev_update?, do: PowerDNSex.update_record(rev_zone, record), else: PowerDNSex.create_record(rev_zone, record))
end
{:ok, zone} = PowerDNSex.show_zone(ptr_zone)
update? = Enum.any?(zone.rrsets, fn(rr) -> rr.name == "#{ip_fqdn}." end)
record = %{name: "#{ip_fqdn}.", type: "PTR", ttl: 3600, records: [%{content: "#{value}.", disabled: false}]}
pdns = if(update?, do: PowerDNSex.update_record(zone, record), else: PowerDNSex.create_record(zone, record))
:ok
end
@doc false
def dets() do
- (LSG.data_path() <> "/subnets.dets") |> String.to_charlist()
+ (Nola.data_path() <> "/subnets.dets") |> String.to_charlist()
end
@doc false
def setup() do
{:ok, dets} = :dets.open_file(dets(), [])
dets
end
defp available_select(dets) do
spec = [{{:"$1", :"$2"}, [is_integer: :"$2"], [{{:"$1", :"$2"}}]}]
{subnets, _} = :dets.select(dets, spec, 20)
subnet = subnets
|> Enum.sort_by(fn({_, last}) -> last end)
|> List.first()
end
end
diff --git a/lib/lsg/token.ex b/lib/lsg/token.ex
index 33946d4..563ac72 100644
--- a/lib/lsg/token.ex
+++ b/lib/lsg/token.ex
@@ -1,38 +1,38 @@
-defmodule LSG.Token do
+defmodule Nola.Token do
use GenServer
def start_link() do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def lookup(id) do
with \
[{_, cred, date}] <- :ets.lookup(__MODULE__.ETS, id),
IO.inspect("cred: #{inspect cred} valid for #{inspect date} now #{inspect DateTime.utc_now()}"),
d when d > 0 <- DateTime.diff(date, DateTime.utc_now())
do
{:ok, cred}
else
err -> {:error, err}
end
end
def new(cred) do
GenServer.call(__MODULE__, {:new, cred})
end
def init(_) do
ets = :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
{:ok, ets}
end
def handle_call({:new, cred}, _, ets) do
id = IRC.UserTrack.Id.large_id()
expire = DateTime.utc_now()
|> DateTime.add(15*60, :second)
obj = {id, cred, expire}
:ets.insert(ets, obj)
{:reply, {:ok, id}, ets}
end
end
diff --git a/lib/lsg_irc/admin_handler.ex b/lib/lsg_irc/admin_handler.ex
index fab4dbc..9a5d557 100644
--- a/lib/lsg_irc/admin_handler.ex
+++ b/lib/lsg_irc/admin_handler.ex
@@ -1,41 +1,41 @@
-defmodule LSG.IRC.AdminHandler do
+defmodule Nola.IRC.AdminHandler do
@moduledoc """
# admin
!op
op; requiert admin
"""
def irc_doc, do: nil
def start_link(client) do
GenServer.start_link(__MODULE__, [client])
end
def init([client]) do
ExIRC.Client.add_handler client, self
:ok = IRC.register("op")
{:ok, client}
end
def handle_info({:irc, :trigger, "op", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}, sender: sender}}, client) do
if IRC.admin?(sender) do
m.replyfun.({:mode, "+o"})
else
m.replyfun.({:kick, "non"})
end
{:noreply, client}
end
def handle_info({:joined, chan, sender}, client) do
if IRC.admin?(sender) do
ExIRC.Client.mode(client, chan, "+o", sender.nick)
end
{:noreply, client}
end
def handle_info(msg, client) do
{:noreply, client}
end
end
diff --git a/lib/lsg_irc/alcolog_plugin.ex b/lib/lsg_irc/alcolog_plugin.ex
index f61b237..145e4fc 100644
--- a/lib/lsg_irc/alcolog_plugin.ex
+++ b/lib/lsg_irc/alcolog_plugin.ex
@@ -1,1229 +1,1229 @@
-defmodule LSG.IRC.AlcoologPlugin do
+defmodule Nola.IRC.AlcoologPlugin 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 = (LSG.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
- dets_meta_filename = (LSG.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
+ dets_filename = (Nola.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
+ dets_meta_filename = (Nola.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
%{dets: dets_filename, meta: dets_meta_filename, ets: __MODULE__.ETS}
end
def init(_) do
triggers = for(t <- @pubsub_triggers, do: "trigger:"<>t)
for sub <- @pubsub ++ triggers do
{:ok, _} = Registry.register(IRC.PubSub, sub, plugin: __MODULE__)
end
- dets_filename = (LSG.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
+ 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 = (LSG.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
+ 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
- LSG.IRC.TxtPlugin.reply_random(m, "alcoolog.santo")
+ Nola.IRC.TxtPlugin.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} = LSG.Token.new({:alcoolog, :index, m.sender.network, m.channel})
- url = LSGWeb.Router.Helpers.alcoolog_url(LSGWeb.Endpoint, :index, m.network, LSGWeb.format_chan(m.channel), token)
+ {: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 = LSGWeb.Router.Helpers.alcoolog_url(LSGWeb.Endpoint, :index, m.network, LSGWeb.format_chan(m.channel))
+ 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 -> LSG.IRC.TxtPlugin.reply_random(m, "alcoolog.drink_toohuge")
- cl == 0 || deg == 0 -> LSG.IRC.TxtPlugin.reply_random(m, "alcoolog.drink_zero")
- cl < 0 || deg < 0 -> LSG.IRC.TxtPlugin.reply_random(m, "alcoolog.drink_negative")
+ cl >= 500 || deg >= 100 -> Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.drink_toohuge")
+ cl == 0 || deg == 0 -> Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.drink_zero")
+ cl < 0 || deg < 0 -> Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.drink_negative")
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 = LSG.IRC.TxtPlugin.random("alcoolog.santai")
+ sante = Nola.IRC.TxtPlugin.random("alcoolog.santai")
k = if user_meta.sex, do: 0.7, else: 0.6
weight = user_meta.weight
peak = Float.round((10*points||0.0)/(k*weight), 4)
stats = get_full_statistics(state, m.account.id)
sober_add = if old_stats && Map.get(old_stats || %{}, :sober_in) do
mins = round(stats.sober_in - old_stats.sober_in)
" [+#{mins}m]"
else
""
end
nonow = DateTime.utc_now()
sober = nonow |> DateTime.add(round(stats.sober_in*60), :second)
|> Timex.Timezone.convert("Europe/Paris")
at = if nonow.day == sober.day do
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
detail
else
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
detail
end
up = if stats.active_drinks > 1 do
" " <> Enum.join(for(_ <- 1..stats.active_drinks, do: "▲")) <> ""
else
""
end
since_str = if stats.since && stats.since_min > 180 do
"(depuis: #{stats.since_s}) "
else
""
end
msg = fn(nick, extra) ->
"#{sante} #{nick} #{extra}#{up} #{format_points(points)} @#{stats.active}g/l [+#{peak} g/l]"
<> " (15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) #{since_str}(sobriété #{at} (dans #{stats.sober_in_s})#{sober_add}) !"
<> " (aujourd'hui #{stats.daily_volumes} points - #{stats.daily_gl} g/l)"
end
meta = if beer_id do
Map.put(meta, "untappd:beer_id", beer_id)
else
meta
end
if beer_id do
spawn(fn() ->
case Untappd.maybe_checkin(m.account, beer_id) do
{:ok, body} ->
badges = get_in(body, ["badges", "items"])
if badges != [] do
badges_s = Enum.map(badges, fn(badge) -> Map.get(badge, "badge_name") end)
|> Enum.filter(fn(b) -> b end)
|> Enum.intersperse(", ")
|> Enum.join("")
badge = if(length(badges) > 1, do: "badges", else: "badge")
m.replyfun.("\\O/ Unlocked untappd #{badge}: #{badges_s}")
end
:ok
{:error, {:http_error, error}} when is_integer(error) -> m.replyfun.("Checkin to Untappd failed: #{to_string(error)}")
{:error, {:http_error, error}} -> m.replyfun.("Checkin to Untappd failed: #{inspect error}")
_ -> :error
end
end)
end
local_extra = if auto_set do
if comment do
" #{comment} (#{cl}cl @ #{deg}°)"
else
"#{cl}cl @ #{deg}°"
end
else
""
end
m.replyfun.(msg.(m.sender.nick, local_extra))
notify = IRC.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
for {net, chan} <- notify do
user = IRC.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
extra = " " <> present_type(name, comment) <> ""
IRC.Connection.broadcast_message(net, chan, msg.(nick, extra))
end
miss = cond do
points <= 0.6 -> :small
stats.active30m >= 2.9 && stats.active30m < 3 -> :miss3
stats.active30m >= 1.9 && stats.active30m < 2 -> :miss2
stats.active30m >= 0.9 && stats.active30m < 1 -> :miss1
stats.active30m >= 0.45 && stats.active30m < 0.5 -> :miss05
stats.active30m >= 0.20 && stats.active30m < 0.20 -> :miss025
stats.active30m >= 3 && stats.active1h < 3.15 -> :small3
stats.active30m >= 2 && stats.active1h < 2.15 -> :small2
stats.active30m >= 1.5 && stats.active1h < 1.5 -> :small15
stats.active30m >= 1 && stats.active1h < 1.15 -> :small1
stats.active30m >= 0.5 && stats.active1h <= 0.51 -> :small05
stats.active30m >= 0.25 && stats.active30m <= 0.255 -> :small025
true -> nil
end
if miss do
- miss = LSG.IRC.TxtPlugin.random("alcoolog.#{to_string(miss)}")
+ miss = Nola.IRC.TxtPlugin.random("alcoolog.#{to_string(miss)}")
if miss do
for {net, chan} <- IRC.Membership.notify_channels(m.account) do
user = IRC.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
IRC.Connection.broadcast_message(net, chan, "#{nick}: #{miss}")
end
end
end
end
end
def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do
m.replyfun.("!santai <cl> <degrés> [commentaire]")
{:noreply, state}
end
def get_all_stats() do
IRC.Account.all_accounts()
|> Enum.map(fn(account) -> {account.id, get_full_statistics(account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
|> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
end
def get_channel_statistics(account, network, nil) do
IRC.Membership.expanded_members_or_friends(account, network, nil)
|> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
|> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
end
def get_channel_statistics(_, network, channel), do: get_channel_statistics(network, channel)
def get_channel_statistics(network, channel) do
IRC.Membership.expanded_members(network, channel)
|> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
|> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
end
@spec since() :: %{IRC.Account.id() => DateTime.t()}
@doc "Returns the last time the user was at 0 g/l"
def since() do
:ets.foldr(fn({{acct, timestamp_or_date}, _vol, current, _cl, _deg, _name, _comment, _m}, acc) ->
if !Map.get(acc, acct) && current == 0 do
date = Util.to_date_time(timestamp_or_date)
Map.put(acc, acct, date)
else
acc
end
end, %{}, __MODULE__.ETS)
end
def get_full_statistics(nick) do
get_full_statistics(data_state(), nick)
end
defp get_full_statistics(state, nick) do
case get_statistics_for_nick(state, nick) do
{count, {_, last_at, last_points, last_active, last_cl, last_deg, last_type, last_descr, _meta}} ->
{active, active_drinks} = current_alcohol_level(state, nick)
{_, m30} = alcohol_level_rising(state, nick)
{rising, m15} = alcohol_level_rising(state, nick, 15)
{_, m5} = alcohol_level_rising(state, nick, 5)
{_, h1} = alcohol_level_rising(state, nick, 60)
trend = if rising do
"▲"
else
"▼"
end
user_state = cond do
active <= 0.0 -> :sober
active <= 0.25 -> :low
active <= 0.50 -> :legal
active <= 1.0 -> :legalhigh
active <= 2.5 -> :high
active < 3 -> :toohigh
true -> :sick
end
rising_file_key = if rising, do: "_rising", else: ""
txt_file = "alcoolog." <> "user_" <> to_string(user_state) <> rising_file_key
- user_status = LSG.IRC.TxtPlugin.random(txt_file)
+ user_status = Nola.IRC.TxtPlugin.random(txt_file)
meta = get_user_meta(state, nick)
minutes_til_sober = h1/((meta.loss_factor/100)/60)
minutes_til_sober = cond do
active < 0 -> 0
m15 < 0 -> 15
m30 < 0 -> 30
h1 < 0 -> 60
minutes_til_sober > 0 ->
Float.round(minutes_til_sober+60)
true -> 0
end
duration = Timex.Duration.from_minutes(minutes_til_sober)
sober_in_s = if minutes_til_sober > 0 do
Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
else
nil
end
since = if active > 0 do
since()
|> Map.get(nick)
end
since_diff = if since, do: Timex.diff(DateTime.utc_now(), since, :minutes)
since_duration = if since, do: Timex.Duration.from_minutes(since_diff)
since_s = if since, do: Timex.Format.Duration.Formatter.lformat(since_duration, "fr", :humanized)
{total_volumes, total_gl} = user_stats(state, nick)
%{active: active, last_at: last_at, last_cl: last_cl, last_deg: last_deg, last_points: last_points, last_type: last_type, last_descr: last_descr,
trend_symbol: trend,
active5m: m5, active15m: m15, active30m: m30, active1h: h1,
rising: rising,
active_drinks: active_drinks,
user_status: user_status,
daily_gl: total_gl, daily_volumes: total_volumes,
sober_in: minutes_til_sober, sober_in_s: sober_in_s,
since: since, since_min: since_diff, since_s: since_s,
}
_ ->
nil
end
end
def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :dot}}}, state) do
nicks = IRC.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
|> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(state, account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && status.sober_in && status.sober_in > 0 end)
|> Enum.sort_by(fn({_, status}) -> status.sober_in end, &</2)
|> Enum.map(fn({nick, stats}) ->
now = DateTime.utc_now()
sober = now |> DateTime.add(round(stats.sober_in*60), :second)
|> Timex.Timezone.convert("Europe/Paris")
at = if now.day == sober.day do
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
detail
else
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
detail
end
"#{nick} sobre #{at} (dans #{stats.sober_in_s})"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
|> (fn(line) ->
case line do
"" -> "tout le monde est sobre......."
line -> line
end
end).()
|> m.replyfun.()
{:noreply, state}
end
def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
account = case args do
[nick] -> IRC.Account.find_always_by_nick(m.network, m.channel, nick)
[] -> m.account
end
if account do
user = IRC.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
stats = get_full_statistics(state, account.id)
if stats && stats.sober_in > 0 do
now = DateTime.utc_now()
sober = now |> DateTime.add(round(stats.sober_in*60), :second)
|> Timex.Timezone.convert("Europe/Paris")
at = if now.day == sober.day do
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
detail
else
{:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
detail
end
m.replyfun.("#{nick} sera sobre #{at} (dans #{stats.sober_in_s})!")
else
m.replyfun.("#{nick} est déjà sobre. aidez le !")
end
else
m.replyfun.("inconnu")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :dot}}}, state) do
nicks = IRC.Membership.expanded_members_or_friends(m.account, m.network, m.channel)
|> Enum.map(fn({account, _, nick}) -> {nick, get_full_statistics(state, account.id)} end)
|> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
|> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
|> Enum.map(fn({nick, status}) ->
trend_symbol = if status.active_drinks > 1 do
Enum.join(for(_ <- 1..status.active_drinks, do: status.trend_symbol))
else
status.trend_symbol
end
since_str = if status.since_min > 180 do
"depuis: #{status.since_s} | "
else
""
end
"#{nick} #{status.user_status} #{trend_symbol} #{Float.round(status.active, 4)} g/l [#{since_str}sobre dans: #{status.sober_in_s}]"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
msg = if nicks == "" do
"wtf?!?! personne n'a bu!"
else
nicks
end
m.replyfun.(msg)
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [time], type: :dot}}}, state) do
time = case time do
"semaine" -> 7
string ->
case Integer.parse(string) do
{time, "j"} -> time
{time, "J"} -> time
_ -> nil
end
end
if time do
aday = time*((24 * 60)*60)
now = DateTime.utc_now()
before = now
|> DateTime.add(-aday, :second)
|> DateTime.to_unix(:millisecond)
over_time_stats(before, time, m, state)
else
m.replyfun.(".alcooolisme semaine|Xj")
end
{:noreply, state}
end
def user_over_time(account, count) do
user_over_time(data_state(), account, count)
end
def user_over_time(state, account, count) do
delay = count*((24 * 60)*60)
now = DateTime.utc_now()
before = DateTime.utc_now()
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
|> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
|> DateTime.to_unix(:millisecond)
#[
# {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
# [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]}
#]
match = [{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
[{:andalso, {:>, :"$2", {:const, before}}, {:==, :"$1", {:const, account.id}}}], [:"$_"]}
]
:ets.select(state.ets, match)
|> Enum.reduce(Map.new, fn({{_, ts}, vol, _, _, _, _, _, _}, acc) ->
date = DateTime.from_unix!(ts, :millisecond)
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
date = if date.hour <= 8 do
DateTime.add(date, -(60*(60*(date.hour+1))), :second, Tzdata.TimeZoneDatabase)
else
date
end
|> DateTime.to_date()
Map.put(acc, date, Map.get(acc, date, 0) + vol)
end)
end
def user_over_time_gl(account, count) do
state = data_state()
meta = get_user_meta(state, account.id)
delay = count*((24 * 60)*60)
now = DateTime.utc_now()
before = DateTime.utc_now()
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
|> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
|> DateTime.to_unix(:millisecond)
#[
# {{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
# [{:andalso, {:==, :"$1", :"$1"}, {:<, :"$2", {:const, 3000}}}], [:lol]}
#]
match = [{{{:"$1", :"$2"}, :_, :_, :_, :_, :_, :_, :_},
[{:andalso, {:>, :"$2", {:const, before}}, {:==, :"$1", {:const, account.id}}}], [:"$_"]}
]
:ets.select(state.ets, match)
|> Enum.reduce(Map.new, fn({{_, ts}, vol, _, _, _, _, _, _}, acc) ->
date = DateTime.from_unix!(ts, :millisecond)
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
date = if date.hour <= 8 do
DateTime.add(date, -(60*(60*(date.hour+1))), :second, Tzdata.TimeZoneDatabase)
else
date
end
|> DateTime.to_date()
weight = meta.weight
k = if meta.sex, do: 0.7, else: 0.6
gl = (10*vol)/(k*weight)
Map.put(acc, date, Map.get(acc, date, 0) + gl)
end)
end
defp over_time_stats(before, j, m, state) do
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _, _, _, _}) when date > before -> obj end)
match = [{{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_},
[{:>, :"$1", {:const, before}}], [:"$_"]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
members = IRC.Membership.members_or_friends(m.account, m.network, m.channel)
drinks = :ets.select(state.ets, match)
|> Enum.filter(fn({{account, _}, _, _, _, _, _, _, _}) -> Enum.member?(members, account) end)
|> Enum.sort_by(fn({{_, ts}, _, _, _, _, _, _, _}) -> ts end, &>/2)
top = Enum.reduce(drinks, %{}, fn({{nick, _}, vol, _, _, _, _, _, _}, acc) ->
all = Map.get(acc, nick, 0)
Map.put(acc, nick, all + vol)
end)
|> Enum.sort_by(fn({_nick, count}) -> count end, &>/2)
|> Enum.map(fn({nick, count}) ->
account = IRC.Account.get(nick)
user = IRC.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
"#{nick}: #{Float.round(count, 4)}"
end)
|> Enum.intersperse(", ")
m.replyfun.("sur #{j} jours: #{top}")
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do
meta = get_user_meta(state, m.account.id)
hf = if meta.sex, do: "h", else: "f"
m.replyfun.("+alcoolisme sexe: #{hf} poids: #{meta.weight} facteur de perte: #{meta.loss_factor}")
{:noreply, state}
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [h, weight | rest], type: :plus}}}, state) do
h = case h do
"h" -> true
"f" -> false
_ -> nil
end
weight = case Util.float_paparse(weight) do
{weight, _} -> weight
_ -> nil
end
{factor} = case rest do
[factor] ->
case Util.float_paparse(factor) do
{float, _} -> {float}
_ -> {@default_user_meta.loss_factor}
end
_ -> {@default_user_meta.loss_factor}
end
if h == nil || weight == nil do
m.replyfun.("paramètres invalides")
else
old_meta = get_user_meta(state, m.account.id)
meta = Map.merge(@default_user_meta, %{sex: h, weight: weight, loss_factor: factor})
put_user_meta(state, m.account.id, meta)
cond do
old_meta.weight < meta.weight ->
- LSG.IRC.TxtPlugin.reply_random(m, "alcoolog.fatter")
+ Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.fatter")
old_meta.weight == meta.weight ->
m.replyfun.("aucun changement!")
true ->
- LSG.IRC.TxtPlugin.reply_random(m, "alcoolog.thinner")
+ Nola.IRC.TxtPlugin.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}")
- LSG.IRC.TxtPlugin.reply_random(m, "alcoolog.delete")
+ Nola.IRC.TxtPlugin.reply_random(m, "alcoolog.delete")
notify = IRC.Membership.notify_channels(m.account) -- [{m.network,m.channel}]
for {net, chan} <- notify do
user = IRC.UserTrack.find_by_account(net, m.account)
nick = if(user, do: user.nick, else: m.account.name)
IRC.Connection.broadcast_message(net, chan, "#{nick} -santai #{points} #{type} #{descr}")
end
{:noreply, state}
_ ->
{:noreply, state}
end
end
def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
{account, duration} = case args do
[nick | rest] -> {IRC.Account.find_always_by_nick(m.network, m.channel, nick), rest}
[] -> {m.account, []}
end
if account do
duration = case duration do
["semaine"] -> 7
[j] ->
case Integer.parse(j) do
{j, "j"} -> j
_ -> nil
end
_ -> nil
end
user = IRC.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
if duration do
if duration > 90 do
m.replyfun.("trop gros, ça rentrera pas")
else
# duration stats
stats = user_over_time(state, account, duration)
|> Enum.sort_by(fn({k,_v}) -> k end, {:asc, Date})
|> Enum.map(fn({date, count}) ->
"#{date.day}: #{Float.round(count, 2)}"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
if stats == "" do
m.replyfun.("alcoolisme a zéro sur #{duration}j :/")
else
m.replyfun.("alcoolisme de #{nick}, #{duration} derniers jours: #{stats}")
end
end
else
if stats = get_full_statistics(state, account.id) do
trend_symbol = if stats.active_drinks > 1 do
Enum.join(for(_ <- 1..stats.active_drinks, do: stats.trend_symbol))
else
stats.trend_symbol
end
# TODO: Lookup nick for account_id
msg = "#{nick} #{stats.user_status} "
<> (if stats.active > 0 || stats.active15m > 0 || stats.active30m > 0 || stats.active1h > 0, do: ": #{trend_symbol} #{Float.round(stats.active, 4)}g/l ", else: "")
<> (if stats.active30m > 0 || stats.active1h > 0, do: "(15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) ", else: "")
<> (if stats.sober_in > 0, do: "— Sobre dans #{stats.sober_in_s} ", else: "")
<> (if stats.since && stats.since_min > 180, do: "— Paitai depuis #{stats.since_s} ", else: "")
<> "— Dernier verre: #{present_type(stats.last_type, stats.last_descr)} [#{Float.round(stats.last_points+0.0, 4)}] "
<> "#{format_duration_from_now(stats.last_at)} "
<> (if stats.daily_volumes > 0, do: "— Aujourd'hui: #{stats.daily_volumes} #{stats.daily_gl}g/l", else: "")
m.replyfun.(msg)
else
m.replyfun.("honteux mais #{nick} n'a pas l'air alcoolique du tout. /kick")
end
end
else
m.replyfun.("je ne connais pas cet utilisateur")
end
{:noreply, state}
end
# Account merge
def handle_info({:account_change, old_id, new_id}, state) do
spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
Logger.debug("alcolog/account_change:: merging #{old_id} -> #{new_id}")
rename_object_owner(table, state.ets, obj, old_id, new_id)
end)
case :dets.lookup(state.meta, {:meta, old_id}) do
[{_, meta}] ->
:dets.delete(state.meta, {:meta, old_id})
:dets.insert(state.meta, {{:meta, new_id}, meta})
_ ->
:ok
end
{:noreply, state}
end
def terminate(_, state) do
for dets <- [state.dets, state.meta] do
:dets.sync(dets)
:dets.close(dets)
end
end
defp rename_object_owner(table, ets, object = {old_id, date, volume, current, cl, deg, name, comment, meta}, old_id, new_id) do
:dets.delete_object(table, object)
:ets.delete(ets, {old_id, date})
:dets.insert(table, {new_id, date, volume, current, cl, deg, name, comment, meta})
:ets.insert(ets, {{new_id, date}, volume, current, cl, deg, name, comment, meta})
end
# Account: move from nick to account id
def handle_info({:accounts, accounts}, state) do
#for x={:account, _, _, _, _} <- accounts, do: handle_info(x, state)
#{:noreply, state}
mapping = Enum.reduce(accounts, Map.new, fn({:account, _net, _chan, nick, account_id}, acc) ->
Map.put(acc, String.downcase(nick), account_id)
end)
spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [], [:"$_"]}]
Logger.debug("accounts:: mappings #{inspect mapping}")
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj = {nick, _date, _vol, _cur, _cl, _deg, _name, _comment, _meta}) ->
#Logger.debug("accounts:: item #{inspect(obj)}")
if new_id = Map.get(mapping, nick) do
Logger.debug("alcolog/accounts:: merging #{nick} -> #{new_id}")
rename_object_owner(table, state.ets, obj, nick, new_id)
end
end)
{:noreply, state}
end
def handle_info({:account, _net, _chan, nick, account_id}, state) do
nick = String.downcase(nick)
spec = [{{:"$1", :_, :_, :_, :_, :_, :_, :_, :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
Logger.debug("alcoolog/account:: merging #{nick} -> #{account_id}")
rename_object_owner(table, state.ets, obj, nick, account_id)
end)
case :dets.lookup(state.meta, {:meta, nick}) do
[{_, meta}] ->
:dets.delete(state.meta, {:meta, nick})
:dets.insert(state.meta, {{:meta, account_id}, meta})
_ ->
:ok
end
{:noreply, state}
end
def handle_info(t, state) do
Logger.debug("AlcoologPlugin: unhandled info #{inspect t}")
{: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/lsg_irc/alcoolog_announcer_plugin.ex b/lib/lsg_irc/alcoolog_announcer_plugin.ex
index 3902d5f..f90dc42 100644
--- a/lib/lsg_irc/alcoolog_announcer_plugin.ex
+++ b/lib/lsg_irc/alcoolog_announcer_plugin.ex
@@ -1,272 +1,272 @@
-defmodule LSG.IRC.AlcoologAnnouncerPlugin do
+defmodule Nola.IRC.AlcoologAnnouncerPlugin 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 = (LSG.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist
+ dets_filename = (Nola.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
from = ~U[2020-08-23 19:41:40.524154Z]
to = ~U[2020-08-24 19:41:40.524154Z]
select = [
{{:"$1", :"$2", :_},
[
{:andalso,
{:andalso, {:==, :"$1", {:const, account.id}},
{:>, :"$2", {:const, DateTime.to_unix(from)}}},
{:<, :"$2", {:const, DateTime.to_unix(to)}}}
], [:"$_"]}
]
res = :dets.select(dets, select)
:dets.close(dets)
res
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "account", [])
stats = get_stats()
Process.send_after(self(), :stats, :timer.seconds(30))
- dets_filename = (LSG.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist
+ 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 = LSG.IRC.SettingPlugin.declare("alcoolog.alerts", __MODULE__, true, :boolean)
- #:ok = LSG.IRC.SettingPlugin.declare("alcoolog.aperoalert", __MODULE__, true, :boolean)
+ #:ok = Nola.IRC.SettingPlugin.declare("alcoolog.alerts", __MODULE__, true, :boolean)
+ #:ok = Nola.IRC.SettingPlugin.declare("alcoolog.aperoalert", __MODULE__, true, :boolean)
#
{:ok, {stats, now(), dets, ets}}#, {:continue, :traverse}}
end
def handle_continue(:traverse, state = {_, _, dets, ets}) do
traverse_fun = fn(obj, dets) ->
case obj do
{nick, %DateTime{} = dt, active} ->
:dets.delete_object(dets, obj)
:dets.insert(dets, {nick, DateTime.to_unix(dt), active})
IO.puts("ok #{inspect obj}")
dets
{nick, ts, value} ->
:ets.insert(ets, { {nick, ts}, value })
dets
end
end
:dets.foldl(traverse_fun, dets, dets)
:dets.sync(dets)
IO.puts("alcoolog announcer fixed")
{:noreply, state}
end
def alcohol_reached(old, new, level) do
(old.active < level && new.active >= level) && (new.active5m >= level)
end
def alcohol_below(old, new, level) do
(old.active > level && new.active <= level) && (new.active5m <= level)
end
def handle_info(:stats, {old_stats, old_now, dets, ets}) do
stats = get_stats()
now = now()
if old_now.hour < 18 && now.hour == 18 do
apero = Enum.shuffle(@apero)
|> Enum.random()
case apero do
{:timed, list} ->
spawn(fn() ->
for line <- list do
IRC.Connection.broadcast_message("evolu.net", "#dmz", line)
:timer.sleep(:timer.seconds(5))
end
end)
string ->
IRC.Connection.broadcast_message("evolu.net", "#dmz", string)
end
end
#IO.puts "newstats #{inspect stats}"
events = for {acct, old} <- old_stats do
new = Map.get(stats, acct, nil)
#IO.puts "#{acct}: #{inspect(old)} -> #{inspect(new)}"
now = DateTime.to_unix(DateTime.utc_now())
if new && new[:active] do
:dets.insert(dets, {acct, now, new[:active]})
:ets.insert(ets, {{acct, now}, new[:active]})
else
:dets.insert(dets, {acct, now, 0.0})
:ets.insert(ets, {{acct, now}, new[:active]})
end
event = cond do
old == nil -> nil
(old.active > 0) && (new == nil) -> :sober
new == nil -> nil
alcohol_reached(old, new, 0.5) -> :stopconduire
alcohol_reached(old, new, 1.0) -> :g1
alcohol_reached(old, new, 2.0) -> :g2
alcohol_reached(old, new, 3.0) -> :g3
alcohol_reached(old, new, 4.0) -> :g4
alcohol_reached(old, new, 5.0) -> :g5
alcohol_reached(old, new, 6.0) -> :g6
alcohol_reached(old, new, 7.0) -> :g7
alcohol_reached(old, new, 10.0) -> :g10
alcohol_reached(old, new, 13.74) -> :record
alcohol_below(old, new, 0.5) -> :conduire
alcohol_below(old, new, 1.0) -> :fini1g
alcohol_below(old, new, 2.0) -> :fini2g
alcohol_below(old, new, 3.0) -> :fini3g
alcohol_below(old, new, 4.0) -> :fini4g
(old.rising) && (!new.rising) -> :lowering
true -> nil
end
{acct, event}
end
for {acct, event} <- events do
message = case event do
:g1 -> [
"[vigicuite jaune] LE GRAMME! LE GRAMME O/",
"début de vigicuite jaune ! LE GRAMME ! \\O/",
"waiiiiiiii le grammmeee",
"bourraiiiiiiiiiiide 1 grammeeeeeeeeeee",
]
:g2 -> [
"[vigicuite orange] \\o_YAY 2 GRAMMES ! _o/",
"PAITAIIIIIIIIII DEUX GRAMMEESSSSSSSSSSSSSSSSS",
"bourrrrrraiiiiiiiiiiiiiiiide 2 grammeeeeeeeeeees",
]
:g3 -> [
"et un ! et deux ! et TROIS GRAMMEEESSSSSSS",
"[vigicuite rouge] _o/ BOURRAIIDDDEEEE 3 GRAMMESSSSSSSSS \\o/ \\o/"
]
:g4 -> [
"[vigicuite écarlate] et un, et deux, et trois, ET QUATRES GRAMMEESSSSSSSSSSSSSSSSSSSssssss"
]
:g5 -> "[vigicuite écarlate+] PUTAIN 5 GRAMMES !"
:g6 -> "[vigicuite écarlate++] 6 grammes ? Vous pouvez joindre Alcool info service au 0 980 980 930"
:g7 -> "[vigicuite c'est la merde] 7 grammes. Le SAMU, c'est le 15."
:g10 -> "BORDLE 10 GRAMMES"
:record -> "RECORD DU MONDE BATTU ! >13.74g/l !!"
:fini1g -> [
"fin d'alerte vigicuite jaune, passage en vert (<1g/l)",
"/!\\ alerte moins de 1g/l /!\\"
]
:fini2g -> [
"t'as moins de 2 g/l, faut se reprendre là [vigicuite jaune]"
]
:fini3g -> [
"fin d'alerte vigicuite rouge, passage en orange (<3g/l)"
]
:fini4g -> [
"fin d'alerte vigicuite écarlate, passage en rouge (<4g/l)"
]
:lowering -> [
"attention ça baisse!",
"tu vas quand même pas en rester là ?",
"IL FAUT CONTINUER À BOIRE !",
"t'abandonnes déjà ?",
"!santai ?",
"faut pas en rester là",
"il faut se resservir",
"coucou faut reboire",
"encore un petit verre ?",
"abwaaaaaaaaaaaaarrrrrrrrrrrrrr",
"taux d'alcoolémie en chute ! agissez avant qu'il soit trop tard!",
"ÇA BAISSE !!"
]
:stopconduire -> [
"0.5g! bientot le gramme?",
"tu peux plus prendre la route... mais... tu peux prendre la route du gramme! !santai !",
"fini la conduite!",
"0.5! continues faut pas en rester là!",
"beau début, continues !",
"ça monte! 0.5g/l!"
]
:conduire -> [
"tu peux conduire, ou recommencer à boire! niveau critique!",
"!santai ?",
"tu peux reprendre la route, ou reprendre la route du gramme..",
"attention, niveau critique!",
"il faut boire !!",
"trop de sang dans ton alcool, c'est mauvais pour la santé",
"faut pas en rester là !",
]
:sober -> [
"sobre…",
"/!\\ alerte sobriété /!\\",
"... sobre?!?!",
"sobre :(",
"attention, t'es sobre :/",
"danger, alcoolémie à 0.0 !",
"sobre! c'était bien on recommence quand ?",
"sobre ? Faut recommencer...",
"T'es sobre. Ne te laisses pas abattre- ton caviste peut aider.",
"Vous êtes sobre ? Ceci n'est pas une fatalité - resservez vous vite !"
]
_ -> nil
end
message = case message do
m when is_binary(m) -> m
m when is_list(m) -> m |> Enum.shuffle() |> Enum.random()
nil -> nil
end
if message do
#IO.puts("#{acct}: #{message}")
account = IRC.Account.get(acct)
for {net, chan} <- IRC.Membership.notify_channels(account) do
user = IRC.UserTrack.find_by_account(net, account)
nick = if(user, do: user.nick, else: account.name)
IRC.Connection.broadcast_message(net, chan, "#{nick}: #{message}")
end
end
end
timer()
#IO.puts "tick stats ok"
{:noreply, {stats,now,dets,ets}}
end
def handle_info(_, state) do
{:noreply, state}
end
defp now() do
DateTime.utc_now()
|> Timex.Timezone.convert("Europe/Paris")
end
defp get_stats() do
- Enum.into(LSG.IRC.AlcoologPlugin.get_all_stats(), %{})
+ Enum.into(Nola.IRC.AlcoologPlugin.get_all_stats(), %{})
end
defp timer() do
Process.send_after(self(), :stats, :timer.seconds(@seconds))
end
end
diff --git a/lib/lsg_irc/base_plugin.ex b/lib/lsg_irc/base_plugin.ex
index 7153c70..ee0352e 100644
--- a/lib/lsg_irc/base_plugin.ex
+++ b/lib/lsg_irc/base_plugin.ex
@@ -1,131 +1,131 @@
-defmodule LSG.IRC.BasePlugin do
+defmodule Nola.IRC.BasePlugin do
def irc_doc, do: nil
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:version", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:help", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:liquidrender", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:plugin", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:plugins", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, "plugins", msg = %{trigger: %{type: :bang, args: []}}}, _) do
enabled_string = IRC.Plugin.enabled()
|> Enum.map(fn(mod) ->
mod
|> Macro.underscore()
|> String.split("/", parts: :infinity)
|> List.last()
|> String.replace("_plugin", "")
|> Enum.sort()
end)
|> Enum.join(", ")
msg.replyfun.("Enabled plugins: #{enabled_string}")
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :query, args: [plugin]}} = m}, _) do
- module = Module.concat([LSG.IRC, Macro.camelize(plugin<>"_plugin")])
+ module = Module.concat([Nola.IRC, Macro.camelize(plugin<>"_plugin")])
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module)
do
m.replyfun.("loaded, active: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
nil ->
msg = case IRC.Plugin.get(module) do
:disabled -> "disabled"
{_, false, _} -> "disabled"
_ -> "not active"
end
m.replyfun.(msg)
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :plus, args: [plugin]}} = m}, _) do
- module = Module.concat([LSG.IRC, Macro.camelize(plugin<>"_plugin")])
+ module = Module.concat([Nola.IRC, Macro.camelize(plugin<>"_plugin")])
with true <- Code.ensure_loaded?(module),
IRC.Plugin.switch(module, true),
{:ok, pid} <- IRC.Plugin.start(module)
do
m.replyfun.("started: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
:ignore -> m.replyfun.("disabled or throttled")
{:error, _} -> m.replyfun.("start error")
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :tilde, args: [plugin]}} = m}, _) do
- module = Module.concat([LSG.IRC, Macro.camelize(plugin<>"_plugin")])
+ module = Module.concat([Nola.IRC, Macro.camelize(plugin<>"_plugin")])
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module),
:ok <- GenServer.stop(pid),
{:ok, pid} <- IRC.Plugin.start(module)
do
m.replyfun.("restarted: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
nil -> m.replyfun.("not active")
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "plugin", %{trigger: %{type: :minus, args: [plugin]}} = m}, _) do
- module = Module.concat([LSG.IRC, Macro.camelize(plugin<>"_plugin")])
+ module = Module.concat([Nola.IRC, Macro.camelize(plugin<>"_plugin")])
with true <- Code.ensure_loaded?(module),
pid when is_pid(pid) <- GenServer.whereis(module),
:ok <- GenServer.stop(pid)
do
IRC.Plugin.switch(module, false)
m.replyfun.("stopped: #{inspect(pid)}")
else
false -> m.replyfun.("not loaded")
nil -> m.replyfun.("not active")
end
{:noreply, nil}
end
def handle_info({:irc, :trigger, "liquidrender", m = %{trigger: %{args: args}}}, _) do
template = Enum.join(args, " ")
m.replyfun.(Tmpl.render(template, m))
{:noreply, nil}
end
def handle_info({:irc, :trigger, "help", m = %{trigger: %{type: :bang}}}, _) do
- url = LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :index, m.network, LSGWeb.format_chan(m.channel))
+ url = NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :index, m.network, NolaWeb.format_chan(m.channel))
m.replyfun.("-> #{url}")
{:noreply, nil}
end
def handle_info({:irc, :trigger, "version", message = %{trigger: %{type: :bang}}}, _) do
{:ok, vsn} = :application.get_key(:lsg, :vsn)
ver = List.to_string(vsn)
- url = LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :index)
+ url = NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :index)
elixir_ver = Application.started_applications() |> List.keyfind(:elixir, 0) |> elem(2) |> to_string()
otp_ver = :erlang.system_info(:system_version) |> to_string() |> String.trim()
system = :erlang.system_info(:system_architecture) |> to_string()
message.replyfun.([
- <<"🤖 I am a robot running", 2, "beautte, version #{ver}", 2, " — source: #{LSG.source_url()}">>,
+ <<"🤖 I am a robot running", 2, "beautte, version #{ver}", 2, " — source: #{Nola.source_url()}">>,
"🦾 Elixir #{elixir_ver} #{otp_ver} on #{system}",
"👷‍♀️ Owner: href <href@random.sh>",
"🌍 Web interface: #{url}"
])
{:noreply, nil}
end
def handle_info(msg, _) do
{:noreply, nil}
end
end
diff --git a/lib/lsg_irc/bourosama_plugin.ex b/lib/lsg_irc/bourosama_plugin.ex
index ba63d81..dd05144 100644
--- a/lib/lsg_irc/bourosama_plugin.ex
+++ b/lib/lsg_irc/bourosama_plugin.ex
@@ -1,58 +1,58 @@
-defmodule LSG.IRC.BoursoramaPlugin do
+defmodule Nola.IRC.BoursoramaPlugin do
def irc_doc() do
"""
# bourses
Un peu comme [finance](#finance), mais en un peu mieux, et un peu moins bien.
Source: [boursorama.com](https://boursorama.com)
* **!caca40** affiche l'état du cac40
"""
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@cac40_url "https://www.boursorama.com/bourse/actions/palmares/france/?france_filter%5Bmarket%5D=1rPCAC&france_filter%5Bsector%5D=&france_filter%5Bvariation%5D=50002&france_filter%5Bperiod%5D=1&france_filter%5Bfilter%5D="
def init(_) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:cac40", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:caca40", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, cac, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) when cac in ["cac40", "caca40"] do
case HTTPoison.get(@cac40_url, [], []) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
html = Floki.parse(body)
board = Floki.find(body, "div.c-tradingboard")
cac40 = Floki.find(board, ".c-tradingboard__main > .c-tradingboard__infos")
instrument = Floki.find(cac40, ".c-instrument")
last = Floki.find(instrument, "span[data-ist-last]")
|> Floki.text()
|> String.replace(" ", "")
variation = Floki.find(instrument, "span[data-ist-variation]")
|> Floki.text()
sign = case variation do
"-"<>_ -> "▼"
"+" -> "▲"
_ -> ""
end
m.replyfun.("caca40: #{sign} #{variation} #{last}")
{:error, %HTTPoison.Response{status_code: code}} ->
m.replyfun.("caca40: erreur http #{code}")
_ ->
m.replyfun.("caca40: erreur http")
end
end
end
diff --git a/lib/lsg_irc/buffer_plugin.ex b/lib/lsg_irc/buffer_plugin.ex
index d278151..eece34e 100644
--- a/lib/lsg_irc/buffer_plugin.ex
+++ b/lib/lsg_irc/buffer_plugin.ex
@@ -1,44 +1,44 @@
-defmodule LSG.IRC.BufferPlugin do
+defmodule Nola.IRC.BufferPlugin do
@table __MODULE__.ETS
def irc_doc, do: nil
def table(), do: @table
def select_buffer(network, channel, limit \\ 50) do
import Ex2ms
spec = fun do {{n, c, _}, m} when n == ^network and (c == ^channel or is_nil(c)) -> m end
:ets.select(@table, spec, limit)
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
for e <- ~w(messages triggers events outputs) do
{:ok, _} = Registry.register(IRC.PubSub, e, plugin: __MODULE__)
end
{:ok, :ets.new(@table, [:named_table, :ordered_set, :protected])}
end
def handle_info({:irc, :trigger, _, message}, ets), do: handle_message(message, ets)
def handle_info({:irc, :text, message}, ets), do: handle_message(message, ets)
def handle_info({:irc, :event, event}, ets), do: handle_message(event, ets)
defp handle_message(message = %{network: network}, ets) do
key = {network, Map.get(message, :channel), ts(message.at)}
:ets.insert(ets, {key, message})
{:noreply, ets}
end
defp ts(nil), do: ts(NaiveDateTime.utc_now())
defp ts(naive = %NaiveDateTime{}) do
ts = naive
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_unix()
-ts
end
end
diff --git a/lib/lsg_irc/calc_plugin.ex b/lib/lsg_irc/calc_plugin.ex
index ca65675..264370c 100644
--- a/lib/lsg_irc/calc_plugin.ex
+++ b/lib/lsg_irc/calc_plugin.ex
@@ -1,37 +1,37 @@
-defmodule LSG.IRC.CalcPlugin do
+defmodule Nola.IRC.CalcPlugin do
@moduledoc """
# calc
* **!calc `<expression>`**: évalue l'expression mathématique `<expression>`.
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:calc", [plugin: __MODULE__])
{:ok, nil}
end
def handle_info({:irc, :trigger, "calc", message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: expr_list}}}, state) do
expr = Enum.join(expr_list, " ")
result = try do
case Abacus.eval(expr) do
{:ok, result} -> result
error -> inspect(error)
end
rescue
error -> if(error[:message], do: "#{error.message}", else: "erreur")
end
message.replyfun.("#{message.sender.nick}: #{expr} = #{result}")
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
end
diff --git a/lib/lsg_irc/coronavirus_plugin.ex b/lib/lsg_irc/coronavirus_plugin.ex
index b9a9e40..d04d8f9 100644
--- a/lib/lsg_irc/coronavirus_plugin.ex
+++ b/lib/lsg_irc/coronavirus_plugin.ex
@@ -1,172 +1,172 @@
-defmodule LSG.IRC.CoronavirusPlugin do
+defmodule Nola.IRC.CoronavirusPlugin do
require Logger
NimbleCSV.define(CovidCsv, separator: ",", escape: "\"")
@moduledoc """
# Corona Virus
Données de [Johns Hopkins University](https://github.com/CSSEGISandData/COVID-19) et mises à jour a peu près tous les jours.
* `!coronavirus [France | Country]`: :-)
* `!coronavirus`: top 10 confirmés et non guéris
* `!coronavirus confirmés`: top 10 confirmés
* `!coronavirus morts`: top 10 morts
* `!coronavirus soignés`: top 10 soignés
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:coronavirus", [plugin: __MODULE__])
{:ok, nil, {:continue, :init}}
:ignore
end
def handle_continue(:init, _) do
date = Date.add(Date.utc_today(), -2)
{data, _} = fetch_data(%{}, date)
{data, next} = fetch_data(data)
:timer.send_after(next, :update)
{:noreply, %{data: data}}
end
def handle_info(:update, state) do
{data, next} = fetch_data(state.data)
:timer.send_after(next, :update)
{:noreply, %{data: data}}
end
def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :bang, args: args}}}, state) when args in [
[], ["morts"], ["confirmés"], ["soignés"], ["malades"], ["n"], ["nmorts"], ["nsoignés"], ["nconfirmés"]] do
{field, name} = case args do
["confirmés"] -> {:confirmed, "confirmés"}
["morts"] -> {:deaths, "morts"}
["soignés"] -> {:recovered, "soignés"}
["nmorts"] -> {:new_deaths, "nouveaux morts"}
["nconfirmés"] -> {:new_confirmed, "nouveaux confirmés"}
["n"] -> {:new_current, "nouveaux malades"}
["nsoignés"] -> {:new_recovered, "nouveaux soignés"}
_ -> {:current, "malades"}
end
IO.puts("FIELD #{inspect field}")
field_evol = String.to_atom("new_#{field}")
sorted = state.data
|> Enum.filter(fn({_, %{region: region}}) -> region == true end)
|> Enum.map(fn({location, data}) -> {location, Map.get(data, field, 0), Map.get(data, field_evol, 0)} end)
|> Enum.sort_by(fn({_,count,_}) -> count end, &>=/2)
|> Enum.take(10)
|> Enum.with_index()
|> Enum.map(fn({{location, count, evol}, index}) ->
ev = if String.starts_with?(name, "nouveaux") do
""
else
" (#{Util.plusminus(evol)})"
end
"##{index+1}: #{location} #{count}#{ev}"
end)
|> Enum.intersperse(" - ")
|> Enum.join()
m.replyfun.("CORONAVIRUS TOP10 #{name}: " <> sorted)
{:noreply, state}
end
def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :bang, args: location}}}, state) do
location = Enum.join(location, " ") |> String.downcase()
if data = Map.get(state.data, location) do
m.replyfun.("coronavirus: #{location}: "
<> "#{data.current} malades (#{Util.plusminus(data.new_current)}), "
<> "#{data.confirmed} confirmés (#{Util.plusminus(data.new_confirmed)}), "
<> "#{data.deaths} morts (#{Util.plusminus(data.new_deaths)}), "
<> "#{data.recovered} soignés (#{Util.plusminus(data.new_recovered)}) (@ #{data.update})")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :query, args: location}}}, state) do
m.replyfun.("https://github.com/CSSEGISandData/COVID-19")
{:noreply, state}
end
# 1. Try to fetch data for today
# 2. Fetch yesterday if no results
defp fetch_data(current_data, date \\ nil) do
now = Date.utc_today()
url = fn(date) ->
"https://github.com/CSSEGISandData/COVID-19/raw/master/csse_covid_19_data/csse_covid_19_daily_reports/#{date}.csv"
end
request_date = date || now
Logger.debug("Coronavirus check date: #{inspect request_date}")
{:ok, date_s} = Timex.format({request_date.year, request_date.month, request_date.day}, "%m-%d-%Y", :strftime)
cur_url = url.(date_s)
Logger.debug "Fetching URL #{cur_url}"
case HTTPoison.get(cur_url, [], follow_redirect: true) do
{:ok, %HTTPoison.Response{status_code: 200, body: csv}} ->
# Parse CSV update data
data = csv
|> CovidCsv.parse_string()
|> Enum.reduce(%{}, fn(line, acc) ->
case line do
# FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Deaths,Recovered,Active,Combined_Key
#0FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Deaths,Recovered,Active,Combined_Key,Incidence_Rate,Case-Fatality_Ratio
[_, _, state, region, update, _lat, _lng, confirmed, deaths, recovered, _active, _combined_key, _incidence_rate, _fatality_ratio] ->
state = String.downcase(state)
region = String.downcase(region)
confirmed = String.to_integer(confirmed)
deaths = String.to_integer(deaths)
recovered = String.to_integer(recovered)
current = (confirmed - recovered) - deaths
entry = %{update: update, confirmed: confirmed, deaths: deaths, recovered: recovered, current: current, region: region}
region_entry = Map.get(acc, region, %{update: nil, confirmed: 0, deaths: 0, recovered: 0, current: 0})
region_entry = %{
update: region_entry.update || update,
confirmed: region_entry.confirmed + confirmed,
deaths: region_entry.deaths + deaths,
current: region_entry.current + current,
recovered: region_entry.recovered + recovered,
region: true
}
changes = if old = Map.get(current_data, region) do
%{
new_confirmed: region_entry.confirmed - old.confirmed,
new_current: region_entry.current - old.current,
new_deaths: region_entry.deaths - old.deaths,
new_recovered: region_entry.recovered - old.recovered,
}
else
%{new_confirmed: 0, new_current: 0, new_deaths: 0, new_recovered: 0}
end
region_entry = Map.merge(region_entry, changes)
acc = Map.put(acc, region, region_entry)
acc = if state && state != "" do
Map.put(acc, state, entry)
else
acc
end
other ->
Logger.info("Coronavirus line failed: #{inspect line}")
acc
end
end)
Logger.info "Updated coronavirus database"
{data, :timer.minutes(60)}
{:ok, %HTTPoison.Response{status_code: 404}} ->
Logger.debug "Corona 404 #{cur_url}"
date = Date.add(date || now, -1)
fetch_data(current_data, date)
other ->
Logger.error "Coronavirus: Update failed #{inspect other}"
{current_data, :timer.minutes(5)}
end
end
end
diff --git a/lib/lsg_irc/correction_plugin.ex b/lib/lsg_irc/correction_plugin.ex
index a77c4a2..5f9b278 100644
--- a/lib/lsg_irc/correction_plugin.ex
+++ b/lib/lsg_irc/correction_plugin.ex
@@ -1,59 +1,59 @@
-defmodule LSG.IRC.CorrectionPlugin do
+defmodule Nola.IRC.CorrectionPlugin do
@moduledoc """
# correction
* `s/pattern/replace` replace `pattern` by `replace` in the last matching message
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "messages", [plugin: __MODULE__])
{:ok, _} = Registry.register(IRC.PubSub, "triggers", [plugin: __MODULE__])
{:ok, %{}}
end
# Trigger fallback
def handle_info({:irc, :trigger, _, m = %IRC.Message{}}, state) do
{:noreply, correction(m, state)}
end
def handle_info({:irc, :text, m = %IRC.Message{}}, state) do
{:noreply, correction(m, state)}
end
def correction(m, state) do
history = Map.get(state, key(m), [])
if String.starts_with?(m.text, "s/") do
case String.split(m.text, "/") do
["s", match, replace | _] ->
case Regex.compile(match) do
{:ok, reg} ->
repl = Enum.find(history, fn(m) -> Regex.match?(reg, m.text) end)
if repl do
new_text = String.replace(repl.text, reg, replace)
m.replyfun.("correction: <#{repl.sender.nick}> #{new_text}")
end
_ ->
m.replyfun.("correction: invalid regex")
end
_ -> m.replyfun.("correction: invalid regex format")
end
state
else
history = if length(history) > 100 do
{_, history} = List.pop_at(history, 99)
[m | history]
else
[m | history]
end
Map.put(state, key(m), history)
end
end
defp key(%{network: net, channel: chan}), do: "#{net}/#{chan}"
end
diff --git a/lib/lsg_irc/dice_plugin.ex b/lib/lsg_irc/dice_plugin.ex
index eafd88a..b5e7649 100644
--- a/lib/lsg_irc/dice_plugin.ex
+++ b/lib/lsg_irc/dice_plugin.ex
@@ -1,66 +1,66 @@
-defmodule LSG.IRC.DicePlugin do
+defmodule Nola.IRC.DicePlugin do
require Logger
@moduledoc """
# dice
* **!dice `[1 | lancés]` `[6 | faces]`**: lance une ou plusieurs fois un dé de 6 ou autre faces
"""
@default_faces 6
@default_rolls 1
@max_rolls 50
def short_irc_doc, do: "!dice (jeter un dé)"
defstruct client: nil, dets: nil
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:dice", [plugin: __MODULE__])
{:ok, %__MODULE__{}}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: args}}}, state) do
to_integer = fn(string, default) ->
case Integer.parse(string) do
{int, _} -> int
_ -> default
end
end
{rolls, faces} = case args do
[] -> {@default_rolls, @default_faces}
[faces, rolls] -> {to_integer.(rolls, @default_rolls), to_integer.(faces, @default_faces)}
[rolls] -> {to_integer.(rolls, @default_rolls), @default_faces}
end
roll(state, message, faces, rolls)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
defp roll(state, message, faces, 1) when faces > 0 do
random = :crypto.rand_uniform(1, faces+1)
message.replyfun.("#{message.sender.nick} dice: #{random}")
end
defp roll(state, message, faces, rolls) when faces > 0 and rolls > 0 and rolls <= @max_rolls do
{results, acc} = Enum.map_reduce(Range.new(1, rolls), 0, fn(i, acc) ->
random = :crypto.rand_uniform(1, faces+1)
{random, acc + random}
end)
results = Enum.join(results, "; ")
message.replyfun.("#{message.sender.nick} dice: [#{acc}] #{results}")
end
defp roll(_, _, _, _, _), do: nil
end
diff --git a/lib/lsg_irc/finance_plugin.ex b/lib/lsg_irc/finance_plugin.ex
index 51ec3f6..c1a1771 100644
--- a/lib/lsg_irc/finance_plugin.ex
+++ b/lib/lsg_irc/finance_plugin.ex
@@ -1,190 +1,190 @@
-defmodule LSG.IRC.FinancePlugin do
+defmodule Nola.IRC.FinancePlugin do
require Logger
@moduledoc """
# finance
Données de [alphavantage.co](https://alphavantage.co).
## forex / monnaies / crypto-monnaies
* **`!forex <MONNAIE1> [MONNAIE2]`**: taux de change entre deux monnaies.
* **`!forex <MONTANT> <MONNAIE1> <MONNAIE2>`**: converti `montant` entre deux monnaies
* **`?currency <recherche>`**: recherche une monnaie
Utiliser le symbole des monnaies (EUR, USD, ...).
## bourses
* **`!stocks <SYMBOLE>`**
* **`?stocks <recherche>`** cherche un symbole
Pour les symboles non-US, ajouter le suffixe (RNO Paris: RNO.PAR).
"""
@currency_list "http://www.alphavantage.co/physical_currency_list/"
@crypto_list "http://www.alphavantage.co/digital_currency_list/"
HTTPoison.start()
load_currency = fn(url) ->
resp = HTTPoison.get!(url)
resp.body
|> String.strip()
|> String.split("\n")
|> Enum.drop(1)
|> Enum.map(fn(line) ->
[symbol, name] = line
|> String.strip()
|> String.split(",", parts: 2)
{symbol, name}
end)
|> Enum.into(Map.new)
end
fiat = load_currency.(@currency_list)
crypto = load_currency.(@crypto_list)
@currencies Map.merge(fiat, crypto)
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:forex", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:currency", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:stocks", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, "stocks", message = %{trigger: %{type: :query, args: args = search}}}, state) do
search = Enum.join(search, "%20")
url = "https://www.alphavantage.co/query?function=SYMBOL_SEARCH&keywords=#{search}&apikey=#{api_key()}"
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: data}} ->
data = Poison.decode!(data)
if error = Map.get(data, "Error Message") do
Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
message.replyfun.("stocks: requête invalide")
else
items = for item <- Map.get(data, "bestMatches") do
symbol = Map.get(item, "1. symbol")
name = Map.get(item, "2. name")
type = Map.get(item, "3. type")
region = Map.get(item, "4. region")
currency = Map.get(item, "8. currency")
"#{symbol}: #{name} (#{region}; #{currency}; #{type})"
end
|> Enum.join(", ")
items = if items == "" do
"no results!"
else
items
end
message.replyfun.(items)
end
{:ok, resp = %HTTPoison.Response{status_code: code}} ->
Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
message.replyfun.("forex: erreur (api #{code})")
{:error, %HTTPoison.Error{reason: error}} ->
Logger.error "AlphaVantage HTTP error: #{inspect error}"
message.replyfun.("forex: erreur (http #{inspect error})")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "stocks", message = %{trigger: %{type: :bang, args: args = [symbol]}}}, state) do
url = "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=#{symbol}&apikey=#{api_key()}"
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: data}} ->
data = Poison.decode!(data)
if error = Map.get(data, "Error Message") do
Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
message.replyfun.("stocks: requête invalide")
else
data = Map.get(data, "Global Quote")
open = Map.get(data, "02. open")
high = Map.get(data, "03. high")
low = Map.get(data, "04. low")
price = Map.get(data, "05. price")
volume = Map.get(data, "06. volume")
prev_close = Map.get(data, "08. previous close")
change = Map.get(data, "09. change")
change_pct = Map.get(data, "10. change percent")
msg = "#{symbol}: #{price} #{change} [#{change_pct}] (high: #{high}, low: #{low}, open: #{open}, prev close: #{prev_close}) (volume: #{volume})"
message.replyfun.(msg)
end
{:ok, resp = %HTTPoison.Response{status_code: code}} ->
Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
message.replyfun.("stocks: erreur (api #{code})")
{:error, %HTTPoison.Error{reason: error}} ->
Logger.error "AlphaVantage HTTP error: #{inspect error}"
message.replyfun.("stocks: erreur (http #{inspect error})")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "forex", message = %{trigger: %{type: :bang, args: args = [_ | _]}}}, state) do
{amount, from, to} = case args do
[amount, from, to] ->
{amount, _} = Float.parse(amount)
{amount, from, to}
[from, to] ->
{1, from, to}
[from] ->
{1, from, "EUR"}
end
url = "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=#{from}&to_currency=#{to}&apikey=#{api_key()}"
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: data}} ->
data = Poison.decode!(data)
if error = Map.get(data, "Error Message") do
Logger.error("AlphaVantage API invalid request #{url} - #{inspect error}")
message.replyfun.("forex: requête invalide")
else
data = Map.get(data, "Realtime Currency Exchange Rate")
from_name = Map.get(data, "2. From_Currency Name")
to_name = Map.get(data, "4. To_Currency Name")
rate = Map.get(data, "5. Exchange Rate")
{rate, _} = Float.parse(rate)
value = amount*rate
message.replyfun.("#{amount} #{from} (#{from_name}) -> #{value} #{to} (#{to_name}) (#{rate})")
end
{:ok, resp = %HTTPoison.Response{status_code: code}} ->
Logger.error "AlphaVantage API error: #{code} #{url} - #{inspect resp}"
message.replyfun.("forex: erreur (api #{code})")
{:error, %HTTPoison.Error{reason: error}} ->
Logger.error "AlphaVantage HTTP error: #{inspect error}"
message.replyfun.("forex: erreur (http #{inspect error})")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "currency", message = %{trigger: %{type: :query, args: args = search}}}, state) do
search = Enum.join(search, " ")
results = Enum.filter(@currencies, fn({symbol, name}) ->
String.contains?(String.downcase(name), String.downcase(search)) || String.contains?(String.downcase(symbol), String.downcase(search))
end)
|> Enum.map(fn({symbol, name}) ->
"#{symbol}: #{name}"
end)
|> Enum.join(", ")
if results == "" do
message.replyfun.("no results!")
else
message.replyfun.(results)
end
{:noreply, state}
end
defp api_key() do
Application.get_env(:lsg, :alphavantage, [])
|> Keyword.get(:api_key, "demo")
end
end
diff --git a/lib/lsg_irc/gpt_plugin.ex b/lib/lsg_irc/gpt_plugin.ex
index e3cefa7..2c8f182 100644
--- a/lib/lsg_irc/gpt_plugin.ex
+++ b/lib/lsg_irc/gpt_plugin.ex
@@ -1,259 +1,259 @@
-defmodule LSG.IRC.GptPlugin do
+defmodule Nola.IRC.GptPlugin do
require Logger
import Irc.Plugin.TempRef
def irc_doc() do
"""
# OpenAI GPT
Uses OpenAI's GPT-3 API to bring natural language prompts to your IRC channel.
_prompts_ are pre-defined prompts and parameters defined in the bot' CouchDB.
_Runs_ (results of the inference of a _prompt_) are also stored in CouchDB and
may be resumed.
* **!gpt** list GPT prompts
* **!gpt `[prompt]` `<prompt or args>`** run a prompt
* **+gpt `[short ref|run id]` `<prompt or args>`** continue a prompt
* **?gpt offensive `<content>`** is content offensive ?
* **?gpt show `[short ref|run id]`** run information and web link
* **?gpt `[prompt]`** prompt information and web link
"""
end
@couch_db "bot-plugin-openai-prompts"
@couch_run_db "bot-plugin-gpt-history"
@trigger "gpt"
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
defstruct [:temprefs]
def get_result(id) do
Couch.get(@couch_run_db, id)
end
def get_prompt(id) do
Couch.get(@couch_db, id)
end
def init(_) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:#{@trigger}", regopts)
{:ok, %__MODULE__{temprefs: new_temp_refs()}}
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [prompt | args]}}}, state) do
case Couch.get(@couch_db, prompt) do
{:ok, prompt} -> {:noreply, prompt(m, prompt, Enum.join(args, " "), state)}
{:error, :not_found} ->
m.replyfun.("gpt: prompt '#{prompt}' does not exists")
{:noreply, state}
error ->
Logger.info("gpt: prompt load error: #{inspect error}")
m.replyfun.("gpt: database error")
{:noreply, state}
end
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do
case Couch.get(@couch_db, "_all_docs") do
{:ok, %{"rows" => []}} -> m.replyfun.("gpt: no prompts available")
{:ok, %{"rows" => prompts}} ->
prompts = prompts |> Enum.map(fn(prompt) -> Map.get(prompt, "id") end) |> Enum.join(", ")
m.replyfun.("gpt: prompts: #{prompts}")
error ->
Logger.info("gpt: prompt load error: #{inspect error}")
m.replyfun.("gpt: database error")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :plus, args: [ref_or_id | args]}}}, state) do
id = lookup_temp_ref(ref_or_id, state.temprefs, ref_or_id)
case Couch.get(@couch_run_db, id) do
{:ok, run} ->
Logger.debug("+gpt run: #{inspect run}")
{:noreply, continue_prompt(m, run, Enum.join(args, " "), state)}
{:error, :not_found} ->
m.replyfun.("gpt: ref or id not found or expired: #{inspect ref_or_id} (if using short ref, try using full id)")
{:noreply, state}
error ->
Logger.info("+gpt: run load error: #{inspect error}")
m.replyfun.("gpt: database error")
{:noreply, state}
end
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: ["offensive" | text]}}}, state) do
text = Enum.join(text, " ")
{moderate?, moderation} = moderation(text, m.account.id)
reply = cond do
moderate? -> "⚠️ #{Enum.join(moderation, ", ")}"
!moderate? && moderation -> "👍"
!moderate? -> "☠️ error"
end
m.replyfun.(reply)
{:noreply, state}
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: ["show", ref_or_id]}}}, state) do
id = lookup_temp_ref(ref_or_id, state.temprefs, ref_or_id)
url = if m.channel do
- LSGWeb.Router.Helpers.gpt_url(LSGWeb.Endpoint, :result, m.network, LSGWeb.format_chan(m.channel), id)
+ NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :result, m.network, NolaWeb.format_chan(m.channel), id)
else
- LSGWeb.Router.Helpers.gpt_url(LSGWeb.Endpoint, :result, id)
+ NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :result, id)
end
m.replyfun.("→ #{url}")
{:noreply, state}
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: [prompt]}}}, state) do
url = if m.channel do
- LSGWeb.Router.Helpers.gpt_url(LSGWeb.Endpoint, :prompt, m.network, LSGWeb.format_chan(m.channel), prompt)
+ NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :prompt, m.network, NolaWeb.format_chan(m.channel), prompt)
else
- LSGWeb.Router.Helpers.gpt_url(LSGWeb.Endpoint, :prompt, prompt)
+ NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :prompt, prompt)
end
m.replyfun.("→ #{url}")
{:noreply, state}
end
def handle_info(info, state) do
Logger.debug("gpt: unhandled info: #{inspect info}")
{:noreply, state}
end
defp continue_prompt(msg, run, content, state) do
prompt_id = Map.get(run, "prompt_id")
prompt_rev = Map.get(run, "prompt_rev")
original_prompt = case Couch.get(@couch_db, prompt_id, rev: prompt_rev) do
{:ok, prompt} -> prompt
_ -> nil
end
if original_prompt do
continue_prompt = %{"_id" => prompt_id,
"_rev" => prompt_rev,
"type" => Map.get(original_prompt, "type"),
"parent_run_id" => Map.get(run, "_id"),
"openai_params" => Map.get(run, "request") |> Map.delete("prompt")}
continue_prompt = if prompt_string = Map.get(original_prompt, "continue_prompt") do
full_text = get_in(run, ~w(request prompt)) <> "\n" <> Map.get(run, "response")
continue_prompt
|> Map.put("prompt", prompt_string)
|> Map.put("prompt_format", "liquid")
|> Map.put("prompt_liquid_variables", %{"previous" => full_text})
else
prompt_content_tag = if content != "", do: " {{content}}", else: ""
string = get_in(run, ~w(request prompt)) <> "\n" <> Map.get(run, "response") <> prompt_content_tag
continue_prompt
|> Map.put("prompt", string)
|> Map.put("prompt_format", "liquid")
end
prompt(msg, continue_prompt, content, state)
else
msg.replyfun.("gpt: cannot continue this prompt: original prompt not found #{prompt_id}@v#{prompt_rev}")
state
end
end
defp prompt(msg, prompt = %{"type" => "completions", "prompt" => prompt_template}, content, state) do
Logger.debug("gpt_plugin:prompt/4 #{inspect prompt}")
prompt_text = case Map.get(prompt, "prompt_format", "liquid") do
"liquid" -> Tmpl.render(prompt_template, msg, Map.merge(Map.get(prompt, "prompt_liquid_variables", %{}), %{"content" => content}))
"norender" -> prompt_template
end
args = Map.get(prompt, "openai_params")
|> Map.put("prompt", prompt_text)
|> Map.put("user", msg.account.id)
{moderate?, moderation} = moderation(content, msg.account.id)
if moderate?, do: msg.replyfun.("⚠️ offensive input: #{Enum.join(moderation, ", ")}")
Logger.debug("GPT: request #{inspect args}")
case OpenAi.post("/v1/completions", args) do
{:ok, %{"choices" => [%{"text" => text, "finish_reason" => finish_reason} | _], "usage" => usage, "id" => gpt_id, "created" => created}} ->
text = String.trim(text)
{o_moderate?, o_moderation} = moderation(text, msg.account.id)
if o_moderate?, do: msg.replyfun.("🚨 offensive output: #{Enum.join(o_moderation, ", ")}")
msg.replyfun.(text)
doc = %{"id" => FlakeId.get(),
"prompt_id" => Map.get(prompt, "_id"),
"prompt_rev" => Map.get(prompt, "_rev"),
"network" => msg.network,
"channel" => msg.channel,
"nick" => msg.sender.nick,
"account_id" => (if msg.account, do: msg.account.id),
"request" => args,
"response" => text,
"message_at" => msg.at,
"reply_at" => DateTime.utc_now(),
"gpt_id" => gpt_id,
"gpt_at" => created,
"gpt_usage" => usage,
"type" => "completions",
"parent_run_id" => Map.get(prompt, "parent_run_id"),
"moderation" => %{"input" => %{flagged: moderate?, categories: moderation},
"output" => %{flagged: o_moderate?, categories: o_moderation}
}
}
Logger.debug("Saving result to couch: #{inspect doc}")
{id, ref, temprefs} = case Couch.post(@couch_run_db, doc) do
{:ok, id, _rev} ->
{ref, temprefs} = put_temp_ref(id, state.temprefs)
{id, ref, temprefs}
error ->
Logger.error("Failed to save to Couch: #{inspect error}")
{nil, nil, state.temprefs}
end
stop = cond do
finish_reason == "stop" -> ""
finish_reason == "length" -> " — truncated"
true -> " — #{finish_reason}"
end
ref_and_prefix = if Map.get(usage, "completion_tokens", 0) == 0 do
"GPT had nothing else to say :( ↪ #{ref || "✗"}"
else
" ↪ #{ref || "✗"}"
end
msg.replyfun.(ref_and_prefix <>
stop <>
" — #{Map.get(usage, "total_tokens", 0)}" <>
" (#{Map.get(usage, "prompt_tokens", 0)}/#{Map.get(usage, "completion_tokens", 0)}) tokens" <>
" — #{id || "save failed"}")
%__MODULE__{state | temprefs: temprefs}
{:error, atom} when is_atom(atom) ->
Logger.error("gpt error: #{inspect atom}")
msg.replyfun.("gpt: ☠️ #{to_string(atom)}")
state
error ->
Logger.error("gpt error: #{inspect error}")
msg.replyfun.("gpt: ☠️ ")
state
end
end
defp moderation(content, user_id) do
case OpenAi.post("/v1/moderations", %{"input" => content, "user" => user_id}) do
{:ok, %{"results" => [%{"flagged" => true, "categories" => categories} | _]}} ->
cat = categories
|> Enum.filter(fn({_key, value}) -> value end)
|> Enum.map(fn({key, _}) -> key end)
{true, cat}
{:ok, moderation} ->
Logger.debug("gpt: moderation: not flagged, #{inspect moderation}")
{false, true}
error ->
Logger.error("gpt: moderation error: #{inspect error}")
{false, false}
end
end
end
diff --git a/lib/lsg_irc/kick_roulette_plugin.ex b/lib/lsg_irc/kick_roulette_plugin.ex
index f810a74..55b7da4 100644
--- a/lib/lsg_irc/kick_roulette_plugin.ex
+++ b/lib/lsg_irc/kick_roulette_plugin.ex
@@ -1,32 +1,32 @@
-defmodule LSG.IRC.KickRoulettePlugin do
+defmodule Nola.IRC.KickRoulettePlugin do
@moduledoc """
# kick roulette
* **!kick**, tentez votre chance…
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:kick", [plugin: __MODULE__])
{:ok, nil}
end
def handle_info({:irc, :trigger, "kick", message = %{trigger: %{type: :bang, args: []}}}, _) do
if 5 == :crypto.rand_uniform(1, 6) do
spawn(fn() ->
:timer.sleep(:crypto.rand_uniform(200, 10_000))
message.replyfun.({:kick, message.sender.nick, "perdu"})
end)
end
{:noreply, nil}
end
def handle_info(msg, _) do
{:noreply, nil}
end
end
diff --git a/lib/lsg_irc/last_fm_plugin.ex b/lib/lsg_irc/last_fm_plugin.ex
index 0c6b8a6..f29982c 100644
--- a/lib/lsg_irc/last_fm_plugin.ex
+++ b/lib/lsg_irc/last_fm_plugin.ex
@@ -1,187 +1,187 @@
-defmodule LSG.IRC.LastFmPlugin do
+defmodule Nola.IRC.LastFmPlugin do
require Logger
@moduledoc """
# last.fm
* **!lastfm|np `[nick|username]`**
* **.lastfm|np**
* **+lastfm, -lastfm `<username last.fm>; ?lastfm`** Configurer un nom d'utilisateur last.fm
"""
@single_trigger ~w(lastfm np)
@pubsub_topics ~w(trigger:lastfm trigger:np)
defstruct dets: nil
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [type: __MODULE__]
for t <- @pubsub_topics, do: {:ok, _} = Registry.register(IRC.PubSub, t, type: __MODULE__)
- dets_filename = (LSG.data_path() <> "/" <> "lastfm.dets") |> String.to_charlist
+ dets_filename = (Nola.data_path() <> "/" <> "lastfm.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [])
{:ok, %__MODULE__{dets: dets}}
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :plus, args: [username]}}}, state) do
username = String.strip(username)
:ok = :dets.insert(state.dets, {message.account.id, username})
message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm configuré: \"#{username}\".")
{:noreply, state}
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :minus, args: []}}}, state) do
text = case :dets.lookup(state.dets, message.account.id) do
[{_nick, _username}] ->
:dets.delete(state.dets, message.account.id)
message.replyfun.("#{message.sender.nick}: nom d'utilisateur last.fm enlevé.")
_ -> nil
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "lastfm", message = %{trigger: %{type: :query, args: []}}}, state) do
text = case :dets.lookup(state.dets, message.account.id) do
[{_nick, username}] ->
message.replyfun.("#{message.sender.nick}: #{username}.")
_ -> nil
end
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: []}}}, state) do
irc_now_playing(message.account.id, message, state)
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :bang, args: [nick_or_user]}}}, state) do
irc_now_playing(nick_or_user, message, state)
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %{trigger: %{type: :dot}}}, state) do
members = IRC.Membership.members(message.network, message.channel)
foldfun = fn({nick, user}, acc) -> [{nick,user}|acc] end
usernames = :dets.foldl(foldfun, [], state.dets)
|> Enum.uniq()
|> Enum.filter(fn({acct,_}) -> Enum.member?(members, acct) end)
|> Enum.map(fn({_, u}) -> u end)
for u <- usernames, do: irc_now_playing(u, message, state)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
def terminate(_reason, state) do
if state.dets do
:dets.sync(state.dets)
:dets.close(state.dets)
end
:ok
end
defp irc_now_playing(nick_or_user, message, state) do
nick_or_user = String.strip(nick_or_user)
id_or_user = if account = IRC.Account.get(nick_or_user) || IRC.Account.find_always_by_nick(message.network, message.channel, nick_or_user) do
account.id
else
nick_or_user
end
username = case :dets.lookup(state.dets, id_or_user) do
[{_, username}] -> username
_ -> id_or_user
end
case now_playing(username) do
{:error, text} when is_binary(text) ->
message.replyfun.(text)
{:ok, map} when is_map(map) ->
track = fetch_track(username, map)
text = format_now_playing(map, track)
user = if account = IRC.Account.get(id_or_user) do
user = IRC.UserTrack.find_by_account(message.network, account)
if(user, do: user.nick, else: account.name)
else
username
end
if user && text do
message.replyfun.("#{user} #{text}")
else
message.replyfun.("#{username}: pas de résultat")
end
other ->
message.replyfun.("erreur :(")
end
end
defp now_playing(user) do
api = Application.get_env(:lsg, :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(:lsg, :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/lsg_irc/link_plugin.ex b/lib/lsg_irc/link_plugin.ex
index aaf6c6f..28e537a 100644
--- a/lib/lsg_irc/link_plugin.ex
+++ b/lib/lsg_irc/link_plugin.ex
@@ -1,271 +1,271 @@
-defmodule LSG.IRC.LinkPlugin do
+defmodule Nola.IRC.LinkPlugin do
@moduledoc """
# Link Previewer
An extensible link previewer for IRC.
To extend the supported sites, create a new handler implementing the callbacks.
See `link_plugin/` directory for examples. The first in list handler that returns true to the `match/2` callback will be used,
and if the handler returns `:error` or crashes, will fallback to the default preview.
Unsupported websites will use the default link preview method, which is for html document the title, otherwise it'll use
the mimetype and size.
## Configuration:
```
- config :lsg, LSG.IRC.LinkPlugin,
+ config :lsg, Nola.IRC.LinkPlugin,
handlers: [
- LSG.IRC.LinkPlugin.Youtube: [
+ Nola.IRC.LinkPlugin.Youtube: [
invidious: true
],
- LSG.IRC.LinkPlugin.Twitter: [],
- LSG.IRC.LinkPlugin.Imgur: [],
+ Nola.IRC.LinkPlugin.Twitter: [],
+ Nola.IRC.LinkPlugin.Imgur: [],
]
```
"""
@ircdoc """
# Link preview
Previews links (just post a link!).
Announces real URL after redirections and provides extended support for YouTube, Twitter and Imgur.
"""
def short_irc_doc, do: false
def irc_doc, do: @ircdoc
require Logger
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@callback match(uri :: URI.t, options :: Keyword.t) :: {true, params :: Map.t} | false
@callback expand(uri :: URI.t, params :: Map.t, options :: Keyword.t) :: {:ok, lines :: [] | String.t} | :error
@callback post_match(uri :: URI.t, content_type :: binary, headers :: [], opts :: Keyword.t) :: {:body | :file, params :: Map.t} | false
@callback post_expand(uri :: URI.t, body :: binary() | Path.t, params :: Map.t, options :: Keyword.t) :: {:ok, lines :: [] | String.t} | :error
@optional_callbacks [expand: 3, post_expand: 4]
defstruct [:client]
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "messages", [plugin: __MODULE__])
#{:ok, _} = Registry.register(IRC.PubSub, "messages:telegram", [plugin: __MODULE__])
Logger.info("Link handler started")
{:ok, %__MODULE__{}}
end
def handle_info({:irc, :text, message = %{text: text}}, state) do
String.split(text)
|> Enum.map(fn(word) ->
if String.starts_with?(word, "http://") || String.starts_with?(word, "https://") do
uri = URI.parse(word)
if uri.scheme && uri.host do
spawn(fn() ->
:timer.kill_after(:timer.seconds(30))
case expand_link([uri]) do
{:ok, uris, text} ->
text = case uris do
[uri] -> text
[luri | _] ->
if luri.host == uri.host && luri.path == luri.path do
text
else
["-> #{URI.to_string(luri)}", text]
end
end
if is_list(text) do
for line <- text, do: message.replyfun.(line)
else
message.replyfun.(text)
end
_ -> nil
end
end)
end
end
end)
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
def terminate(_reason, state) do
:ok
end
# 1. Match the first valid handler
# 2. Try to run the handler
# 3. If :error or crash, default link.
# If :skip, nothing
# 4. ?
# Over five redirections: cancel.
def expand_link(acc = [_, _, _, _, _ | _]) do
{:ok, acc, "link redirects more than five times"}
end
def expand_link(acc=[uri | _]) do
Logger.debug("link: expanding: #{inspect uri}")
handlers = Keyword.get(Application.get_env(:lsg, __MODULE__, [handlers: []]), :handlers)
handler = Enum.reduce_while(handlers, nil, fn({module, opts}, acc) ->
Logger.debug("link: attempt expanding: #{inspect module} for #{inspect uri}")
module = Module.concat([module])
case module.match(uri, opts) do
{true, params} -> {:halt, {module, params, opts}}
false -> {:cont, acc}
end
end)
run_expand(acc, handler)
end
def run_expand(acc, nil) do
expand_default(acc)
end
def run_expand(acc=[uri|_], {module, params, opts}) do
Logger.debug("link: expanding #{inspect uri} with #{inspect module}")
case module.expand(uri, params, opts) do
{:ok, data} -> {:ok, acc, data}
:error -> expand_default(acc)
:skip -> nil
end
rescue
e ->
Logger.error("link: rescued #{inspect uri} with #{inspect module}: #{inspect e}")
Logger.error(Exception.format(:error, e, __STACKTRACE__))
expand_default(acc)
catch
e, b ->
Logger.error("link: catched #{inspect uri} with #{inspect module}: #{inspect {e, b}}")
expand_default(acc)
end
defp get(url, headers \\ [], options \\ []) do
get_req(url, :hackney.get(url, headers, <<>>, options))
end
defp get_req(_, {:error, reason}) do
{:error, reason}
end
defp get_req(url, {:ok, 200, headers, client}) do
headers = Enum.reduce(headers, %{}, fn({key, value}, acc) ->
Map.put(acc, String.downcase(key), value)
end)
content_type = Map.get(headers, "content-type", "application/octect-stream")
length = Map.get(headers, "content-length", "0")
{length, _} = Integer.parse(length)
handlers = Keyword.get(Application.get_env(:lsg, __MODULE__, [handlers: []]), :handlers)
handler = Enum.reduce_while(handlers, false, fn({module, opts}, acc) ->
module = Module.concat([module])
try do
case module.post_match(url, content_type, headers, opts) do
{mode, params} when mode in [:body, :file] -> {:halt, {module, params, opts, mode}}
false -> {:cont, acc}
end
rescue
e ->
Logger.error(inspect(e))
{:cont, false}
catch
e, b ->
Logger.error(inspect({b}))
{:cont, false}
end
end)
cond do
handler != false and length <= 30_000_000 ->
case get_body(url, 30_000_000, client, handler, <<>>) do
{:ok, _} = ok -> ok
:error ->
{:ok, "file: #{content_type}, size: #{human_size(length)}"}
end
#String.starts_with?(content_type, "text/html") && length <= 30_000_000 ->
# get_body(url, 30_000_000, client, <<>>)
true ->
:hackney.close(client)
{:ok, "file: #{content_type}, size: #{human_size(length)}"}
end
end
defp get_req(_, {:ok, redirect, headers, client}) when redirect in 300..399 do
headers = Enum.reduce(headers, %{}, fn({key, value}, acc) ->
Map.put(acc, String.downcase(key), value)
end)
location = Map.get(headers, "location")
:hackney.close(client)
{:redirect, location}
end
defp get_req(_, {:ok, status, headers, client}) do
:hackney.close(client)
{:error, status, headers}
end
defp get_body(url, len, client, {handler, params, opts, mode} = h, acc) when len >= byte_size(acc) do
case :hackney.stream_body(client) do
{:ok, data} ->
get_body(url, len, client, h, << acc::binary, data::binary >>)
:done ->
body = case mode do
:body -> acc
:file ->
{:ok, tmpfile} = Plug.Upload.random_file("linkplugin")
File.write!(tmpfile, acc)
tmpfile
end
handler.post_expand(url, body, params, opts)
{:error, reason} ->
{:ok, "failed to fetch body: #{inspect reason}"}
end
end
defp get_body(_, len, client, h, _acc) do
:hackney.close(client)
IO.inspect(h)
{:ok, "Error: file over 30"}
end
def expand_default(acc = [uri = %URI{scheme: scheme} | _]) when scheme in ["http", "https"] do
Logger.debug("link: expanding #{uri} with default")
headers = [{"user-agent", "DmzBot (like TwitterBot)"}]
options = [follow_redirect: false, max_body_length: 30_000_000]
case get(URI.to_string(uri), headers, options) do
{:ok, text} ->
{:ok, acc, text}
{:redirect, link} ->
new_uri = URI.parse(link)
#new_uri = %URI{new_uri | scheme: scheme, authority: uri.authority, host: uri.host, port: uri.port}
expand_link([new_uri | acc])
{:error, status, _headers} ->
text = Plug.Conn.Status.reason_phrase(status)
{:ok, acc, "Error: HTTP #{text} (#{status})"}
{:error, {:tls_alert, {:handshake_failure, err}}} ->
{:ok, acc, "TLS Error: #{to_string(err)}"}
{:error, reason} ->
{:ok, acc, "Error: #{to_string(reason)}"}
end
end
# Unsupported scheme, came from a redirect.
def expand_default(acc = [uri | _]) do
{:ok, [uri], "-> #{URI.to_string(uri)}"}
end
defp human_size(bytes) do
bytes
|> FileSize.new(:b)
|> FileSize.scale()
|> FileSize.format()
end
end
diff --git a/lib/lsg_irc/link_plugin/github.ex b/lib/lsg_irc/link_plugin/github.ex
index 19be89b..93e0892 100644
--- a/lib/lsg_irc/link_plugin/github.ex
+++ b/lib/lsg_irc/link_plugin/github.ex
@@ -1,49 +1,49 @@
-defmodule LSG.IRC.LinkPlugin.Github do
- @behaviour LSG.IRC.LinkPlugin
+defmodule Nola.IRC.LinkPlugin.Github do
+ @behaviour Nola.IRC.LinkPlugin
@impl true
def match(uri = %URI{host: "github.com", path: path}, _) do
case String.split(path, "/") do
["", user, repo] ->
{true, %{user: user, repo: repo, path: "#{user}/#{repo}"}}
_ ->
false
end
end
def match(_, _), do: false
@impl true
def post_match(_, _, _, _), do: false
@impl true
def expand(_uri, %{user: user, repo: repo}, _opts) do
case HTTPoison.get("https://api.github.com/repos/#{user}/#{repo}") do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, json} = Jason.decode(body)
src = json["source"]["full_name"]
disabled = if(json["disabled"], do: " (disabled)", else: "")
archived = if(json["archived"], do: " (archived)", else: "")
fork = if src && src != json["full_name"] do
" (⑂ #{json["source"]["full_name"]})"
else
""
end
start = "#{json["full_name"]}#{disabled}#{archived}#{fork} - #{json["description"]}"
tags = for(t <- json["topics"]||[], do: "##{t}") |> Enum.intersperse(", ") |> Enum.join("")
lang = if(json["language"], do: "#{json["language"]} - ", else: "")
issues = if(json["open_issues_count"], do: "#{json["open_issues_count"]} issues - ", else: "")
last_push = if at = json["pushed_at"] do
{:ok, date, _} = DateTime.from_iso8601(at)
" - last pushed #{DateTime.to_string(date)}"
else
""
end
network = "#{lang}#{issues}#{json["stargazers_count"]} stars - #{json["subscribers_count"]} watchers - #{json["forks_count"]} forks#{last_push}"
{:ok, [start, tags, network]}
other ->
:error
end
end
end
diff --git a/lib/lsg_irc/link_plugin/html.ex b/lib/lsg_irc/link_plugin/html.ex
index e0e4229..56a8ceb 100644
--- a/lib/lsg_irc/link_plugin/html.ex
+++ b/lib/lsg_irc/link_plugin/html.ex
@@ -1,106 +1,106 @@
-defmodule LSG.IRC.LinkPlugin.HTML do
- @behaviour LSG.IRC.LinkPlugin
+defmodule Nola.IRC.LinkPlugin.HTML do
+ @behaviour Nola.IRC.LinkPlugin
@impl true
def match(_, _), do: false
@impl true
def post_match(_url, "text/html"<>_, _header, _opts) do
{:body, nil}
end
def post_match(_, _, _, _), do: false
@impl true
def post_expand(url, body, _params, _opts) do
html = Floki.parse(body)
title = collect_title(html)
opengraph = collect_open_graph(html)
itemprops = collect_itemprops(html)
text = if Map.has_key?(opengraph, "title") && Map.has_key?(opengraph, "description") do
sitename = if sn = Map.get(opengraph, "site_name") do
"#{sn}"
else
""
end
paywall? = if Map.get(opengraph, "article:content_tier", Map.get(itemprops, "article:content_tier", "free")) == "free" do
""
else
"[paywall] "
end
section = if section = Map.get(opengraph, "article:section", Map.get(itemprops, "article:section", nil)) do
": #{section}"
else
""
end
date = case DateTime.from_iso8601(Map.get(opengraph, "article:published_time", Map.get(itemprops, "article:published_time", ""))) do
{:ok, date, _} ->
"#{Timex.format!(date, "%d/%m/%y", :strftime)}. "
_ ->
""
end
uri = URI.parse(url)
prefix = "#{paywall?}#{Map.get(opengraph, "site_name", uri.host)}#{section}"
prefix = unless prefix == "" do
"#{prefix} — "
else
""
end
[clean_text("#{prefix}#{Map.get(opengraph, "title")}")] ++ IRC.splitlong(clean_text("#{date}#{Map.get(opengraph, "description")}"))
else
clean_text(title)
end
{:ok, text}
end
defp collect_title(html) do
case Floki.find(html, "title") do
[{"title", [], [title]} | _] ->
String.trim(title)
_ ->
nil
end
end
defp collect_open_graph(html) do
Enum.reduce(Floki.find(html, "head meta"), %{}, fn(tag, acc) ->
case tag do
{"meta", values, []} ->
name = List.keyfind(values, "property", 0, {nil, nil}) |> elem(1)
content = List.keyfind(values, "content", 0, {nil, nil}) |> elem(1)
case name do
"og:" <> key ->
Map.put(acc, key, content)
"article:"<>_ ->
Map.put(acc, name, content)
_other -> acc
end
_other -> acc
end
end)
end
defp collect_itemprops(html) do
Enum.reduce(Floki.find(html, "[itemprop]"), %{}, fn(tag, acc) ->
case tag do
{"meta", values, []} ->
name = List.keyfind(values, "itemprop", 0, {nil, nil}) |> elem(1)
content = List.keyfind(values, "content", 0, {nil, nil}) |> elem(1)
case name do
"article:" <> key ->
Map.put(acc, name, content)
_other -> acc
end
_other -> acc
end
end)
end
defp clean_text(text) do
text
|> String.replace("\n", " ")
|> HtmlEntities.decode()
end
end
diff --git a/lib/lsg_irc/link_plugin/imgur.ex b/lib/lsg_irc/link_plugin/imgur.ex
index 443afdb..af0b36a 100644
--- a/lib/lsg_irc/link_plugin/imgur.ex
+++ b/lib/lsg_irc/link_plugin/imgur.ex
@@ -1,96 +1,96 @@
-defmodule LSG.IRC.LinkPlugin.Imgur do
- @behaviour LSG.IRC.LinkPlugin
+defmodule Nola.IRC.LinkPlugin.Imgur do
+ @behaviour Nola.IRC.LinkPlugin
@moduledoc """
# Imgur link preview
No options.
Needs to have a Imgur API key configured:
```
config :lsg, :imgur,
client_id: "xxxxxxxx",
client_secret: "xxxxxxxxxxxxxxxxxxxx"
```
"""
@impl true
def match(uri = %URI{host: "imgur.io"}, arg) do
match(%URI{uri | host: "imgur.com"}, arg)
end
def match(uri = %URI{host: "i.imgur.io"}, arg) do
match(%URI{uri | host: "i.imgur.com"}, arg)
end
def match(uri = %URI{host: "imgur.com", path: "/a/"<>album_id}, _) do
{true, %{album_id: album_id}}
end
def match(uri = %URI{host: "imgur.com", path: "/gallery/"<>album_id}, _) do
{true, %{album_id: album_id}}
end
def match(uri = %URI{host: "i.imgur.com", path: "/"<>image}, _) do
[hash, _] = String.split(image, ".", parts: 2)
{true, %{image_id: hash}}
end
def match(_, _), do: false
@impl true
def post_match(_, _, _, _), do: false
def expand(_uri, %{album_id: album_id}, opts) do
expand_imgur_album(album_id, opts)
end
def expand(_uri, %{image_id: image_id}, opts) do
expand_imgur_image(image_id, opts)
end
def expand_imgur_image(image_id, opts) do
client_id = Keyword.get(Application.get_env(:lsg, :imgur, []), :client_id, "42")
headers = [{"Authorization", "Client-ID #{client_id}"}]
options = []
case HTTPoison.get("https://api.imgur.com/3/image/#{image_id}", headers, options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, json} = Jason.decode(body)
data = json["data"]
title = String.slice(data["title"] || data["description"], 0, 180)
nsfw = if data["nsfw"], do: "(NSFW) - ", else: " "
height = Map.get(data, "height")
width = Map.get(data, "width")
size = Map.get(data, "size")
{:ok, "image, #{width}x#{height}, #{size} bytes #{nsfw}#{title}"}
other ->
:error
end
end
def expand_imgur_album(album_id, opts) do
client_id = Keyword.get(Application.get_env(:lsg, :imgur, []), :client_id, "42")
headers = [{"Authorization", "Client-ID #{client_id}"}]
options = []
case HTTPoison.get("https://api.imgur.com/3/album/#{album_id}", headers, options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, json} = Jason.decode(body)
data = json["data"]
title = data["title"]
nsfw = data["nsfw"]
nsfw = if nsfw, do: "(NSFW) - ", else: ""
if data["images_count"] == 1 do
[image] = data["images"]
title = if title || data["title"] do
title = [title, data["title"]] |> Enum.filter(fn(x) -> x end) |> Enum.uniq() |> Enum.join(" — ")
"#{title} — "
else
""
end
{:ok, "#{nsfw}#{title}#{image["link"]}"}
else
title = if title, do: title, else: "Untitled album"
{:ok, "#{nsfw}#{title} - #{data["images_count"]} images"}
end
other ->
:error
end
end
end
diff --git a/lib/lsg_irc/link_plugin/pdf.ex b/lib/lsg_irc/link_plugin/pdf.ex
index 8c4869c..5f72ef5 100644
--- a/lib/lsg_irc/link_plugin/pdf.ex
+++ b/lib/lsg_irc/link_plugin/pdf.ex
@@ -1,39 +1,39 @@
-defmodule LSG.IRC.LinkPlugin.PDF do
+defmodule Nola.IRC.LinkPlugin.PDF do
require Logger
- @behaviour LSG.IRC.LinkPlugin
+ @behaviour Nola.IRC.LinkPlugin
@impl true
def match(_, _), do: false
@impl true
def post_match(_url, "application/pdf"<>_, _header, _opts) do
{:file, nil}
end
def post_match(_, _, _, _), do: false
@impl true
def post_expand(url, file, _, _) do
case System.cmd("pdftitle", ["-p", file]) do
{text, 0} ->
text = text
|> String.trim()
if text == "" do
:error
else
basename = Path.basename(url, ".pdf")
text = "[#{basename}] " <> text
|> String.split("\n")
{:ok, text}
end
{_, 127} ->
Logger.error("dependency `pdftitle` is missing, please install it: `pip3 install pdftitle`.")
:error
{error, code} ->
Logger.warn("command `pdftitle` exited with status code #{code}:\n#{inspect error}")
:error
end
end
end
diff --git a/lib/lsg_irc/link_plugin/redacted.ex b/lib/lsg_irc/link_plugin/redacted.ex
index 2e92b4a..7a6229d 100644
--- a/lib/lsg_irc/link_plugin/redacted.ex
+++ b/lib/lsg_irc/link_plugin/redacted.ex
@@ -1,18 +1,18 @@
-defmodule LSG.IRC.LinkPlugin.Redacted do
- @behaviour LSG.IRC.LinkPlugin
+defmodule Nola.IRC.LinkPlugin.Redacted do
+ @behaviour Nola.IRC.LinkPlugin
@impl true
def match(uri = %URI{host: "redacted.ch", path: "/torrent.php", query: query = "id="<>id}, _opts) do
%{"id" => id} = URI.decode_query(id)
{true, %{torrent: id}}
end
def match(_, _), do: false
@impl true
def post_match(_, _, _, _), do: false
def expand(_uri, %{torrent: id}, _opts) do
end
end
diff --git a/lib/lsg_irc/link_plugin/reddit.ex b/lib/lsg_irc/link_plugin/reddit.ex
index 6fc1723..79102e0 100644
--- a/lib/lsg_irc/link_plugin/reddit.ex
+++ b/lib/lsg_irc/link_plugin/reddit.ex
@@ -1,119 +1,119 @@
-defmodule LSG.IRC.LinkPlugin.Reddit do
- @behaviour LSG.IRC.LinkPlugin
+defmodule Nola.IRC.LinkPlugin.Reddit do
+ @behaviour Nola.IRC.LinkPlugin
@impl true
def match(uri = %URI{host: "reddit.com", path: path}, _) do
case String.split(path, "/") do
["", "r", sub, "comments", post_id, _slug] ->
{true, %{mode: :post, path: path, sub: sub, post_id: post_id}}
["", "r", sub, "comments", post_id, _slug, ""] ->
{true, %{mode: :post, path: path, sub: sub, post_id: post_id}}
["", "r", sub, ""] ->
{true, %{mode: :sub, path: path, sub: sub}}
["", "r", sub] ->
{true, %{mode: :sub, path: path, sub: sub}}
# ["", "u", user] ->
# {true, %{mode: :user, path: path, user: user}}
_ ->
false
end
end
def match(uri = %URI{host: host, path: path}, opts) do
if String.ends_with?(host, ".reddit.com") do
match(%URI{uri | host: "reddit.com"}, opts)
else
false
end
end
@impl true
def post_match(_, _, _, _), do: false
@impl true
def expand(_, %{mode: :sub, sub: sub}, _opts) do
url = "https://api.reddit.com/r/#{sub}/about"
case HTTPoison.get(url) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
sr = Jason.decode!(body)
|> Map.get("data")
|> IO.inspect(limit: :infinity)
description = Map.get(sr, "public_description")||Map.get(sr, "description", "")
|> String.split("\n")
|> List.first()
name = if title = Map.get(sr, "title") do
Map.get(sr, "display_name_prefixed") <> ": " <> title
else
Map.get(sr, "display_name_prefixed")
end
nsfw = if Map.get(sr, "over18") do
"[NSFW] "
else
""
end
quarantine = if Map.get(sr, "quarantine") do
"[Quarantined] "
else
""
end
count = "#{Map.get(sr, "subscribers")} subscribers, #{Map.get(sr, "active_user_count")} active"
preview = "#{quarantine}#{nsfw}#{name} — #{description} (#{count})"
{:ok, preview}
_ ->
:error
end
end
def expand(_uri, %{mode: :post, path: path, sub: sub, post_id: post_id}, _opts) do
case HTTPoison.get("https://api.reddit.com#{path}?sr_detail=true") do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
json = Jason.decode!(body)
op = List.first(json)
|> Map.get("data")
|> Map.get("children")
|> List.first()
|> Map.get("data")
|> IO.inspect(limit: :infinity)
sr = get_in(op, ["sr_detail", "display_name_prefixed"])
{self?, url} = if Map.get(op, "selftext") == "" do
{false, Map.get(op, "url")}
else
{true, nil}
end
self_str = if(self?, do: "text", else: url)
up = Map.get(op, "ups")
down = Map.get(op, "downs")
comments = Map.get(op, "num_comments")
nsfw = if Map.get(op, "over_18") do
"[NSFW] "
else
""
end
state = cond do
Map.get(op, "hidden") -> "hidden"
Map.get(op, "archived") -> "archived"
Map.get(op, "locked") -> "locked"
Map.get(op, "quarantine") -> "quarantined"
Map.get(op, "removed_by") || Map.get(op, "removed_by_category") -> "removed"
Map.get(op, "banned_by") -> "banned"
Map.get(op, "pinned") -> "pinned"
Map.get(op, "stickied") -> "stickied"
true -> nil
end
flair = if flair = Map.get(op, "link_flair_text") do
"[#{flair}] "
else
""
end
title = "#{nsfw}#{sr}: #{flair}#{Map.get(op, "title")}"
state_str = if(state, do: "#{state}, ")
content = "by u/#{Map.get(op, "author")} - #{state_str}#{up} up, #{down} down, #{comments} comments - #{self_str}"
{:ok, [title, content]}
err ->
:error
end
end
end
diff --git a/lib/lsg_irc/link_plugin/twitter.ex b/lib/lsg_irc/link_plugin/twitter.ex
index 41e10ab..640b193 100644
--- a/lib/lsg_irc/link_plugin/twitter.ex
+++ b/lib/lsg_irc/link_plugin/twitter.ex
@@ -1,158 +1,158 @@
-defmodule LSG.IRC.LinkPlugin.Twitter do
- @behaviour LSG.IRC.LinkPlugin
+defmodule Nola.IRC.LinkPlugin.Twitter do
+ @behaviour Nola.IRC.LinkPlugin
@moduledoc """
# Twitter Link Preview
Configuration:
needs an API key and auth tokens:
```
config :extwitter, :oauth, [
consumer_key: "zzzzz",
consumer_secret: "xxxxxxx",
access_token: "yyyyyy",
access_token_secret: "ssshhhhhh"
]
```
options:
* `expand_quoted`: Add the quoted tweet instead of its URL. Default: true.
"""
def match(uri = %URI{host: twitter, path: path}, _opts) when twitter in ["twitter.com", "m.twitter.com", "mobile.twitter.com"] do
case String.split(path, "/", parts: 4) do
["", _username, "status", status_id] ->
{status_id, _} = Integer.parse(status_id)
{true, %{status_id: status_id}}
_ -> false
end
end
def match(_, _), do: false
@impl true
def post_match(_, _, _, _), do: false
def expand(_uri, %{status_id: status_id}, opts) do
expand_tweet(ExTwitter.show(status_id, tweet_mode: "extended"), opts)
end
defp expand_tweet(nil, _opts) do
:error
end
defp link_tweet(tweet_or_screen_id_tuple, opts, force_twitter_com \\ false)
defp link_tweet({screen_name, id}, opts, force_twitter_com) do
path = "/#{screen_name}/status/#{id}"
nitter = Keyword.get(opts, :nitter)
host = if !force_twitter_com && nitter, do: nitter, else: "twitter.com"
"https://#{host}/#{screen_name}/status/#{id}"
end
defp link_tweet(tweet, opts, force_twitter_com) do
link_tweet({tweet.user.screen_name, tweet.id}, opts, force_twitter_com)
end
defp expand_tweet(tweet, opts) do
head = format_tweet_header(tweet, opts)
# Format tweet text
text = expand_twitter_text(tweet, opts)
text = if tweet.quoted_status do
quote_url = link_tweet(tweet.quoted_status, opts, true)
String.replace(text, quote_url, "")
else
text
end
text = IRC.splitlong(text)
reply_to = if tweet.in_reply_to_status_id do
reply_url = link_tweet({tweet.in_reply_to_screen_name, tweet.in_reply_to_status_id}, opts)
text = if tweet.in_reply_to_screen_name == tweet.user.screen_name, do: "continued from", else: "replying to"
<<3, 15, " ↪ ", text::binary, " ", reply_url::binary, 3>>
end
quoted = if tweet.quoted_status do
full_text = tweet.quoted_status
|> expand_twitter_text(opts)
|> IRC.splitlong_with_prefix(">")
head = format_tweet_header(tweet.quoted_status, opts, details: false, prefix: "↓ quoting")
[head | full_text]
else
[]
end
#<<2, "#{tweet.user.name} (@#{tweet.user.screen_name})", 2, " ", 3, 61, "#{foot} #{nitter_link}", 3>>, reply_to] ++ text ++ quoted
text = [head, reply_to | text] ++ quoted
|> Enum.filter(& &1)
{:ok, text}
end
defp expand_twitter_text(tweet, _opts) do
text = Enum.reduce(tweet.entities.urls, tweet.full_text, fn(entity, text) ->
String.replace(text, entity.url, entity.expanded_url)
end)
extended = tweet.extended_entities || %{media: []}
text = Enum.reduce(extended.media, text, fn(entity, text) ->
url = Enum.filter(extended.media, fn(e) -> entity.url == e.url end)
|> Enum.map(fn(e) ->
cond do
e.type == "video" -> e.expanded_url
true -> e.media_url_https
end
end)
|> Enum.join(" ")
String.replace(text, entity.url, url)
end)
|> HtmlEntities.decode()
end
defp format_tweet_header(tweet, opts, format_opts \\ []) do
prefix = Keyword.get(format_opts, :prefix, nil)
details = Keyword.get(format_opts, :details, true)
padded_prefix = if prefix, do: "#{prefix} ", else: ""
author = <<padded_prefix::binary, 2, "#{tweet.user.name} (@#{tweet.user.screen_name})", 2>>
link = link_tweet(tweet, opts)
{:ok, at} = Timex.parse(tweet.created_at, "%a %b %e %H:%M:%S %z %Y", :strftime)
{:ok, formatted_time} = Timex.format(at, "{relative}", :relative)
nsfw = if tweet.possibly_sensitive, do: <<3, 52, "NSFW", 3>>
rts = if tweet.retweet_count && tweet.retweet_count > 0, do: "#{tweet.retweet_count} RT"
likes = if tweet.favorite_count && tweet.favorite_count > 0, do: "#{tweet.favorite_count} ❤︎"
qrts = if tweet.quote_count && tweet.quote_count > 0, do: "#{tweet.quote_count} QRT"
replies = if tweet.reply_count && tweet.reply_count > 0, do: "#{tweet.reply_count} Reps"
dmcad = if tweet.withheld_copyright, do: <<3, 52, "DMCA", 3>>
withheld_local = if tweet.withheld_in_countries && length(tweet.withheld_in_countries) > 0 do
"Withheld in #{length(tweet.withheld_in_countries)} countries"
end
verified = if tweet.user.verified, do: <<3, 51, "✔", 3>>
meta = if details do
[verified, nsfw, formatted_time, dmcad, withheld_local, rts, qrts, likes, replies]
else
[verified, nsfw, formatted_time, dmcad, withheld_local]
end
meta = meta
|> Enum.filter(& &1)
|> Enum.join(" - ")
meta = <<3, 15, meta::binary, " → #{link}", 3>>
<<author::binary, " — ", meta::binary>>
end
end
diff --git a/lib/lsg_irc/link_plugin/youtube.ex b/lib/lsg_irc/link_plugin/youtube.ex
index f38eca3..1e3a0de 100644
--- a/lib/lsg_irc/link_plugin/youtube.ex
+++ b/lib/lsg_irc/link_plugin/youtube.ex
@@ -1,72 +1,72 @@
-defmodule LSG.IRC.LinkPlugin.YouTube do
- @behaviour LSG.IRC.LinkPlugin
+defmodule Nola.IRC.LinkPlugin.YouTube do
+ @behaviour Nola.IRC.LinkPlugin
@moduledoc """
# YouTube link preview
needs an API key:
```
config :lsg, :youtube,
api_key: "xxxxxxxxxxxxx"
```
options:
* `invidious`: Add a link to invidious.
"""
@impl true
def match(uri = %URI{host: yt, path: "/watch", query: "v="<>video_id}, _opts) when yt in ["youtube.com", "www.youtube.com"] do
{true, %{video_id: video_id}}
end
def match(%URI{host: "youtu.be", path: "/"<>video_id}, _opts) do
{true, %{video_id: video_id}}
end
def match(_, _), do: false
@impl true
def post_match(_, _, _, _), do: false
@impl true
def expand(uri, %{video_id: video_id}, opts) do
key = Application.get_env(:lsg, :youtube)[:api_key]
params = %{
"part" => "snippet,contentDetails,statistics",
"id" => video_id,
"key" => key
}
headers = []
options = [params: params]
case HTTPoison.get("https://www.googleapis.com/youtube/v3/videos", [], options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
case Jason.decode(body) do
{:ok, json} ->
item = List.first(json["items"])
if item do
snippet = item["snippet"]
duration = item["contentDetails"]["duration"] |> String.replace("PT", "") |> String.downcase
date = snippet["publishedAt"]
|> DateTime.from_iso8601()
|> elem(1)
|> Timex.format("{relative}", :relative)
|> elem(1)
line = if host = Keyword.get(opts, :invidious) do
["-> https://#{host}/watch?v=#{video_id}"]
else
[]
end
{:ok, line ++ ["#{snippet["title"]}", "— #{duration} — uploaded by #{snippet["channelTitle"]} — #{date}"
<> " — #{item["statistics"]["viewCount"]} views, #{item["statistics"]["likeCount"]} likes"]}
else
:error
end
_ -> :error
end
end
end
end
diff --git a/lib/lsg_irc/logger_plugin.ex b/lib/lsg_irc/logger_plugin.ex
index de601a6..b13f33a 100644
--- a/lib/lsg_irc/logger_plugin.ex
+++ b/lib/lsg_irc/logger_plugin.ex
@@ -1,70 +1,70 @@
-defmodule LSG.IRC.LoggerPlugin do
+defmodule Nola.IRC.LoggerPlugin do
require Logger
@couch_db "bot-logs"
def irc_doc(), do: nil
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "triggers", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "irc:outputs", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages:private", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, _, m}, state) do
{:noreply, log(m, state)}
end
def handle_info({:irc, :text, m}, state) do
{:noreply, log(m, state)}
end
def handle_info(info, state) do
Logger.debug("logger_plugin: unhandled info: #{info}")
{:noreply, state}
end
def log(entry, state) do
case Couch.post(@couch_db, format_to_db(entry)) do
{:ok, id, _rev} ->
Logger.debug("logger_plugin: saved: #{inspect id}")
state
error ->
Logger.error("logger_plugin: save failed: #{inspect error}")
end
rescue
e ->
Logger.error("logger_plugin: rescued processing for #{inspect entry}: #{inspect e}")
Logger.error(Exception.format(:error, e, __STACKTRACE__))
state
catch
e, b ->
Logger.error("logger_plugin: catched processing for #{inspect entry}: #{inspect e}")
Logger.error(Exception.format(e, b, __STACKTRACE__))
state
end
def format_to_db(msg = %IRC.Message{id: id}) do
msg
|> Poison.encode!()
|> Map.drop("id")
%{"_id" => id || FlakeId.get(),
"type" => "irc.message/v1",
"object" => msg}
end
def format_to_db(anything) do
%{"_id" => FlakeId.get(),
"type" => "object",
"object" => anything}
end
end
diff --git a/lib/lsg_irc/lsg_irc.ex b/lib/lsg_irc/lsg_irc.ex
index a50abed..f64978a 100644
--- a/lib/lsg_irc/lsg_irc.ex
+++ b/lib/lsg_irc/lsg_irc.ex
@@ -1,34 +1,34 @@
-defmodule LSG.IRC do
+defmodule Nola.IRC do
require Logger
- def env(), do: LSG.env(:irc)
+ def env(), do: Nola.env(:irc)
def env(key, default \\ nil), do: Keyword.get(env(), key, default)
def application_childs do
import Supervisor.Spec
IRC.Connection.setup()
IRC.Plugin.setup()
[
worker(Registry, [[keys: :duplicate, name: IRC.ConnectionPubSub]], id: :registr_irc_conn),
worker(Registry, [[keys: :duplicate, name: IRC.PubSub]], id: :registry_irc),
worker(IRC.Membership, []),
worker(IRC.Account, []),
worker(IRC.UserTrack.Storage, []),
worker(IRC.Account.AccountPlugin, []),
supervisor(IRC.Plugin.Supervisor, [], [name: IRC.Plugin.Supervisor]),
supervisor(IRC.Connection.Supervisor, [], [name: IRC.Connection.Supervisor]),
supervisor(IRC.PuppetConnection.Supervisor, [], [name: IRC.PuppetConnection.Supervisor]),
]
end
# Start plugins first to let them get on connection events.
def after_start() do
Logger.info("Starting plugins")
IRC.Plugin.start_all()
Logger.info("Starting connections")
IRC.Connection.start_all()
end
end
diff --git a/lib/lsg_irc/outline_plugin.ex b/lib/lsg_irc/outline_plugin.ex
index 47fa6fa..820500e 100644
--- a/lib/lsg_irc/outline_plugin.ex
+++ b/lib/lsg_irc/outline_plugin.ex
@@ -1,108 +1,108 @@
-defmodule LSG.IRC.OutlinePlugin do
+defmodule Nola.IRC.OutlinePlugin do
@moduledoc """
# outline auto-link
Envoie un lien vers Outline quand un lien est envoyé.
* **!outline `<url>`** crée un lien outline pour `<url>`.
* **+outline `<host>`** active outline pour `<host>`.
* **-outline `<host>`** désactive outline pour `<host>`.
"""
def short_irc_doc, do: false
def irc_doc, do: @moduledoc
require Logger
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
defstruct [:file, :hosts]
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:outline", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages", regopts)
- file = Path.join(LSG.data_path, "/outline.txt")
+ file = Path.join(Nola.data_path, "/outline.txt")
hosts = case File.read(file) do
{:error, :enoent} ->
[]
{:ok, lines} ->
String.split(lines, "\n", trim: true)
end
{:ok, %__MODULE__{file: file, hosts: hosts}}
end
def handle_info({:irc, :trigger, "outline", message = %IRC.Message{trigger: %IRC.Trigger{type: :plus, args: [host]}}}, state) do
state = %{state | hosts: [host | state.hosts]}
save(state)
message.replyfun.("ok")
{:noreply, state}
end
def handle_info({:irc, :trigger, "outline", message = %IRC.Message{trigger: %IRC.Trigger{type: :minus, args: [host]}}}, state) do
state = %{state | hosts: List.delete(state.hosts, host)}
save(state)
message.replyfun.("ok")
{:noreply, state}
end
def handle_info({:irc, :trigger, "outline", message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [url]}}}, state) do
line = "-> #{outline(url)}"
message.replyfun.(line)
end
def handle_info({:irc, :text, message = %IRC.Message{text: text}}, state) do
String.split(text)
|> Enum.map(fn(word) ->
if String.starts_with?(word, "http://") || String.starts_with?(word, "https://") do
uri = URI.parse(word)
if uri.scheme && uri.host do
if Enum.any?(state.hosts, fn(host) -> String.ends_with?(uri.host, host) end) do
outline_url = outline(word)
line = "-> #{outline_url}"
message.replyfun.(line)
end
end
end
end)
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
def save(state = %{file: file, hosts: hosts}) do
string = Enum.join(hosts, "\n")
File.write(file, string)
end
def outline(url) do
unexpanded = "https://outline.com/#{url}"
headers = [
{"User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:77.0) Gecko/20100101 Firefox/77.0"},
{"Accept", "*/*"},
{"Accept-Language", "en-US,en;q=0.5"},
{"Origin", "https://outline.com"},
{"DNT", "1"},
{"Referer", unexpanded},
{"Pragma", "no-cache"},
{"Cache-Control", "no-cache"}
]
params = %{"source_url" => url}
case HTTPoison.get("https://api.outline.com/v3/parse_article", headers, params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: json}} ->
body = Poison.decode!(json)
if Map.get(body, "success") do
code = get_in(body, ["data", "short_code"])
"https://outline.com/#{code}"
else
unexpanded
end
error ->
Logger.info("outline.com error: #{inspect error}")
unexpanded
end
end
end
diff --git a/lib/lsg_irc/preums_plugin.ex b/lib/lsg_irc/preums_plugin.ex
index 68257f0..9344ce9 100644
--- a/lib/lsg_irc/preums_plugin.ex
+++ b/lib/lsg_irc/preums_plugin.ex
@@ -1,276 +1,276 @@
-defmodule LSG.IRC.PreumsPlugin do
+defmodule Nola.IRC.PreumsPlugin do
@moduledoc """
# preums !!!
* `!preums`: affiche le preums du jour
* `.preums`: stats des preums
"""
# WIP Scores
# L'idée c'est de donner un score pour mettre un peu de challenge en pénalisant les preums faciles.
#
# Un preums ne vaut pas 1 point, mais plutôt 0.10 ou 0.05, et on arrondi au plus proche. C'est un jeu sur le long
# terme. Un gros bonus pourrait apporter beaucoup de points.
#
# Il faudrait ces données:
# - moyenne des preums
# - activité récente du channel et par nb actifs d'utilisateurs
# (aggréger memberships+usertrack last_active ?)
# (faire des stats d'activité habituelle (un peu a la pisg) ?)
# - preums consécutifs
#
# Malus:
# - est proche de la moyenne en faible activité
# - trop consécutif de l'utilisateur sauf si activité
#
# Bonus:
# - plus le preums est éloigné de la moyenne
# - après 18h double
# - plus l'activité est élévée, exponentiel selon la moyenne
# - derns entre 4 et 6 (pourrait être adapté selon les stats d'activité)
#
# WIP Badges:
# - derns
# - streaks
# - faciles
# - ?
require Logger
@perfects [~r/preum(s|)/i]
# dets {{chan, day = {yyyy, mm, dd}}, nick, now, perfect?, text}
def all(dets) do
:dets.foldl(fn(i, acc) -> [i|acc] end, [], dets)
end
def all(dets, channel) do
fun = fn({{chan, date}, account_id, time, perfect, text}, acc) ->
if channel == chan do
[%{date: date, account_id: account_id, time: time, perfect: perfect, text: text} | acc]
else
acc
end
end
:dets.foldl(fun, [], dets)
end
def topnicks(dets, channel, options \\ []) do
sort_elem = case Keyword.get(options, :sort_by, :score) do
:score -> 1
:count -> 0
end
fun = fn(x = {{chan, date}, account_id, time, perfect, text}, acc) ->
if (channel == nil and chan) or (channel == chan) do
{count, points} = Map.get(acc, account_id, {0, 0})
score = score(chan, account_id, time, perfect, text)
Map.put(acc, account_id, {count + 1, points + score})
else
acc
end
end
:dets.foldl(fun, %{}, dets)
|> Enum.sort_by(fn({_account_id, value}) -> elem(value, sort_elem) end, &>=/2)
end
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def dets do
- (LSG.data_path() <> "/preums.dets") |> String.to_charlist()
+ (Nola.data_path() <> "/preums.dets") |> String.to_charlist()
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "account", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "triggers", regopts)
{:ok, dets} = :dets.open_file(dets(), [{:repair, :force}])
Util.ets_mutate_select_each(:dets, dets, [{:"$1", [], [:"$1"]}], fn(table, obj) ->
{key, nick, now, perfect, text} = obj
case key do
{{net, {bork,chan}}, date} ->
:dets.delete(table, key)
nick = if IRC.Account.get(nick) do
nick
else
if acct = IRC.Account.find_always_by_nick(net, nil, nick) do
acct.id
else
nick
end
end
:dets.insert(table, { { {net,chan}, date }, nick, now, perfect, text})
{{_net, nil}, _} ->
:dets.delete(table, key)
{{net, chan}, date} ->
if !IRC.Account.get(nick) do
if acct = IRC.Account.find_always_by_nick(net, chan, nick) do
:dets.delete(table, key)
:dets.insert(table, { { {net,chan}, date }, acct.id, now, perfect, text})
end
end
_ ->
Logger.debug("DID NOT FIX: #{inspect key}")
end
end)
{:ok, %{dets: dets}}
end
# Latest
def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) do
channelkey = {m.network, m.channel}
state = handle_preums(m, state)
tz = timezone(channelkey)
{:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
date = {now.year, now.month, now.day}
key = {channelkey, date}
chan_cache = Map.get(state, channelkey, %{})
item = if i = Map.get(chan_cache, date) do
i
else
case :dets.lookup(state.dets, key) do
[item = {^key, _account_id, _now, _perfect, _text}] -> item
_ -> nil
end
end
if item do
{_, account_id, date, _perfect, text} = item
h = "#{date.hour}:#{date.minute}:#{date.second}"
account = IRC.Account.get(account_id)
user = IRC.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
m.replyfun.("preums: #{nick} à #{h}: “#{text}”")
end
{:noreply, state}
end
# Stats
def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :dot}}}, state) do
channel = {m.network, m.channel}
state = handle_preums(m, state)
top = topnicks(state.dets, channel, sort_by: :score)
|> Enum.map(fn({account_id, {count, score}}) ->
account = IRC.Account.get(account_id)
user = IRC.UserTrack.find_by_account(m.network, account)
nick = if(user, do: user.nick, else: account.name)
"#{nick}: #{score} (#{count})"
end)
|> Enum.intersperse(", ")
|> Enum.join("")
msg = unless top == "" do
"top preums: #{top}"
else
"vous êtes tous nuls"
end
m.replyfun.(msg)
{:noreply, state}
end
# Help
def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :query}}}, state) do
state = handle_preums(m, state)
msg = "!preums - preums du jour, .preums top preumseurs"
m.replymsg.(msg)
{:noreply, state}
end
# Trigger fallback
def handle_info({:irc, :trigger, _, m = %IRC.Message{}}, state) do
state = handle_preums(m, state)
{:noreply, state}
end
# Message fallback
def handle_info({:irc, :text, m = %IRC.Message{}}, state) do
{:noreply, handle_preums(m, state)}
end
# Account
def handle_info({:account_change, old_id, new_id}, state) do
spec = [{{:_, :"$1", :_, :_, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
rename_object_owner(table, obj, new_id)
end)
{:noreply, state}
end
# Account: move from nick to account id
# FIXME: Doesn't seem to work.
def handle_info({:accounts, accounts}, state) do
for x={:account, _net, _chan, _nick, _account_id} <- accounts do
handle_info(x, state)
end
{:noreply, state}
end
def handle_info({:account, _net, _chan, nick, account_id}, state) do
nick = String.downcase(nick)
spec = [{{:_, :"$1", :_, :_, :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
Logger.debug("account:: merging #{nick} -> #{account_id}")
rename_object_owner(table, obj, account_id)
end)
{:noreply, state}
end
def handle_info(_, dets) do
{:noreply, dets}
end
defp rename_object_owner(table, object = {key, _, now, perfect, time}, new_id) do
:dets.delete_object(table, key)
:dets.insert(table, {key, new_id, now, perfect, time})
end
defp timezone(channel) do
- env = Application.get_env(:lsg, LSG.IRC.PreumsPlugin, [])
+ env = Application.get_env(:lsg, Nola.IRC.PreumsPlugin, [])
channels = Keyword.get(env, :channels, %{})
channel_settings = Map.get(channels, channel, [])
default = Keyword.get(env, :default_tz, "Europe/Paris")
Keyword.get(channel_settings, :tz, default) || default
end
defp handle_preums(%IRC.Message{channel: nil}, state) do
state
end
defp handle_preums(m = %IRC.Message{text: text, sender: sender}, state) do
channel = {m.network, m.channel}
tz = timezone(channel)
{:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
date = {now.year, now.month, now.day}
key = {channel, date}
chan_cache = Map.get(state, channel, %{})
unless i = Map.get(chan_cache, date) do
case :dets.lookup(state.dets, key) do
[item = {^key, _nick, _now, _perfect, _text}] ->
# Preums lost, but wasn't cached
Map.put(state, channel, %{date => item})
[] ->
# Preums won!
perfect? = Enum.any?(@perfects, fn(perfect) -> Regex.match?(perfect, text) end)
item = {key, m.account.id, now, perfect?, text}
:dets.insert(state.dets, item)
:dets.sync(state.dets)
Map.put(state, channel, %{date => item})
{:error, _} = error ->
Logger.error("#{__MODULE__} dets lookup failed: #{inspect error}")
state
end
else
state
end
end
def score(_chan, _account, _time, _perfect, _text) do
1
end
end
diff --git a/lib/lsg_irc/quatre_cent_vingt_plugin.ex b/lib/lsg_irc/quatre_cent_vingt_plugin.ex
index fff7e4f..8953ea3 100644
--- a/lib/lsg_irc/quatre_cent_vingt_plugin.ex
+++ b/lib/lsg_irc/quatre_cent_vingt_plugin.ex
@@ -1,149 +1,149 @@
-defmodule LSG.IRC.QuatreCentVingtPlugin do
+defmodule Nola.IRC.QuatreCentVingtPlugin do
require Logger
@moduledoc """
# 420
* **!420**: recorde un nouveau 420.
* **!420*x**: recorde un nouveau 420*x (*2 = 840, ...) (à vous de faire la multiplication).
* **!420 pseudo**: stats du pseudo.
"""
@achievements %{
1 => ["[le premier… il faut bien commencer un jour]"],
10 => ["T'en es seulement à 10 ? ╭∩╮(Ο_Ο)╭∩╮"],
42 => ["Bravo, et est-ce que autant de pétards t'on aidés à trouver la Réponse ? ٩(- ̮̮̃-̃)۶ [42]"],
100 => ["°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ 100 °º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸"],
115 => [" ۜ\(סּںסּَ` )/ۜ 115!!"]
}
@emojis [
"\\o/",
"~o~",
"~~o∞~~",
"*\\o/*",
"**\\o/**",
"*ô*",
]
@coeffs Range.new(1, 100)
def irc_doc, do: @moduledoc
def start_link, do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
def init(_) do
for coeff <- @coeffs do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:#{420*coeff}", [plugin: __MODULE__])
end
{:ok, _} = Registry.register(IRC.PubSub, "account", [plugin: __MODULE__])
- dets_filename = (LSG.data_path() <> "/420.dets") |> String.to_charlist
+ dets_filename = (Nola.data_path() <> "/420.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag},{:repair,:force}])
{:ok, dets}
:ignore
end
for coeff <- @coeffs do
qvc = to_string(420 * coeff)
def handle_info({:irc, :trigger, unquote(qvc), m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :bang}}}, dets) do
{count, last} = get_statistics_for_nick(dets, m.account.id)
count = count + unquote(coeff)
text = achievement_text(count)
now = DateTime.to_unix(DateTime.utc_now())-1 # this is ugly
for i <- Range.new(1, unquote(coeff)) do
:ok = :dets.insert(dets, {m.account.id, now+i})
end
last_s = if last do
last_s = format_relative_timestamp(last)
" (le dernier était #{last_s})"
else
""
end
m.replyfun.("#{m.sender.nick} 420 +#{unquote(coeff)} #{text}#{last_s}")
{:noreply, dets}
end
end
def handle_info({:irc, :trigger, "420", m = %IRC.Message{trigger: %IRC.Trigger{args: [nick], type: :bang}}}, dets) do
account = IRC.Account.find_by_nick(m.network, nick)
if account do
text = case get_statistics_for_nick(dets, m.account.id) do
{0, _} -> "#{nick} n'a jamais !420 ... honte à lui."
{count, last} ->
last_s = format_relative_timestamp(last)
"#{nick} 420: total #{count}, le dernier #{last_s}"
end
m.replyfun.(text)
else
m.replyfun.("je connais pas de #{nick}")
end
{:noreply, dets}
end
# Account
def handle_info({:account_change, old_id, new_id}, dets) do
spec = [{{:"$1", :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, dets, spec, fn(table, obj) ->
rename_object_owner(table, obj, new_id)
end)
{:noreply, dets}
end
# Account: move from nick to account id
def handle_info({:accounts, accounts}, dets) do
for x={:account, _net, _chan, _nick, _account_id} <- accounts do
handle_info(x, dets)
end
{:noreply, dets}
end
def handle_info({:account, _net, _chan, nick, account_id}, dets) do
nick = String.downcase(nick)
spec = [{{:"$1", :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, dets, spec, fn(table, obj) ->
Logger.debug("account:: merging #{nick} -> #{account_id}")
rename_object_owner(table, obj, account_id)
end)
{:noreply, dets}
end
def handle_info(_, dets) do
{:noreply, dets}
end
defp rename_object_owner(table, object = {_, at}, account_id) do
:dets.delete_object(table, object)
:dets.insert(table, {account_id, at})
end
defp format_relative_timestamp(timestamp) do
alias Timex.Format.DateTime.Formatters
alias Timex.Timezone
date = timestamp
|> DateTime.from_unix!
|> Timezone.convert("Europe/Paris")
{:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
{:ok, detail} = Formatters.Default.lformat(date, " ({h24}:{m})", "fr")
relative <> detail
end
defp get_statistics_for_nick(dets, acct) do
qvc = :dets.lookup(dets, acct) |> Enum.sort
count = Enum.reduce(qvc, 0, fn(_, acc) -> acc + 1 end)
{_, last} = List.last(qvc) || {nil, nil}
{count, last}
end
@achievements_keys Map.keys(@achievements)
defp achievement_text(count) when count in @achievements_keys do
Enum.random(Map.get(@achievements, count))
end
defp achievement_text(count) do
emoji = Enum.random(@emojis)
"#{emoji} [#{count}]"
end
end
diff --git a/lib/lsg_irc/radio_france_plugin.ex b/lib/lsg_irc/radio_france_plugin.ex
index 34935ca..c2e966f 100644
--- a/lib/lsg_irc/radio_france_plugin.ex
+++ b/lib/lsg_irc/radio_france_plugin.ex
@@ -1,133 +1,133 @@
-defmodule LSG.IRC.RadioFrancePlugin do
+defmodule Nola.IRC.RadioFrancePlugin do
require Logger
def irc_doc() do
"""
# radio france
Qu'est ce qu'on écoute sur radio france ?
* **!radiofrance `[station]`, !rf `[station]`**
* **!fip, !inter, !info, !bleu, !culture, !musique, !fip `[sous-station]`, !bleu `[région]`**
"""
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@trigger "radiofrance"
@shortcuts ~w(fip inter info bleu culture musique)
def init(_) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:radiofrance", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:rf", regopts)
for s <- @shortcuts do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:#{s}", regopts)
end
{:ok, nil}
end
def handle_info({:irc, :trigger, "rf", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) do
handle_info({:irc, :trigger, "radiofrance", m}, state)
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do
m.replyfun.("radiofrance: précisez la station!")
{:noreply, state}
end
def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do
now(args_to_station(args), m)
{:noreply, state}
end
def handle_info({:irc, :trigger, trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) when trigger in @shortcuts do
now(args_to_station([trigger | args]), m)
{:noreply, state}
end
defp args_to_station(args) do
args
|> Enum.map(&unalias/1)
|> Enum.map(&String.downcase/1)
|> Enum.join("_")
end
def handle_info(info, state) do
Logger.debug("unhandled info: #{inspect info}")
{:noreply, state}
end
defp now(station, m) when is_binary(station) do
case HTTPoison.get(np_url(station), [], []) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
json = Poison.decode!(body)
song? = !!get_in(json, ["now", "song"])
station = reformat_station_name(get_in(json, ["now", "stationName"]))
now_title = get_in(json, ["now", "firstLine", "title"])
now_subtitle = get_in(json, ["now", "secondLine", "title"])
next_title = get_in(json, ["next", "firstLine", "title"])
next_subtitle = get_in(json, ["next", "secondLine", "title"])
next_song? = !!get_in(json, ["next", "song"])
next_at = get_in(json, ["next", "startTime"])
now = format_title(song?, now_title, now_subtitle)
prefix = if song?, do: "🎶", else: "🎤"
m.replyfun.("#{prefix} #{station}: #{now}")
next = format_title(song?, next_title, next_subtitle)
if next do
next_prefix = if next_at do
next_date = DateTime.from_unix!(next_at)
in_seconds = DateTime.diff(next_date, DateTime.utc_now())
in_minutes = ceil(in_seconds / 60)
if in_minutes >= 5 do
if next_song?, do: "#{in_minutes}m 🔜", else: "dans #{in_minutes} minutes:"
else
if next_song?, do: "🔜", else: "suivi de:"
end
else
if next_song?, do: "🔜", else: "à suivre:"
end
m.replyfun.("#{next_prefix} #{next}")
end
{:error, %HTTPoison.Response{status_code: 404}} ->
m.replyfun.("radiofrance: la radio \"#{station}\" n'existe pas")
{:error, %HTTPoison.Response{status_code: code}} ->
m.replyfun.("radiofrance: erreur http #{code}")
_ ->
m.replyfun.("radiofrance: ça n'a pas marché, rip")
end
end
defp np_url(station), do: "https://www.radiofrance.fr/api/v2.0/stations/#{station}/live"
defp unalias("inter"), do: "franceinter"
defp unalias("info"), do: "franceinfo"
defp unalias("bleu"), do: "francebleu"
defp unalias("culture"), do: "franceculture"
defp unalias("musique"), do: "francemusique"
defp unalias(station), do: station
defp format_title(_, nil, nil) do
nil
end
defp format_title(true, title, artist) do
[artist, title] |> Enum.filter(& &1) |> Enum.join(" - ")
end
defp format_title(false, show, section) do
[show, section] |> Enum.filter(& &1) |> Enum.join(": ")
end
defp reformat_station_name(station) do
station
|> String.replace("france", "france ")
|> String.replace("_", " ")
end
end
diff --git a/lib/lsg_irc/say_plugin.ex b/lib/lsg_irc/say_plugin.ex
index 8e93ec2..915b0f6 100644
--- a/lib/lsg_irc/say_plugin.ex
+++ b/lib/lsg_irc/say_plugin.ex
@@ -1,73 +1,73 @@
-defmodule LSG.IRC.SayPlugin do
+defmodule Nola.IRC.SayPlugin do
def irc_doc do
"""
# say
Say something...
* **!say `<channel>` `<text>`** say something on `channel`
* **!asay `<channel>` `<text>`** same but anonymously
You must be a member of the channel.
"""
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [type: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "trigger:say", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:asay", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages:private", regopts)
{:ok, nil}
end
def handle_info({:irc, :trigger, "say", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do
text = Enum.join(text, " ")
say_for(m.account, target, text, true)
{:noreply, state}
end
def handle_info({:irc, :trigger, "asay", m = %{trigger: %{type: :bang, args: [target | text]}}}, state) do
text = Enum.join(text, " ")
say_for(m.account, target, text, false)
{:noreply, state}
end
def handle_info({:irc, :text, m = %{text: "say "<>rest}}, state) do
case String.split(rest, " ", parts: 2) do
[target, text] -> say_for(m.account, target, text, true)
_ -> nil
end
{:noreply, state}
end
def handle_info({:irc, :text, m = %{text: "asay "<>rest}}, state) do
case String.split(rest, " ", parts: 2) do
[target, text] -> say_for(m.account, target, text, false)
_ -> nil
end
{:noreply, state}
end
def handle_info(_, state) do
{:noreply, state}
end
defp say_for(account, target, text, with_nick?) do
for {net, chan} <- IRC.Membership.of_account(account) do
chan2 = String.replace(chan, "#", "")
if (target == "#{net}/#{chan}" || target == "#{net}/#{chan2}" || target == chan || target == chan2) do
if with_nick? do
IRC.send_message_as(account, net, chan, text)
else
IRC.Connection.broadcast_message(net, chan, text)
end
end
end
end
end
diff --git a/lib/lsg_irc/script_plugin.ex b/lib/lsg_irc/script_plugin.ex
index bae6f3f..94d4edf 100644
--- a/lib/lsg_irc/script_plugin.ex
+++ b/lib/lsg_irc/script_plugin.ex
@@ -1,42 +1,42 @@
-defmodule LSG.IRC.ScriptPlugin do
+defmodule Nola.IRC.ScriptPlugin do
require Logger
@moduledoc """
Allows to run outside scripts. Scripts are expected to be long running and receive/send data as JSON over stdin/stdout.
"""
@ircdoc """
# script
Allows to run an outside script.
* **+script `<name>` `[command]`** défini/lance un script
* **-script `<name>`** arrête un script
* **-script del `<name>`** supprime un script
"""
def irc_doc, do: @ircdoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:script", [plugin: __MODULE__])
- dets_filename = (LSG.data_path() <> "/" <> "scripts.dets") |> String.to_charlist
+ dets_filename = (Nola.data_path() <> "/" <> "scripts.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [])
{:ok, %{dets: dets}}
end
def handle_info({:irc, :trigger, "script", m = %{trigger: %{type: :plus, args: [name | args]}}}, state) do
end
def handle_info({:irc, :trigger, "script", m = %{trigger: %{type: :minus, args: args}}}, state) do
case args do
["del", name] -> :ok #prout
[name] -> :ok#stop
end
end
end
diff --git a/lib/lsg_irc/seen_plugin.ex b/lib/lsg_irc/seen_plugin.ex
index 405c372..2a4d0dd 100644
--- a/lib/lsg_irc/seen_plugin.ex
+++ b/lib/lsg_irc/seen_plugin.ex
@@ -1,59 +1,59 @@
-defmodule LSG.IRC.SeenPlugin do
+defmodule Nola.IRC.SeenPlugin do
@moduledoc """
# seen
* **!seen `<nick>`**
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "triggers", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "messages", regopts)
- dets_filename = (LSG.data_path() <> "/seen.dets") |> String.to_charlist()
+ dets_filename = (Nola.data_path() <> "/seen.dets") |> String.to_charlist()
{:ok, dets} = :dets.open_file(dets_filename, [])
{:ok, %{dets: dets}}
end
def handle_info({:irc, :trigger, "seen", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [nick]}}}, state) do
witness(m, state)
m.replyfun.(last_seen(m.channel, nick, state))
{:noreply, state}
end
def handle_info({:irc, :trigger, _, m}, state) do
witness(m, state)
{:noreply, state}
end
def handle_info({:irc, :text, m}, state) do
witness(m, state)
{:noreply, state}
end
defp witness(%IRC.Message{channel: channel, text: text, sender: %{nick: nick}}, %{dets: dets}) do
:dets.insert(dets, {{channel, nick}, DateTime.utc_now(), text})
:ok
end
defp last_seen(channel, nick, %{dets: dets}) do
case :dets.lookup(dets, {channel, nick}) do
[{_, date, text}] ->
diff = round(DateTime.diff(DateTime.utc_now(), date)/60)
cond do
diff >= 30 ->
duration = Timex.Duration.from_minutes(diff)
format = Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
"#{nick} a parlé pour la dernière fois il y a #{format}: “#{text}”"
true -> "#{nick} est là..."
end
[] ->
"je ne connais pas de #{nick}"
end
end
end
diff --git a/lib/lsg_irc/sms_plugin.ex b/lib/lsg_irc/sms_plugin.ex
index be1611f..2bbf13e 100644
--- a/lib/lsg_irc/sms_plugin.ex
+++ b/lib/lsg_irc/sms_plugin.ex
@@ -1,165 +1,165 @@
-defmodule LSG.IRC.SmsPlugin do
+defmodule Nola.IRC.SmsPlugin do
@moduledoc """
## sms
* **!sms `<nick>` `<message>`** envoie un SMS.
"""
def short_irc_doc, do: false
def irc_doc, do: @moduledoc
require Logger
def incoming(from, "enable "<>key) do
key = String.trim(key)
account = IRC.Account.find_meta_account("sms-validation-code", String.downcase(key))
if account do
net = IRC.Account.get_meta(account, "sms-validation-target")
IRC.Account.put_meta(account, "sms-number", from)
IRC.Account.delete_meta(account, "sms-validation-code")
IRC.Account.delete_meta(account, "sms-validation-number")
IRC.Account.delete_meta(account, "sms-validation-target")
IRC.Connection.broadcast_message(net, account, "SMS Number #{from} added!")
send_sms(from, "Yay! Number linked to account #{account.name}")
end
end
def incoming(from, message) do
account = IRC.Account.find_meta_account("sms-number", from)
if account do
reply_fun = fn(text) ->
send_sms(from, text)
end
trigger_text = if Enum.any?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(message, trigger) end) do
message
else
"!"<>message
end
message = %IRC.Message{
id: FlakeId.get(),
transport: :sms,
network: "sms",
channel: nil,
text: message,
account: account,
sender: %ExIRC.SenderInfo{nick: account.name},
replyfun: reply_fun,
trigger: IRC.Connection.extract_trigger(trigger_text)
}
Logger.debug("converted sms to message: #{inspect message}")
IRC.Connection.publish(message, ["messages:sms"])
message
end
end
def my_number() do
Keyword.get(Application.get_env(:lsg, :sms, []), :number, "+33000000000")
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def path() do
account = Keyword.get(Application.get_env(:lsg, :sms), :account)
"https://eu.api.ovh.com/1.0/sms/#{account}"
end
def path(rest) do
Path.join(path(), rest)
end
def send_sms(number, text) do
url = path("/virtualNumbers/#{my_number()}/jobs")
body = %{
"message" => text,
"receivers" => [number],
#"senderForResponse" => true,
#"noStopClause" => true,
"charset" => "UTF-8",
"coding" => "8bit"
} |> Poison.encode!()
headers = [{"content-type", "application/json"}] ++ sign("POST", url, body)
options = []
case HTTPoison.post(url, body, headers, options) do
{:ok, %HTTPoison.Response{status_code: 200}} -> :ok
{:ok, %HTTPoison.Response{status_code: code} = resp} ->
Logger.error("SMS Error: #{inspect resp}")
{:error, code}
{:error, error} -> {:error, error}
end
end
def init([]) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:sms", [plugin: __MODULE__])
:ok = register_ovh_callback()
{:ok, %{}}
:ignore
end
def handle_info({:irc, :trigger, "sms", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [nick | text]}}}, state) do
with \
{:tree, false} <- {:tree, m.sender.nick == "Tree"},
{_, %IRC.Account{} = account} <- {:account, IRC.Account.find_always_by_nick(m.network, m.channel, nick)},
{_, number} when not is_nil(number) <- {:number, IRC.Account.get_meta(account, "sms-number")}
do
text = Enum.join(text, " ")
sender = if m.channel do
"#{m.channel} <#{m.sender.nick}> "
else
"<#{m.sender.nick}> "
end
case send_sms(number, sender<>text) do
:ok -> m.replyfun.("sent!")
{:error, error} -> m.replyfun.("not sent, error: #{inspect error}")
end
else
{:tree, _} -> m.replyfun.("Tree: va en enfer")
{:account, _} -> m.replyfun.("#{nick} not known")
{:number, _} -> m.replyfun.("#{nick} have not enabled sms")
end
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
defp register_ovh_callback() do
url = path()
body = %{
- "callBack" =>LSGWeb.Router.Helpers.sms_url(LSGWeb.Endpoint, :ovh_callback),
+ "callBack" =>NolaWeb.Router.Helpers.sms_url(NolaWeb.Endpoint, :ovh_callback),
"smsResponse" => %{
- "cgiUrl" => LSGWeb.Router.Helpers.sms_url(LSGWeb.Endpoint, :ovh_callback),
+ "cgiUrl" => NolaWeb.Router.Helpers.sms_url(NolaWeb.Endpoint, :ovh_callback),
"responseType" => "cgi"
}
} |> Poison.encode!()
headers = [{"content-type", "application/json"}] ++ sign("PUT", url, body)
options = []
case HTTPoison.put(url, body, headers, options) do
{:ok, %HTTPoison.Response{status_code: 200}} ->
:ok
error -> error
end
end
defp sign(method, url, body) do
ts = DateTime.utc_now() |> DateTime.to_unix()
as = env(:app_secret)
ck = env(:consumer_key)
sign = Enum.join([as, ck, String.upcase(method), url, body, ts], "+")
sign_hex = :crypto.hash(:sha, sign) |> Base.encode16(case: :lower)
headers = [{"X-OVH-Application", env(:app_key)}, {"X-OVH-Timestamp", ts},
{"X-OVH-Signature", "$1$"<>sign_hex}, {"X-Ovh-Consumer", ck}]
end
def parse_number(num) do
{:error, :todo}
end
defp env() do
Application.get_env(:lsg, :sms)
end
defp env(key) do
Keyword.get(env(), key)
end
end
diff --git a/lib/lsg_irc/tell_plugin.ex b/lib/lsg_irc/tell_plugin.ex
index baaa0c5..ecc98df 100644
--- a/lib/lsg_irc/tell_plugin.ex
+++ b/lib/lsg_irc/tell_plugin.ex
@@ -1,106 +1,106 @@
-defmodule LSG.IRC.TellPlugin do
+defmodule Nola.IRC.TellPlugin do
use GenServer
@moduledoc """
# Tell
* **!tell `<nick>` `<message>`**: tell `message` to `nick` when they reconnect.
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def dets do
- (LSG.data_path() <> "/tell.dets") |> String.to_charlist()
+ (Nola.data_path() <> "/tell.dets") |> String.to_charlist()
end
def tell(m, target, message) do
GenServer.cast(__MODULE__, {:tell, m, target, message})
end
def init([]) do
regopts = [plugin: __MODULE__]
{:ok, _} = Registry.register(IRC.PubSub, "account", regopts)
{:ok, _} = Registry.register(IRC.PubSub, "trigger:tell", regopts)
{:ok, dets} = :dets.open_file(dets(), [type: :bag])
{:ok, %{dets: dets}}
end
def handle_cast({:tell, m, target, message}, state) do
do_tell(state, m, target, message)
{:noreply, state}
end
def handle_info({:irc, :trigger, "tell", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [target | message]}}}, state) do
do_tell(state, m, target, message)
{:noreply, state}
end
def handle_info({:account, network, channel, nick, account_id}, state) do
messages = :dets.lookup(state.dets, {network, channel, account_id})
if messages != [] do
strs = Enum.map(messages, fn({_, from, message, at}) ->
account = IRC.Account.get(from)
user = IRC.UserTrack.find_by_account(network, account)
fromnick = if user, do: user.nick, else: account.name
"#{nick}: <#{fromnick}> #{message}"
end)
Enum.each(strs, fn(s) -> IRC.Connection.broadcast_message(network, channel, s) end)
:dets.delete(state.dets, {network, channel, account_id})
end
{:noreply, state}
end
def handle_info({:account_change, old_id, new_id}, state) do
#:ets.fun2ms(fn({ {_net, _chan, target_id}, from_id, _, _} = obj) when (target_id == old_id) or (from_id == old_id) -> obj end)
spec = [{{{:"$1", :"$2", :"$3"}, :"$4", :_, :_}, [{:orelse, {:==, :"$3", {:const, old_id}}, {:==, :"$4", {:const, old_id}}}], [:"$_"]}]
Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
case obj do
{ {net, chan, ^old_id}, from_id, message, at } = obj ->
:dets.delete(obj)
:dets.insert(table, {{net, chan, new_id}, from_id, message, at})
{key, ^old_id, message, at} = obj ->
:dets.delete(table, obj)
:dets.insert(table, {key, new_id, message, at})
_ -> :ok
end
end)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
def terminate(_, state) do
:dets.close(state.dets)
:ok
end
defp do_tell(state, m, nick_target, message) do
target = IRC.Account.find_always_by_nick(m.network, m.channel, nick_target)
message = Enum.join(message, " ")
with \
{:target, %IRC.Account{} = target} <- {:target, target},
{:same, false} <- {:same, target.id == m.account.id},
target_user = IRC.UserTrack.find_by_account(m.network, target),
target_nick = if(target_user, do: target_user.nick, else: target.name),
present? = if(target_user, do: Map.has_key?(target_user.last_active, m.channel)),
{:absent, true, _} <- {:absent, !present?, target_nick},
{:message, message} <- {:message, message}
do
obj = { {m.network, m.channel, target.id}, m.account.id, message, NaiveDateTime.utc_now()}
:dets.insert(state.dets, obj)
m.replyfun.("will tell to #{target_nick}")
else
{:same, _} -> m.replyfun.("are you so stupid that you need a bot to tell yourself things ?")
{:target, _} -> m.replyfun.("#{nick_target} unknown")
{:absent, _, nick} -> m.replyfun.("#{nick} is here, tell yourself!")
{:message, _} -> m.replyfun.("can't tell without a message")
end
end
end
diff --git a/lib/lsg_irc/txt_plugin.ex b/lib/lsg_irc/txt_plugin.ex
index 2c9dfca..d2bb627 100644
--- a/lib/lsg_irc/txt_plugin.ex
+++ b/lib/lsg_irc/txt_plugin.ex
@@ -1,556 +1,556 @@
-defmodule LSG.IRC.TxtPlugin do
+defmodule Nola.IRC.TxtPlugin do
alias IRC.UserTrack
require Logger
@moduledoc """
# [txt]({{context_path}}/txt)
* **.txt**: liste des fichiers et statistiques.
Les fichiers avec une `*` sont vérrouillés.
[Voir sur le web]({{context_path}}/txt).
* **!txt**: lis aléatoirement une ligne dans tous les fichiers.
* **!txt `<recherche>`**: recherche une ligne dans tous les fichiers.
* **~txt**: essaie de générer une phrase (markov).
* **~txt `<début>`**: essaie de générer une phrase commencant par `<debut>`.
* **!`FICHIER`**: lis aléatoirement une ligne du fichier `FICHIER`.
* **!`FICHIER` `<chiffre>`**: lis la ligne `<chiffre>` du fichier `FICHIER`.
* **!`FICHIER` `<recherche>`**: recherche une ligne contenant `<recherche>` dans `FICHIER`.
* **+txt `<file`>**: crée le fichier `<file>`.
* **+`FICHIER` `<texte>`**: ajoute une ligne `<texte>` dans le fichier `FICHIER`.
* **-`FICHIER` `<chiffre>`**: supprime la ligne `<chiffre>` du fichier `FICHIER`.
* **-txtrw, +txtrw**. op seulement. active/désactive le mode lecture seule.
* **+txtlock `<fichier>`, -txtlock `<fichier>`**. op seulement. active/désactive le verrouillage d'un fichier.
Insérez `\\\\` pour faire un saut de ligne.
"""
def short_irc_doc, do: "!txt https://sys.115ans.net/irc/txt "
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
defstruct triggers: %{}, rw: true, locks: nil, markov_handler: nil, markov: nil
def random(file) do
GenServer.call(__MODULE__, {:random, file})
end
def reply_random(message, file) do
if line = random(file) do
line
|> format_line(nil, message)
|> message.replyfun.()
line
end
end
def init([]) do
- dets_locks_filename = (LSG.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist
+ dets_locks_filename = (Nola.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist
{:ok, locks} = :dets.open_file(dets_locks_filename, [])
- markov_handler = Keyword.get(Application.get_env(:lsg, __MODULE__, []), :markov_handler, LSG.IRC.TxtPlugin.Markov.Native)
+ markov_handler = Keyword.get(Application.get_env(:lsg, __MODULE__, []), :markov_handler, Nola.IRC.TxtPlugin.Markov.Native)
{:ok, markov} = markov_handler.start_link()
{:ok, _} = Registry.register(IRC.PubSub, "triggers", [plugin: __MODULE__])
{:ok, %__MODULE__{locks: locks, markov_handler: markov_handler, markov: markov, triggers: load()}}
end
def handle_info({:received, "!reload", _, chan}, state) do
{:noreply, %__MODULE__{state | triggers: load()}}
end
#
# ADMIN: RW/RO
#
def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :plus}}}, state = %{rw: false}) do
if channel && UserTrack.operator?(msg.network, channel, msg.sender.nick) do
msg.replyfun.("txt: écriture réactivée")
{:noreply, %__MODULE__{state | rw: true}}
else
{:noreply, state}
end
end
def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :minus}}}, state = %{rw: true}) do
if channel && UserTrack.operator?(msg.network, channel, msg.sender.nick) do
msg.replyfun.("txt: écriture désactivée")
{:noreply, %__MODULE__{state | rw: false}}
else
{:noreply, state}
end
end
#
# ADMIN: LOCKS
#
def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- UserTrack.operator?(msg.network, msg.channel, msg.sender.nick)
do
:dets.insert(state.locks, {trigger})
msg.replyfun.("txt: #{trigger} verrouillé")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :minus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- UserTrack.operator?(msg.network, msg.channel, msg.sender.nick),
true <- :dets.member(state.locks, trigger)
do
:dets.delete(state.locks, trigger)
msg.replyfun.("txt: #{trigger} déverrouillé")
end
{:noreply, state}
end
#
# FILE LIST
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :dot}}}, state) do
map = Enum.map(state.triggers, fn({key, data}) ->
ignore? = String.contains?(key, ".")
locked? = case :dets.lookup(state.locks, key) do
[{trigger}] -> "*"
_ -> ""
end
unless ignore?, do: "#{key}: #{to_string(Enum.count(data))}#{locked?}"
end)
|> Enum.filter(& &1)
total = Enum.reduce(state.triggers, 0, fn({_, data}, acc) ->
acc + Enum.count(data)
end)
detail = Enum.join(map, ", ")
total = ". total: #{Enum.count(state.triggers)} fichiers, #{to_string(total)} lignes. Détail: https://sys.115ans.net/irc/txt"
ro = if !state.rw, do: " (lecture seule activée)", else: ""
(detail<>total<>ro)
|> msg.replyfun.()
{:noreply, state}
end
#
# GLOBAL: RANDOM
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: []}}}, state) do
result = Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
Enum.reduce(data, acc, fn({l, _}, acc) ->
[{trigger, l} | acc]
end)
end)
|> Enum.shuffle()
if !Enum.empty?(result) do
{source, line} = Enum.random(result)
msg.replyfun.(format_line(line, "#{source}: ", msg))
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: args}}}, state) do
grep = Enum.join(args, " ")
|> String.downcase
|> :unicode.characters_to_nfd_binary()
result = with_stateful_results(msg, {:bang,"txt",msg.network,msg.channel,grep}, fn() ->
Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
if !String.contains?(trigger, ".") do
Enum.reduce(data, acc, fn({l, _}, acc) ->
[{trigger, l} | acc]
end)
else
acc
end
end)
|> Enum.filter(fn({_, line}) ->
line
|> String.downcase()
|> :unicode.characters_to_nfd_binary()
|> String.contains?(grep)
end)
|> Enum.shuffle()
end)
if result do
{source, line} = result
msg.replyfun.(["#{source}: " | line])
end
{:noreply, state}
end
def with_stateful_results(msg, key, initfun) do
me = self()
scope = {msg.network, msg.channel || msg.sender.nick}
key = {__MODULE__, me, scope, key}
with_stateful_results(key, initfun)
end
def with_stateful_results(key, initfun) do
pid = case :global.whereis_name(key) do
:undefined ->
start_stateful_results(key, initfun.())
pid -> pid
end
if pid, do: wait_stateful_results(key, initfun, pid)
end
def start_stateful_results(key, []) do
nil
end
def start_stateful_results(key, list) do
me = self()
{pid, _} = spawn_monitor(fn() ->
Process.monitor(me)
stateful_results(me, list)
end)
:yes = :global.register_name(key, pid)
pid
end
def wait_stateful_results(key, initfun, pid) do
send(pid, :get)
receive do
{:stateful_results, line} ->
line
{:DOWN, _ref, :process, ^pid, reason} ->
with_stateful_results(key, initfun)
after
5000 ->
nil
end
end
defp stateful_results(owner, []) do
send(owner, :empty)
:ok
end
@stateful_results_expire :timer.minutes(30)
defp stateful_results(owner, [line | rest] = acc) do
receive do
:get ->
send(owner, {:stateful_results, line})
stateful_results(owner, rest)
{:DOWN, _ref, :process, ^owner, _} ->
:ok
after
@stateful_results_expire -> :ok
end
end
#
# GLOBAL: MARKOV
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :tilde, args: []}}}, state) do
case state.markov_handler.sentence(state.markov) do
{:ok, line} ->
msg.replyfun.(line)
error ->
Logger.error "Txt Markov error: "<>inspect error
end
{:noreply, state}
end
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :tilde, args: complete}}}, state) do
complete = Enum.join(complete, " ")
case state.markov_handler.complete_sentence(complete, state.markov) do
{:ok, line} ->
msg.replyfun.(line)
error ->
Logger.error "Txt Markov error: "<>inspect error
end
{:noreply, state}
end
#
# TXT CREATE
#
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- can_write?(state, msg, trigger),
:ok <- create_file(trigger)
do
msg.replyfun.("#{trigger}.txt créé. Ajouter: `+#{trigger} …` ; Lire: `!#{trigger}`")
{:noreply, %__MODULE__{state | triggers: load()}}
else
_ -> {:noreply, state}
end
end
#
# TXT: RANDOM
#
def handle_info({:irc, :trigger, trigger, m = %{trigger: %{type: :query, args: opts}}}, state) do
{trigger, _} = clean_trigger(trigger)
if Map.get(state.triggers, trigger) do
url = if m.channel do
- LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :txt, m.network, LSGWeb.format_chan(m.channel), trigger)
+ NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :txt, m.network, NolaWeb.format_chan(m.channel), trigger)
else
- LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :txt, trigger)
+ NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :txt, trigger)
end
m.replyfun.("-> #{url}")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :bang, args: opts}}}, state) do
{trigger, _} = clean_trigger(trigger)
line = get_random(msg, state.triggers, trigger, String.trim(Enum.join(opts, " ")))
if line do
msg.replyfun.(format_line(line, nil, msg))
end
{:noreply, state}
end
#
# TXT: ADD
#
def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :plus, args: content}}}, state) do
with \
true <- can_write?(state, msg, trigger),
{:ok, idx} <- add(state.triggers, msg.text)
do
msg.replyfun.("#{msg.sender.nick}: ajouté à #{trigger}. (#{idx})")
{:noreply, %__MODULE__{state | triggers: load()}}
else
{:error, {:jaro, string, idx}} ->
msg.replyfun.("#{msg.sender.nick}: doublon #{trigger}##{idx}: #{string}")
error ->
Logger.debug("txt add failed: #{inspect error}")
{:noreply, state}
end
end
#
# TXT: DELETE
#
def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :minus, args: [id]}}}, state) do
with \
true <- can_write?(state, msg, trigger),
data <- Map.get(state.triggers, trigger),
{id, ""} <- Integer.parse(id),
{text, _id} <- Enum.find(data, fn({_, idx}) -> id-1 == idx end)
do
data = data |> Enum.into(Map.new)
data = Map.delete(data, text)
msg.replyfun.("#{msg.sender.nick}: #{trigger}.txt##{id} supprimée: #{text}")
dump(trigger, data)
{:noreply, %__MODULE__{state | triggers: load()}}
else
_ ->
{:noreply, state}
end
end
def handle_info(:reload_markov, state=%__MODULE__{triggers: triggers, markov: markov}) do
state.markov_handler.reload(state.triggers, state.markov)
{:noreply, state}
end
def handle_info(msg, state) do
{:noreply, state}
end
def handle_call({:random, file}, _from, state) do
random = get_random(nil, state.triggers, file, [])
{:reply, random, state}
end
def terminate(_reason, state) do
if state.locks do
:dets.sync(state.locks)
:dets.close(state.locks)
end
:ok
end
# Load/Reloads text files from disk
defp load() do
triggers = Path.wildcard(directory() <> "/*.txt")
|> Enum.reduce(%{}, fn(path, m) ->
file = Path.basename(path)
key = String.replace(file, ".txt", "")
data = directory() <> file
|> File.read!
|> String.split("\n")
|> Enum.reject(fn(line) ->
cond do
line == "" -> true
!line -> true
true -> false
end
end)
|> Enum.with_index
Map.put(m, key, data)
end)
|> Enum.sort
|> Enum.into(Map.new)
send(self(), :reload_markov)
triggers
end
defp dump(trigger, data) do
data = data
|> Enum.sort_by(fn({_, idx}) -> idx end)
|> Enum.map(fn({text, _}) -> text end)
|> Enum.join("\n")
File.write!(directory() <> "/" <> trigger <> ".txt", data<>"\n", [])
end
defp get_random(msg, triggers, trigger, []) do
if data = Map.get(triggers, trigger) do
{data, _idx} = Enum.random(data)
data
else
nil
end
end
defp get_random(msg, triggers, trigger, opt) do
arg = case Integer.parse(opt) do
{pos, ""} -> {:index, pos}
{_pos, _some_string} -> {:grep, opt}
_error -> {:grep, opt}
end
get_with_param(msg, triggers, trigger, arg)
end
defp get_with_param(msg, triggers, trigger, {:index, pos}) do
data = Map.get(triggers, trigger, %{})
case Enum.find(data, fn({_, index}) -> index+1 == pos end) do
{text, _} -> text
_ -> nil
end
end
defp get_with_param(msg, triggers, trigger, {:grep, query}) do
out = with_stateful_results(msg, {:grep, trigger, query}, fn() ->
data = Map.get(triggers, trigger, %{})
regex = Regex.compile!("#{query}", "i")
Enum.filter(data, fn({txt, _}) -> Regex.match?(regex, txt) end)
|> Enum.map(fn({txt, _}) -> txt end)
|> Enum.shuffle()
end)
if out, do: out
end
defp create_file(name) do
File.touch!(directory() <> "/" <> name <> ".txt")
:ok
end
defp add(triggers, trigger_and_content) do
case String.split(trigger_and_content, " ", parts: 2) do
[trigger, content] ->
{trigger, _} = clean_trigger(trigger)
if Map.has_key?(triggers, trigger) do
jaro = Enum.find(triggers[trigger], fn({string, idx}) -> String.jaro_distance(content, string) > 0.9 end)
if jaro do
{string, idx} = jaro
{:error, {:jaro, string, idx}}
else
File.write!(directory() <> "/" <> trigger <> ".txt", content<>"\n", [:append])
idx = Enum.count(triggers[trigger])+1
{:ok, idx}
end
else
{:error, :notxt}
end
_ -> {:error, :badarg}
end
end
# fixme: this is definitely the ugliest thing i've ever done
defp clean_trigger(trigger) do
[trigger | opts] = trigger
|> String.strip
|> String.split(" ", parts: 2)
trigger = trigger
|> String.downcase
|> :unicode.characters_to_nfd_binary()
|> String.replace(~r/[^a-z0-9._]/, "")
|> String.trim(".")
|> String.trim("_")
{trigger, opts}
end
def format_line(line, prefix, msg) do
prefix = unless(prefix, do: "", else: prefix)
prefix <> line
|> String.split("\\\\")
|> Enum.map(fn(line) ->
String.split(line, "\\\\\\\\")
end)
|> List.flatten()
|> Enum.map(fn(line) ->
String.trim(line)
|> Tmpl.render(msg)
end)
end
def directory() do
Application.get_env(:lsg, :data_path) <> "/irc.txt/"
end
defp can_write?(%{rw: rw?, locks: locks}, msg = %{channel: nil, sender: sender}, trigger) do
admin? = IRC.admin?(sender)
locked? = case :dets.lookup(locks, trigger) do
[{trigger}] -> true
_ -> false
end
unlocked? = if rw? == false, do: false, else: !locked?
can? = unlocked? || admin?
if !can? do
reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé"
msg.replyfun.("#{sender.nick}: permission refusée (#{reason})")
end
can?
end
defp can_write?(state = %__MODULE__{rw: rw?, locks: locks}, msg = %{channel: channel, sender: sender}, trigger) do
admin? = IRC.admin?(sender)
operator? = IRC.UserTrack.operator?(msg.network, channel, sender.nick)
locked? = case :dets.lookup(locks, trigger) do
[{trigger}] -> true
_ -> false
end
unlocked? = if rw? == false, do: false, else: !locked?
can? = admin? || operator? || unlocked?
if !can? do
reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé"
msg.replyfun.("#{sender.nick}: permission refusée (#{reason})")
end
can?
end
end
diff --git a/lib/lsg_irc/txt_plugin/markov.ex b/lib/lsg_irc/txt_plugin/markov.ex
index 311138c..2e30dfa 100644
--- a/lib/lsg_irc/txt_plugin/markov.ex
+++ b/lib/lsg_irc/txt_plugin/markov.ex
@@ -1,9 +1,9 @@
-defmodule LSG.IRC.TxtPlugin.Markov do
+defmodule Nola.IRC.TxtPlugin.Markov do
@type state :: any()
@callback start_link() :: {:ok, state()}
@callback reload(content :: Map.t, state()) :: any()
@callback sentence(state()) :: {:ok, String.t} | {:error, String.t}
@callback complete_sentence(state()) :: {:ok, String.t} | {:error, String.t}
end
diff --git a/lib/lsg_irc/txt_plugin/markov_native.ex b/lib/lsg_irc/txt_plugin/markov_native.ex
index 524e860..4c403c2 100644
--- a/lib/lsg_irc/txt_plugin/markov_native.ex
+++ b/lib/lsg_irc/txt_plugin/markov_native.ex
@@ -1,33 +1,33 @@
-defmodule LSG.IRC.TxtPlugin.MarkovNative do
- @behaviour LSG.IRC.TxtPlugin.Markov
+defmodule Nola.IRC.TxtPlugin.MarkovNative do
+ @behaviour Nola.IRC.TxtPlugin.Markov
def start_link() do
ExChain.MarkovModel.start_link()
end
def reload(data, markov) do
data = data
|> Enum.map(fn({_, data}) ->
for {line, _idx} <- data, do: line
end)
|> List.flatten
ExChain.MarkovModel.populate_model(markov, data)
:ok
end
def sentence(markov) do
case ExChain.SentenceGenerator.create_filtered_sentence(markov) do
{:ok, line, _, _} -> {:ok, line}
error -> error
end
end
def complete_sentence(sentence, markov) do
case ExChain.SentenceGenerator.complete_sentence(markov, sentence) do
{line, _} -> {:ok, line}
error -> error
end
end
end
diff --git a/lib/lsg_irc/txt_plugin/markov_py_markovify.ex b/lib/lsg_irc/txt_plugin/markov_py_markovify.ex
index a3838cd..cda7853 100644
--- a/lib/lsg_irc/txt_plugin/markov_py_markovify.ex
+++ b/lib/lsg_irc/txt_plugin/markov_py_markovify.ex
@@ -1,39 +1,39 @@
-defmodule LSG.IRC.TxtPlugin.MarkovPyMarkovify do
+defmodule Nola.IRC.TxtPlugin.MarkovPyMarkovify do
def start_link() do
{:ok, nil}
end
def reload(_data, _markov) do
:ok
end
def sentence(_) do
{:ok, run()}
end
def complete_sentence(sentence, _) do
{:ok, run([sentence])}
end
defp run(args \\ []) do
{binary, script} = script()
- args = [script, Path.expand(LSG.IRC.TxtPlugin.directory()) | args]
+ args = [script, Path.expand(Nola.IRC.TxtPlugin.directory()) | args]
IO.puts "Args #{inspect args}"
case MuonTrap.cmd(binary, args) do
{response, 0} -> response
{response, code} -> "error #{code}: #{response}"
end
end
defp script() do
default_script = to_string(:code.priv_dir(:lsg)) <> "/irc/txt/markovify.py"
- env = Application.get_env(:lsg, LSG.IRC.TxtPlugin, [])
+ env = Application.get_env(:lsg, Nola.IRC.TxtPlugin, [])
|> Keyword.get(:py_markovify, [])
{Keyword.get(env, :python, "python3"), Keyword.get(env, :script, default_script)}
end
end
diff --git a/lib/lsg_irc/untappd_plugin.ex b/lib/lsg_irc/untappd_plugin.ex
index 69e4be6..50b0c4d 100644
--- a/lib/lsg_irc/untappd_plugin.ex
+++ b/lib/lsg_irc/untappd_plugin.ex
@@ -1,66 +1,66 @@
-defmodule LSG.IRC.UntappdPlugin do
+defmodule Nola.IRC.UntappdPlugin do
def irc_doc() do
"""
# [Untappd](https://untappd.com)
* `!beer <beer name>` Information about the first beer matching `<beer name>`
* `?beer <beer name>` List the 10 firsts beer matching `<beer name>`
_Note_: The best way to search is always "Brewery Name + Beer Name", such as "Dogfish 60 Minute".
Link your Untappd account to the bot (for automated checkins on [alcoolog](#alcoolog), ...) with the `enable-untappd` command, in private.
"""
end
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:beer", [plugin: __MODULE__])
{:ok, %{}}
end
def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %{type: :bang, args: args}}}, state) do
case Untappd.search_beer(Enum.join(args, " "), limit: 1) do
{:ok, %{"response" => %{"beers" => %{"count" => count, "items" => [result | _]}}}} ->
%{"beer" => beer, "brewery" => brewery} = result
description = Map.get(beer, "beer_description")
|> String.replace("\n", " ")
|> String.replace("\r", " ")
|> String.trim()
beer_s = "#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")} - #{Map.get(beer, "beer_abv")}°"
city = get_in(brewery, ["location", "brewery_city"])
location = [Map.get(brewery, "brewery_type"), city, Map.get(brewery, "country_name")]
|> Enum.filter(fn(x) -> x end)
|> Enum.join(", ")
extra = "#{Map.get(beer, "beer_style")} - IBU: #{Map.get(beer, "beer_ibu")} - #{location}"
m.replyfun.([beer_s, extra, description])
err ->
m.replyfun.("Error")
end
{:noreply, state}
end
def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %{type: :query, args: args}}}, state) do
case Untappd.search_beer(Enum.join(args, " ")) do
{:ok, %{"response" => %{"beers" => %{"count" => count, "items" => results}}}} ->
beers = for %{"beer" => beer, "brewery" => brewery} <- results do
"#{Map.get(brewery, "brewery_name")}: #{Map.get(beer, "beer_name")} - #{Map.get(beer, "beer_abv")}°"
end
|> Enum.intersperse(", ")
|> Enum.join("")
m.replyfun.("#{count}. #{beers}")
err ->
m.replyfun.("Error")
end
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
end
diff --git a/lib/lsg_irc/user_mention_plugin.ex b/lib/lsg_irc/user_mention_plugin.ex
index ca743c4..eb230fd 100644
--- a/lib/lsg_irc/user_mention_plugin.ex
+++ b/lib/lsg_irc/user_mention_plugin.ex
@@ -1,52 +1,52 @@
-defmodule LSG.IRC.UserMentionPlugin do
+defmodule Nola.IRC.UserMentionPlugin do
@moduledoc """
# mention
* **@`<nick>` `<message>`**: notifie si possible le nick immédiatement via Telegram, SMS, ou équivalent à `!tell`.
"""
require Logger
def short_irc_doc, do: false
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "triggers", plugin: __MODULE__)
{:ok, nil}
end
def handle_info({:irc, :trigger, nick, message = %IRC.Message{sender: sender, account: account, network: network, channel: channel, trigger: %IRC.Trigger{type: :at, args: content}}}, state) do
nick = nick
|> String.trim(":")
|> String.trim(",")
target = IRC.Account.find_always_by_nick(network, channel, nick)
if target do
telegram = IRC.Account.get_meta(target, "telegram-id")
sms = IRC.Account.get_meta(target, "sms-number")
text = "#{channel} <#{sender.nick}> #{Enum.join(content, " ")}"
cond do
telegram ->
- LSG.Telegram.send_message(telegram, "`#{channel}` <**#{sender.nick}**> #{Enum.join(content, " ")}")
+ Nola.Telegram.send_message(telegram, "`#{channel}` <**#{sender.nick}**> #{Enum.join(content, " ")}")
sms ->
- case LSG.IRC.SmsPlugin.send_sms(sms, text) do
+ case Nola.IRC.SmsPlugin.send_sms(sms, text) do
{:error, code} -> message.replyfun("#{sender.nick}: erreur #{code} (sms)")
end
true ->
- LSG.IRC.TellPlugin.tell(message, nick, content)
+ Nola.IRC.TellPlugin.tell(message, nick, content)
end
else
message.replyfun.("#{nick} m'est inconnu")
end
{:noreply, state}
end
def handle_info(_, state) do
{:noreply, state}
end
end
diff --git a/lib/lsg_irc/wikipedia_plugin.ex b/lib/lsg_irc/wikipedia_plugin.ex
index 618eb66..3202e13 100644
--- a/lib/lsg_irc/wikipedia_plugin.ex
+++ b/lib/lsg_irc/wikipedia_plugin.ex
@@ -1,90 +1,90 @@
-defmodule LSG.IRC.WikipediaPlugin do
+defmodule Nola.IRC.WikipediaPlugin do
require Logger
@moduledoc """
# wikipédia
* **!wp `<recherche>`**: retourne le premier résultat de la `<recherche>` Wikipedia
* **!wp**: un article Wikipédia au hasard
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:wp", [plugin: __MODULE__])
{:ok, nil}
end
def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do
irc_random(message)
{:noreply, state}
end
def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do
irc_search(Enum.join(args, " "), message)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
defp irc_search("", message), do: irc_random(message)
defp irc_search(query, message) do
params = %{
"action" => "query",
"list" => "search",
"srsearch" => String.strip(query),
"srlimit" => 1,
}
case query_wikipedia(params) do
{:ok, %{"query" => %{"search" => [item | _]}}} ->
title = item["title"]
url = "https://fr.wikipedia.org/wiki/" <> String.replace(title, " ", "_")
msg = "Wikipédia: #{title} — #{url}"
message.replyfun.(msg)
_ ->
nil
end
end
defp irc_random(message) do
params = %{
"action" => "query",
"generator" => "random",
"grnnamespace" => 0,
"prop" => "info"
}
case query_wikipedia(params) do
{:ok, %{"query" => %{"pages" => map = %{}}}} ->
[{_, item}] = Map.to_list(map)
title = item["title"]
url = "https://fr.wikipedia.org/wiki/" <> String.replace(title, " ", "_")
msg = "Wikipédia: #{title} — #{url}"
message.replyfun.(msg)
_ ->
nil
end
end
defp query_wikipedia(params) do
url = "https://fr.wikipedia.org/w/api.php"
params = params
|> Map.put("format", "json")
|> Map.put("utf8", "")
case HTTPoison.get(url, [], params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} -> Jason.decode(body)
{:ok, %HTTPoison.Response{status_code: 400, body: body}} ->
Logger.error "Wikipedia HTTP 400: #{inspect body}"
{:error, "http 400"}
error ->
Logger.error "Wikipedia http error: #{inspect error}"
{:error, "http client error"}
end
end
end
diff --git a/lib/lsg_irc/wolfram_alpha_plugin.ex b/lib/lsg_irc/wolfram_alpha_plugin.ex
index c07f659..b553e63 100644
--- a/lib/lsg_irc/wolfram_alpha_plugin.ex
+++ b/lib/lsg_irc/wolfram_alpha_plugin.ex
@@ -1,47 +1,47 @@
-defmodule LSG.IRC.WolframAlphaPlugin do
+defmodule Nola.IRC.WolframAlphaPlugin do
use GenServer
require Logger
@moduledoc """
# wolfram alpha
* **`!wa <requête>`** lance `<requête>` sur WolframAlpha
"""
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:wa", [plugin: __MODULE__])
{:ok, nil}
end
def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: query}}}, state) do
query = Enum.join(query, " ")
params = %{
"appid" => Keyword.get(Application.get_env(:lsg, :wolframalpha, []), :app_id, "NO_APP_ID"),
"units" => "metric",
"i" => query
}
url = "https://www.wolframalpha.com/input/?i=" <> URI.encode(query)
case HTTPoison.get("http://api.wolframalpha.com/v1/result", [], [params: params]) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
m.replyfun.(["#{query} -> #{body}", url])
{:ok, %HTTPoison.Response{status_code: code, body: body}} ->
error = case {code, body} do
{501, b} -> "input invalide: #{body}"
{code, error} -> "erreur #{code}: #{body || ""}"
end
m.replyfun.("wa: #{error}")
{:error, %HTTPoison.Error{reason: reason}} ->
m.replyfun.("wa: erreur http: #{to_string(reason)}")
_ ->
m.replyfun.("wa: erreur http")
end
{:noreply, state}
end
end
diff --git a/lib/lsg_irc/youtube_plugin.ex b/lib/lsg_irc/youtube_plugin.ex
index 49fc31c..3d2acfb 100644
--- a/lib/lsg_irc/youtube_plugin.ex
+++ b/lib/lsg_irc/youtube_plugin.ex
@@ -1,104 +1,104 @@
-defmodule LSG.IRC.YouTubePlugin do
+defmodule Nola.IRC.YouTubePlugin do
require Logger
@moduledoc """
# youtube
* **!yt `<recherche>`**, !youtube `<recherche>`: retourne le premier résultat de la `<recherche>` YouTube
"""
defstruct client: nil
def irc_doc, do: @moduledoc
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
for t <- ["trigger:yt", "trigger:youtube"], do: {:ok, _} = Registry.register(IRC.PubSub, t, [plugin: __MODULE__])
{:ok, %__MODULE__{}}
end
def handle_info({:irc, :trigger, _, message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: args}}}, state) do
irc_search(Enum.join(args, " "), message)
{:noreply, state}
end
def handle_info(info, state) do
{:noreply, state}
end
defp irc_search(query, message) do
case search(query) do
{:ok, %{"items" => [item | _]}} ->
url = "https://youtube.com/watch?v=" <> item["id"]
snippet = item["snippet"]
duration = item["contentDetails"]["duration"] |> String.replace("PT", "") |> String.downcase
date = snippet["publishedAt"]
|> DateTime.from_iso8601()
|> elem(1)
|> Timex.format("{relative}", :relative)
|> elem(1)
info_line = "— #{duration} — uploaded by #{snippet["channelTitle"]} — #{date}"
<> " — #{item["statistics"]["viewCount"]} views, #{item["statistics"]["likeCount"]} likes,"
<> " #{item["statistics"]["dislikeCount"]} dislikes"
message.replyfun.("#{snippet["title"]} — #{url}")
message.replyfun.(info_line)
{:error, error} ->
message.replyfun.("Erreur YouTube: "<>error)
_ ->
nil
end
end
defp search(query) do
query = query
|> String.strip
key = Application.get_env(:lsg, :youtube)[:api_key]
params = %{
"key" => key,
"maxResults" => 1,
"part" => "id",
"safeSearch" => "none",
"type" => "video",
"q" => query,
}
url = "https://www.googleapis.com/youtube/v3/search"
case HTTPoison.get(url, [], params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, json} = Jason.decode(body)
item = List.first(json["items"])
if item do
video_id = item["id"]["videoId"]
params = %{
"part" => "snippet,contentDetails,statistics",
"id" => video_id,
"key" => key
}
headers = []
options = [params: params]
case HTTPoison.get("https://www.googleapis.com/youtube/v3/videos", [], options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
Jason.decode(body)
{:ok, %HTTPoison.Response{status_code: code, body: body}} ->
Logger.error "YouTube HTTP #{code}: #{inspect body}"
{:error, "http #{code}"}
error ->
Logger.error "YouTube http error: #{inspect error}"
:error
end
else
:error
end
{:ok, %HTTPoison.Response{status_code: code, body: body}} ->
Logger.error "YouTube HTTP #{code}: #{inspect body}"
{:error, "http #{code}"}
error ->
Logger.error "YouTube http error: #{inspect error}"
:error
end
end
end
diff --git a/lib/lsg_matrix/matrix.ex b/lib/lsg_matrix/matrix.ex
index 49da6b2..9334816 100644
--- a/lib/lsg_matrix/matrix.ex
+++ b/lib/lsg_matrix/matrix.ex
@@ -1,169 +1,169 @@
-defmodule LSG.Matrix do
+defmodule Nola.Matrix do
require Logger
alias Polyjuice.Client
@behaviour MatrixAppService.Adapter.Room
@behaviour MatrixAppService.Adapter.Transaction
@behaviour MatrixAppService.Adapter.User
@env Mix.env
def dets(part) do
- (LSG.data_path() <> "/matrix-#{to_string(part)}.dets") |> String.to_charlist()
+ (Nola.data_path() <> "/matrix-#{to_string(part)}.dets") |> String.to_charlist()
end
def setup() do
{:ok, _} = :dets.open_file(dets(:rooms), [])
{:ok, _} = :dets.open_file(dets(:room_aliases), [])
{:ok, _} = :dets.open_file(dets(:users), [])
:ok
end
def myself?("@_dev:random.sh"), do: true
def myself?("@_bot:random.sh"), do: true
def myself?("@_dev."<>_), do: true
def myself?("@_bot."<>_), do: true
def myself?(_), do: false
def mxc_to_http(mxc = "mxc://"<>_) do
uri = URI.parse(mxc)
%URI{uri | scheme: "https", path: "/_matrix/media/r0/download/#{uri.authority}#{uri.path}"}
|> URI.to_string()
end
def get_or_create_matrix_user(id) do
if mxid = lookup_user(id) do
mxid
else
opts = [
type: "m.login.application_service",
inhibit_login: true,
device_id: "APP_SERVICE",
initial_device_display_name: "Application Service",
username: if(@env == :dev, do: "_dev.#{id}", else: "_bot.#{id}")
]
Logger.debug("Registering user for #{id}")
{:ok, %{"user_id" => mxid}} = Polyjuice.Client.LowLevel.register(client(), opts)
:dets.insert(dets(:users), {id, mxid})
end
end
def lookup_user(id) do
case :dets.lookup(dets(:users), id) do
[{_, matrix_id}] -> matrix_id
_ -> nil
end
end
def user_name("@"<>name) do
[username, _] = String.split(name, ":", parts: 2)
username
end
def application_childs() do
import Supervisor.Spec
[
- supervisor(LSG.Matrix.Room.Supervisor, [], [name: IRC.PuppetConnection.Supervisor]),
+ supervisor(Nola.Matrix.Room.Supervisor, [], [name: IRC.PuppetConnection.Supervisor]),
]
end
def after_start() do
rooms = :dets.foldl(fn({id, _, _, _}, acc) -> [id | acc] end, [], dets(:rooms))
- for room <- rooms, do: LSG.Matrix.Room.start(room)
+ for room <- rooms, do: Nola.Matrix.Room.start(room)
end
def lookup_room(room) do
case :dets.lookup(dets(:rooms), room) do
[{_, network, channel, opts}] -> {:ok, Map.merge(opts, %{network: network, channel: channel})}
_ -> {:error, :no_such_room}
end
end
def lookup_room_alias(room_alias) do
case :dets.lookup(dets(:room_aliases), room_alias) do
[{_, room_id}] -> {:ok, room_id}
_ -> {:error, :no_such_room_alias}
end
end
def lookup_or_create_room(room_alias) do
case lookup_room_alias(room_alias) do
{:ok, room_id} -> {:ok, room_id}
{:error, :no_such_room_alias} -> create_room(room_alias)
end
end
def create_room(room_alias) do
Logger.debug("Matrix: creating room #{inspect room_alias}")
localpart = localpart(room_alias)
with {:ok, network, channel} <- extract_network_channel_from_localpart(localpart),
%IRC.Connection{} <- IRC.Connection.get_network(network, channel),
room = [visibility: :public, room_alias_name: localpart, name: if(network == "random", do: channel, else: "#{network}/#{channel}")],
{:ok, %{"room_id" => room_id}} <- Client.Room.create_room(client(), room) do
Logger.info("Matrix: created room #{room_alias} #{room_id}")
:dets.insert(dets(:rooms), {room_id, network, channel, %{}})
:dets.insert(dets(:room_aliases), {room_alias, room_id})
{:ok, room_id}
else
nil -> {:error, :no_such_network_channel}
error -> error
end
end
def localpart(room_alias) do
[<<"#", localpart :: binary>>, _] = String.split(room_alias, ":", parts: 2)
localpart
end
def extract_network_channel_from_localpart(localpart) do
s = localpart
|> String.replace("dev.", "")
|> String.split("/", parts: 2)
case s do
[network, channel] -> {:ok, network, channel}
[channel] -> {:ok, "random", channel}
_ -> {:error, :invalid_localpart}
end
end
@impl MatrixAppService.Adapter.Room
def query_alias(room_alias) do
case lookup_or_create_room(room_alias) do
{:ok, room_id} ->
- LSG.Matrix.Room.start(room_id)
+ Nola.Matrix.Room.start(room_id)
:ok
error -> error
end
end
@impl MatrixAppService.Adapter.Transaction
def new_event(event = %MatrixAppService.Event{}) do
Logger.debug("New matrix event: #{inspect event}")
if event.room_id do
- LSG.Matrix.Room.start_and_send_matrix_event(event.room_id, event)
+ Nola.Matrix.Room.start_and_send_matrix_event(event.room_id, event)
end
:noop
end
@impl MatrixAppService.Adapter.User
def query_user(user_id) do
Logger.warn("Matrix lookup user: #{inspect user_id}")
:error
end
def client(opts \\ []) do
base_url = Application.get_env(:matrix_app_service, :base_url)
access_token = Application.get_env(:matrix_app_service, :access_token)
default_opts = [
access_token: access_token,
device_id: "APP_SERVICE",
application_service: true,
user_id: nil
]
opts = Keyword.merge(default_opts, opts)
Polyjuice.Client.LowLevel.create(base_url, opts)
end
end
diff --git a/lib/lsg_matrix/plug.ex b/lib/lsg_matrix/plug.ex
index c0c027f..c64ed11 100644
--- a/lib/lsg_matrix/plug.ex
+++ b/lib/lsg_matrix/plug.ex
@@ -1,25 +1,25 @@
-defmodule LSG.Matrix.Plug do
+defmodule Nola.Matrix.Plug do
defmodule Auth do
def init(state) do
state
end
def call(conn, _) do
hs = Application.get_env(:matrix_app_service, :homeserver_token)
MatrixAppServiceWeb.AuthPlug.call(conn, hs)
end
end
defmodule SetConfig do
def init(state) do
state
end
def call(conn, _) do
config = Application.get_all_env(:matrix_app_service)
MatrixAppServiceWeb.SetConfigPlug.call(conn, config)
end
end
end
diff --git a/lib/lsg_matrix/room.ex b/lib/lsg_matrix/room.ex
index 72b02c4..c790760 100644
--- a/lib/lsg_matrix/room.ex
+++ b/lib/lsg_matrix/room.ex
@@ -1,196 +1,196 @@
-defmodule LSG.Matrix.Room do
+defmodule Nola.Matrix.Room do
require Logger
- alias LSG.Matrix
+ 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: {LSG.Matrix.Room, :start_link, [room_id]}, restart: :transient}
+ spec = %{id: room_id, start: {Nola.Matrix.Room, :start_link, [room_id]}, restart: :transient}
DynamicSupervisor.start_child(__MODULE__, spec)
end
@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(IRC.PubSub, "#{state.network}:events", plugin: __MODULE__)
for t <- ["messages", "triggers", "outputs", "events"] do
{:ok, _} = Registry.register(IRC.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 ?
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
IRC.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 = IRC.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/lsg_telegram/room.ex b/lib/lsg_telegram/room.ex
index 4e86382..794cca3 100644
--- a/lib/lsg_telegram/room.ex
+++ b/lib/lsg_telegram/room.ex
@@ -1,188 +1,188 @@
-defmodule LSG.TelegramRoom do
+defmodule Nola.TelegramRoom do
require Logger
@behaviour Telegram.ChatBot
alias Telegram.Api
@couch "bot-telegram-rooms"
def rooms(), do: rooms(:with_docs)
@spec rooms(:with_docs | :ids) :: [Map.t | integer( )]
def rooms(:with_docs) do
case Couch.get(@couch, :all_docs, include_docs: true) do
{:ok, %{"rows" => rows}} -> {:ok, for(%{"doc" => doc} <- rows, do: doc)}
error = {:error, _} -> error
end
end
def rooms(:ids) do
case Couch.get(@couch, :all_docs) do
{:ok, %{"rows" => rows}} -> {:ok, for(%{"id" => id} <- rows, do: id)}
error = {:error, _} -> error
end
end
def room(id, opts \\ []) do
Couch.get(@couch, id, opts)
end
# TODO: Create couch
def setup() do
:ok
end
def after_start() do
- for id <- room(:ids), do: Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(LSG.Telegram, Integer.parse(id) |> elem(0))
+ for id <- room(:ids), do: Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(Nola.Telegram, Integer.parse(id) |> elem(0))
end
@impl Telegram.ChatBot
def init(id) when is_integer(id) and id < 0 do
token = Keyword.get(Application.get_env(:lsg, :telegram, []), :key)
{:ok, chat} = Api.request(token, "getChat", chat_id: id)
Logger.metadata(transport: :telegram, id: id, telegram_room_id: id)
tg_room = case room(id) do
{:ok, tg_room = %{"network" => _net, "channel" => _chan}} -> tg_room
{:error, :not_found} ->
[net, chan] = String.split(chat["title"], "/", parts: 2)
{net, chan} = case IRC.Connection.get_network(net, chan) do
%IRC.Connection{} -> {net, chan}
_ -> {nil, nil}
end
{:ok, _id, _rev} = Couch.post(@couch, %{"_id" => id, "network" => net, "channel" => nil})
{:ok, tg_room} = room(id)
tg_room
end
%{"network" => net, "channel" => chan} = tg_room
Logger.info("Starting ChatBot for room #{id} \"#{chat["title"]}\" #{inspect tg_room}")
irc_plumbed = if net && chan do
{:ok, _} = Registry.register(IRC.PubSub, "#{net}/#{chan}:messages", plugin: __MODULE__)
{:ok, _} = Registry.register(IRC.PubSub, "#{net}/#{chan}:triggers", plugin: __MODULE__)
{:ok, _} = Registry.register(IRC.PubSub, "#{net}/#{chan}:outputs", plugin: __MODULE__)
true
else
Logger.warn("Did not found telegram match for #{id} \"#{chat["title"]}\"")
false
end
{:ok, %{id: id, net: net, chan: chan, irc: irc_plumbed}}
end
def init(id) do
Logger.error("telegram_room: bad id (not room id)", transport: :telegram, id: id, telegram_room_id: id)
:ignoree
end
defp find_or_create_meta_account(from = %{"id" => user_id}, state) do
if account = IRC.Account.find_meta_account("telegram-id", user_id) do
account
else
first_name = Map.get(from, "first_name")
last_name = Map.get(from, "last_name")
name = [first_name, last_name]
|> Enum.filter(& &1)
|> Enum.join(" ")
username = Map.get(from, "username", first_name)
account = username
|> IRC.Account.new_account()
|> IRC.Account.update_account_name(name)
|> IRC.Account.put_meta("telegram-id", user_id)
Logger.info("telegram_room: created account #{account.id} for telegram user #{user_id}")
account
end
end
def handle_update(%{"message" => %{"from" => from = %{"id" => user_id}, "text" => text}}, _token, state) do
account = find_or_create_meta_account(from, state)
connection = IRC.Connection.get_network(state.net)
IRC.send_message_as(account, state.net, state.chan, text, true)
{:ok, state}
end
def handle_update(data = %{"message" => %{"from" => from = %{"id" => user_id}, "location" => %{"latitude" => lat, "longitude" => lon}}}, _token, state) do
account = find_or_create_meta_account(from, state)
connection = IRC.Connection.get_network(state.net)
IRC.send_message_as(account, state.net, state.chan, "@ #{lat}, #{lon}", true)
{:ok, state}
end
for type <- ~w(photo voice video document animation) do
def handle_update(data = %{"message" => %{unquote(type) => _}}, token, state) do
upload(unquote(type), data, token, state)
end
end
def handle_update(update, token, state) do
{:ok, state}
end
def handle_info({:irc, _, _, message}, state) do
handle_info({:irc, nil, message}, state)
end
def handle_info({:irc, _, message = %IRC.Message{sender: %{nick: nick}, text: text}}, state) do
if Map.get(message.meta, :from) == self() do
else
body = if Map.get(message.meta, :self), do: text, else: "<#{nick}> #{text}"
- LSG.Telegram.send_message(state.id, body)
+ Nola.Telegram.send_message(state.id, body)
end
{:ok, state}
end
def handle_info(info, state) do
Logger.info("UNhandled #{inspect info}")
{:ok, state}
end
defp upload(_type, %{"message" => m = %{"chat" => %{"id" => chat_id}, "from" => from = %{"id" => user_id}}}, token, state) do
account = find_or_create_meta_account(from, state)
if account do
{content, type} = cond do
m["photo"] -> {m["photo"], "photo"}
m["voice"] -> {m["voice"], "voice message"}
m["video"] -> {m["video"], "video"}
m["document"] -> {m["document"], "file"}
m["animation"] -> {m["animation"], "gif"}
end
file = if is_list(content) && Enum.count(content) > 1 do
Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2)
|> List.first()
else
content
end
file_id = file["file_id"]
file_unique_id = file["file_unique_id"]
text = if(m["caption"], do: m["caption"] <> " ", else: "")
spawn(fn() ->
with \
{:ok, file} <- Telegram.Api.request(token, "getFile", file_id: file_id),
path = "https://api.telegram.org/file/bot#{token}/#{file["file_path"]}",
{:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path),
<<smol_body::binary-size(20), _::binary>> = body,
- {:ok, magic} <- GenMagic.Pool.perform(LSG.GenMagic, {:bytes, smol_body}),
+ {:ok, magic} <- GenMagic.Pool.perform(Nola.GenMagic, {:bytes, smol_body}),
bucket = Application.get_env(:lsg, :s3, []) |> Keyword.get(:bucket),
ext = Path.extname(file["file_path"]),
s3path = "#{account.id}/#{file_unique_id}#{ext}",
s3req = ExAws.S3.put_object(bucket, s3path, body, acl: :public_read, content_type: magic.mime_type),
{:ok, _} <- ExAws.request(s3req)
do
- path = LSGWeb.Router.Helpers.url(LSGWeb.Endpoint) <> "/files/#{s3path}"
+ path = NolaWeb.Router.Helpers.url(NolaWeb.Endpoint) <> "/files/#{s3path}"
txt = "#{type}: #{text}#{path}"
connection = IRC.Connection.get_network(state.net)
IRC.send_message_as(account, state.net, state.chan, txt, true)
else
error ->
Telegram.Api.request(token, "sendMessage", chat_id: chat_id, text: "File upload failed, sorry.")
Logger.error("Failed upload from Telegram: #{inspect error}")
end
end)
{:ok, state}
end
end
end
diff --git a/lib/lsg_telegram/telegram.ex b/lib/lsg_telegram/telegram.ex
index 748a456..ef5c3b8 100644
--- a/lib/lsg_telegram/telegram.ex
+++ b/lib/lsg_telegram/telegram.ex
@@ -1,233 +1,233 @@
-defmodule LSG.Telegram do
+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(:lsg, :telegram, []), :key)
- Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(LSG.Telegram, id)
+ 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} = LSG.TelegramRoom.init(chat_id)
+ {: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 = IRC.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} = LSG.TelegramRoom.handle_update(update, token, room_state)
+ {: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 = IRC.Account.find_meta_account("telegram-validation-code", String.downcase(key))
text = if account do
net = IRC.Account.get_meta(account, "telegram-validation-target")
IRC.Account.put_meta(account, "telegram-id", m["chat"]["id"])
IRC.Account.put_meta(account, "telegram-username", m["chat"]["username"])
IRC.Account.put_meta(account, "telegram-username", m["chat"]["username"])
IRC.Account.delete_meta(account, "telegram-validation-code")
IRC.Account.delete_meta(account, "telegram-validation-target")
IRC.Connection.broadcast_message(net, account, "Telegram #{m["chat"]["username"]} account added!")
"Yay! Linked to account **#{account.name}**."
else
"Token invalid"
end
send_message(m["chat"]["id"], text)
{: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 = IRC.Account.find_meta_account("telegram-id", chat_id)
if account do
target = case String.split(target, "/") do
["everywhere"] -> IRC.Membership.of_account(account)
[net, chan] -> [{net, chan}]
end
Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "Processing...", reply_markup: %{})
{content, type} = cond do
op["photo"] -> {op["photo"], ""}
op["voice"] -> {op["voice"], " a voice message"}
op["video"] -> {op["video"], ""}
op["document"] -> {op["document"], ""}
op["animation"] -> {op["animation"], ""}
end
file = if is_list(content) && Enum.count(content) > 1 do
Enum.sort_by(content, fn(p) -> p["file_size"] end, &>=/2)
|> List.first()
else
content
end
file_id = file["file_id"]
file_unique_id = file["file_unique_id"]
text = if(op["caption"], do: ": "<> op["caption"] <> "", else: "")
resend = %{"inline_keyboard" => [ [%{"text" => "re-share", "callback_data" => "resend"}] ]}
spawn(fn() ->
with \
{:ok, file} <- Telegram.Api.request(t, "getFile", file_id: file_id),
path = "https://api.telegram.org/file/bot#{t}/#{file["file_path"]}",
{:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(path),
<<smol_body::binary-size(20), _::binary>> = body,
- {:ok, magic} <- GenMagic.Pool.perform(LSG.GenMagic, {:bytes, smol_body}),
+ {:ok, magic} <- GenMagic.Pool.perform(Nola.GenMagic, {:bytes, smol_body}),
bucket = Application.get_env(:lsg, :s3, []) |> Keyword.get(:bucket),
ext = Path.extname(file["file_path"]),
s3path = "#{account.id}/#{file_unique_id}#{ext}",
Telegram.Api.request(t, "editMessageText", chat_id: chat_id, message_id: m_id, text: "*Uploading...*", reply_markup: %{}, 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 = LSGWeb.Router.Helpers.url(LSGWeb.Endpoint) <> "/files/#{s3path}"
+ 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 = IRC.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} = LSG.TelegramRoom.handle_info(info, room_state)
+ {: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 = IRC.Account.find_meta_account("telegram-id", id)
if account do
text = if(m["text"], do: m["text"], else: nil)
targets = IRC.Membership.of_account(account)
|> Enum.map(fn({net, chan}) -> "#{net}/#{chan}" end)
|> Enum.map(fn(i) -> %{"text" => i, "callback_data" => "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/lsg_web/channels/user_socket.ex b/lib/lsg_web/channels/user_socket.ex
index 691a26c..eadd4e0 100644
--- a/lib/lsg_web/channels/user_socket.ex
+++ b/lib/lsg_web/channels/user_socket.ex
@@ -1,37 +1,37 @@
-defmodule LSGWeb.UserSocket do
+defmodule NolaWeb.UserSocket do
use Phoenix.Socket
## Channels
- # channel "room:*", LSGWeb.RoomChannel
+ # channel "room:*", NolaWeb.RoomChannel
## Transports
#transport :websocket, Phoenix.Transports.WebSocket
# transport :longpoll, Phoenix.Transports.LongPoll
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error`.
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
def connect(_params, socket) do
{:ok, socket}
end
# Socket id's are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
- # LSGWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
+ # NolaWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
def id(_socket), do: nil
end
diff --git a/lib/lsg_web/components/component.ex b/lib/lsg_web/components/component.ex
index d504129..fff8263 100644
--- a/lib/lsg_web/components/component.ex
+++ b/lib/lsg_web/components/component.ex
@@ -1,44 +1,44 @@
-defmodule LSGWeb.Component do
+defmodule NolaWeb.Component do
use Phoenix.Component
@date_time_default_format "%F %H:%M"
@date_time_formats %{"time-24-with-seconds" => "%H:%M:%S"}
def naive_date_time_utc(assigns = %{at: nil}) do
""
end
def naive_date_time_utc(assigns = %{format: format}) do
assigns = assign(assigns, :format, Map.get(@date_time_formats, format, format))
~H"""
<time class="component"
id={"time-#{:erlang.phash2(@datetime)}"}
phx-hook="NaiveDateTimeUTC"
data-time-format={get_luxon_format(@format)}
datetime={NaiveDateTime.to_iso8601(@datetime)}>
<%= Timex.format!(@datetime, @format, :strftime) %>
</time>
"""
end
def naive_date_time_utc(assigns) do
naive_date_time_utc(assign(assigns, :format, "%F %H:%M"))
end
def get_luxon_format("%H:%M:%S"), do: "TIME_24_WITH_SECONDS"
def nick(assigns = %{self: false}) do
~H"""
<span class="nickname" data-account-id={@account_id} data-user-id={@user_id}>
<%= @nick %>
</span>
"""
end
def nick(assigns = %{self: true}) do
~H"""
<span class="nickname self" data-account-id={@account_id} data-user-id={@user_id}>
You
</span>
"""
end
end
diff --git a/lib/lsg_web/components/event_component.ex b/lib/lsg_web/components/event_component.ex
index fa81d19..8af3c67 100644
--- a/lib/lsg_web/components/event_component.ex
+++ b/lib/lsg_web/components/event_component.ex
@@ -1,43 +1,43 @@
-defmodule LSGWeb.EventComponent do
+defmodule NolaWeb.EventComponent do
use Phoenix.Component
def content(assigns = %{event: %{type: :day_changed}}) do
~H"""
Day changed:
<span class="reason"><%= Date.to_string(@date) %></span>
"""
end
def content(assigns = %{event: %{type: :quit}}) do
~H"""
- <LSGWeb.Component.nick self={@self} nick={@user.nick} user_id={@user.id} account_id={@user.account} />
+ <NolaWeb.Component.nick self={@self} nick={@user.nick} user_id={@user.id} account_id={@user.account} />
has quit:
<span class="reason"><%= @reason %></span>
"""
end
def content(assigns = %{event: %{type: :part}}) do
~H"""
- <LSGWeb.Component.nick self={@self} nick={@user.nick} user_id={@user.id} account_id={@user.account} />
+ <NolaWeb.Component.nick self={@self} nick={@user.nick} user_id={@user.id} account_id={@user.account} />
has left:
<span class="reason"><%= @reason %></span>
"""
end
def content(assigns = %{event: %{type: :nick}}) do
~H"""
<span class="old-nick"><%= @old_nick %></span>
is now known as
- <LSGWeb.Component.nick self={@self} nick={@user.nick} user_id={@user.id} account_id={@user.account} />
+ <NolaWeb.Component.nick self={@self} nick={@user.nick} user_id={@user.id} account_id={@user.account} />
"""
end
def content(assigns = %{event: %{type: :join}}) do
~H"""
- <LSGWeb.Component.nick self={@self} nick={@user.nick} user_id={@user.id} account_id={@user.account} />
+ <NolaWeb.Component.nick self={@self} nick={@user.nick} user_id={@user.id} account_id={@user.account} />
joined
"""
end
end
diff --git a/lib/lsg_web/components/message_component.ex b/lib/lsg_web/components/message_component.ex
index 5997754..5d0386b 100644
--- a/lib/lsg_web/components/message_component.ex
+++ b/lib/lsg_web/components/message_component.ex
@@ -1,12 +1,12 @@
-defmodule LSGWeb.MessageComponent do
+defmodule NolaWeb.MessageComponent do
use Phoenix.Component
def content(assigns) do
~H"""
- <LSGWeb.Component.naive_date_time_utc datetime={@message.at} format="time-24-with-seconds" />
+ <NolaWeb.Component.naive_date_time_utc datetime={@message.at} format="time-24-with-seconds" />
<div class="inline-block font-bold flex-none cursor-default"><%= @message.sender.nick %></div>
<div class="inline-block flex-grow cursor-default"><%= @text %></div>
"""
end
end
diff --git a/lib/lsg_web/context_plug.ex b/lib/lsg_web/context_plug.ex
index aaf851e..ebededa 100644
--- a/lib/lsg_web/context_plug.ex
+++ b/lib/lsg_web/context_plug.ex
@@ -1,92 +1,92 @@
-defmodule LSGWeb.ContextPlug do
+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 = IRC.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, IRC.Account.get(account_id)}
do
account
else
_ -> nil
end
network = Map.get(conn.params, "network")
network = if network == "-", do: nil, else: network
oidc_account = IRC.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
- LSGWeb.reformat_chan(c)
+ NolaWeb.reformat_chan(c)
end
chan_conn = IRC.Connection.get_network(network, chan)
memberships = if account do
IRC.Membership.of_account(account)
end
auth_required = cond do
Keyword.get(opts, :restrict) == :public -> false
account == nil -> true
network == nil -> false
Keyword.get(opts, :restrict) == :logged_in -> false
network && chan ->
!Enum.member?(memberships, {network, chan})
network ->
!Enum.any?(memberships, fn({n, _}) -> n == network end)
end
bot = cond do
network && chan && chan_conn -> chan_conn.nick
network && conns -> conns.nick
true -> nil
end
cond do
account && auth_required ->
conn
|> put_status(404)
|> text("Page not found")
|> halt()
auth_required ->
conn
|> put_status(403)
- |> render(LSGWeb.AlcoologView, "auth.html", bot: bot, no_header: true, network: network)
+ |> 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/lsg_web/controllers/alcoolog_controller.ex b/lib/lsg_web/controllers/alcoolog_controller.ex
index 6542f15..3081762 100644
--- a/lib/lsg_web/controllers/alcoolog_controller.ex
+++ b/lib/lsg_web/controllers/alcoolog_controller.ex
@@ -1,323 +1,323 @@
-defmodule LSGWeb.AlcoologController do
- use LSGWeb, :controller
+defmodule NolaWeb.AlcoologController do
+ use NolaWeb, :controller
require Logger
- plug LSGWeb.ContextPlug when action not in [:token]
- plug LSGWeb.ContextPlug, [restrict: :public] when action in [:token]
+ plug NolaWeb.ContextPlug when action not in [:token]
+ plug NolaWeb.ContextPlug, [restrict: :public] when action in [:token]
def token(conn, %{"token" => token}) do
- case LSG.Token.lookup(token) do
+ case Nola.Token.lookup(token) do
{:ok, {:alcoolog, :index, network, channel}} -> index(conn, nil, network, channel)
err ->
Logger.debug("AlcoologControler: token #{inspect err} invalid")
conn
|> put_status(404)
|> text("Page not found")
end
end
def nick(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
profile_account = IRC.Account.find_always_by_nick(network, nick, nick)
days = String.to_integer(Map.get(params, "days", "180"))
friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
if friend? do
- stats = LSG.IRC.AlcoologPlugin.get_full_statistics(profile_account.id)
- history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- LSG.IRC.AlcoologPlugin.nick_history(profile_account) do
+ stats = Nola.IRC.AlcoologPlugin.get_full_statistics(profile_account.id)
+ history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- Nola.IRC.AlcoologPlugin.nick_history(profile_account) do
%{
at: ts |> DateTime.from_unix!(:millisecond),
points: points,
active: active,
cl: cl,
deg: deg,
type: type,
description: descr,
meta: meta
}
end
history = Enum.sort(history, &(DateTime.compare(&1.at, &2.at) != :lt))
|> IO.inspect()
conn
|> assign(:title, "alcoolog #{nick}")
|> render("user.html", network: network, profile: profile_account, days: days, nick: nick, history: history, stats: stats)
else
conn
|> put_status(404)
|> text("Page not found")
end
end
def nick_stats_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
profile_account = IRC.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
if friend? do
- stats = LSG.IRC.AlcoologPlugin.get_full_statistics(profile_account.id)
+ stats = Nola.IRC.AlcoologPlugin.get_full_statistics(profile_account.id)
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(stats))
else
conn
|> put_status(404)
|> json([])
end
end
def nick_gls_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
profile_account = IRC.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
count = String.to_integer(Map.get(params, "days", "180"))
if friend? do
- data = LSG.IRC.AlcoologPlugin.user_over_time_gl(profile_account, count)
+ data = Nola.IRC.AlcoologPlugin.user_over_time_gl(profile_account, count)
delay = count*((24 * 60)*60)
now = DateTime.utc_now()
start_date = DateTime.utc_now()
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
|> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
|> DateTime.to_date()
|> Date.to_erl()
filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
|> Enum.to_list
|> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
|> Enum.map(&Date.from_erl!(&1))
|> Enum.map(fn(date) ->
%{date: date, gls: Map.get(data, date, 0)}
end)
|> Enum.sort(&(Date.compare(&1.date, &2.date) != :gt))
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(filled))
else
conn
|> put_status(404)
|> json([])
end
end
def nick_volumes_json(conn = %{assigns: %{account: account}}, params = %{"network" => network, "nick" => nick}) do
profile_account = IRC.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
count = String.to_integer(Map.get(params, "days", "180"))
if friend? do
- data = LSG.IRC.AlcoologPlugin.user_over_time(profile_account, count)
+ data = Nola.IRC.AlcoologPlugin.user_over_time(profile_account, count)
delay = count*((24 * 60)*60)
now = DateTime.utc_now()
start_date = DateTime.utc_now()
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
|> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
|> DateTime.to_date()
|> Date.to_erl()
filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
|> Enum.to_list
|> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
|> Enum.map(&Date.from_erl!(&1))
|> Enum.map(fn(date) ->
%{date: date, volumes: Map.get(data, date, 0)}
end)
|> Enum.sort(&(Date.compare(&1.date, &2.date) != :gt))
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(filled))
else
conn
|> put_status(404)
|> json([])
end
end
def nick_log_json(conn = %{assigns: %{account: account}}, %{"network" => network, "nick" => nick}) do
profile_account = IRC.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
if friend? do
- history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- LSG.IRC.AlcoologPlugin.nick_history(profile_account) do
+ history = for {{nick, ts}, points, active, cl, deg, type, descr, meta} <- Nola.IRC.AlcoologPlugin.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} = LSG.IRC.AlcoologPlugin.user_stats(profile_account)
+ {_, active} = Nola.IRC.AlcoologPlugin.user_stats(profile_account)
last = %{last | active: active, at: DateTime.utc_now() |> DateTime.to_iso8601()}
history = history ++ [last]
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(history))
else
conn
|> put_status(404)
|> json([])
end
end
def nick_history_json(conn = %{assigns: %{account: account}}, %{"network" => network, "nick" => nick}) do
profile_account = IRC.Account.find_always_by_nick(network, nick, nick)
friend? = Enum.member?(IRC.Membership.friends(account), profile_account.id)
if friend? do
- history = for {_, date, value} <- LSG.IRC.AlcoologAnnouncerPlugin.log(profile_account) do
+ history = for {_, date, value} <- Nola.IRC.AlcoologAnnouncerPlugin.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, LSGWeb.reformat_chan(channel))
+ 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
- # LSGWeb.reformat_chan(c)
+ # NolaWeb.reformat_chan(c)
# end
# irc_conn = if network do
# IRC.Connection.get_network(network, chan)
# end
# bot = if(irc_conn, do: irc_conn.nick)#
#
# conn
# |> put_status(403)
# |> render("auth.html", network: network, channel: chan, irc_conn: conn, bot: bot)
#end
def index(conn, account, network, channel) do
aday = ((24 * 60)*60)
now = DateTime.utc_now()
before7 = now
|> DateTime.add(-(7*aday), :second)
|> DateTime.to_unix(:millisecond)
before15 = now
|> DateTime.add(-(15*aday), :second)
|> DateTime.to_unix(:millisecond)
before31 = now
|> DateTime.add(-(31*aday), :second)
|> DateTime.to_unix(:millisecond)
#match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
match = [
{{{:_, :"$1"}, :_, :_, :_, :_, :_, :_, :_},
[
{:>, :"$1", {:const, before15}},
], [:"$_"]}
]
# tuple ets: {{nick, date}, volumes, current, nom, commentaire}
members = IRC.Membership.expanded_members_or_friends(account, network, channel)
members_ids = Enum.map(members, fn({account, _, nick}) -> account.id end)
member_names = Enum.reduce(members, %{}, fn({account, _, nick}, acc) -> Map.put(acc, account.id, nick) end)
- drinks = :ets.select(LSG.IRC.AlcoologPlugin.ETS, match)
+ drinks = :ets.select(Nola.IRC.AlcoologPlugin.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 = LSG.IRC.AlcoologPlugin.get_channel_statistics(account, network, channel)
+ stats = Nola.IRC.AlcoologPlugin.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 = LSGWeb.reformat_chan(channel)
+ channel = NolaWeb.reformat_chan(channel)
members = IRC.Membership.expanded_members_or_friends(account, network, channel)
members_ids = Enum.map(members, fn({account, _, nick}) -> account.id end)
member_names = Enum.reduce(members, %{}, fn({account, _, nick}, acc) -> Map.put(acc, account.id, nick) end)
delay = count*((24 * 60)*60)
now = DateTime.utc_now()
start_date = DateTime.utc_now()
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
|> DateTime.add(-delay, :second, Tzdata.TimeZoneDatabase)
|> DateTime.to_date()
|> Date.to_erl()
filled = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
|> Enum.to_list
|> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
|> Enum.map(&Date.from_erl!(&1))
|> Enum.map(fn(date) ->
{date, (for {a, _, _} <- members, into: Map.new, do: {Map.get(member_names, a.id, a.id), 0})}
end)
|> Enum.into(Map.new)
gls = Enum.reduce(members, filled, fn({account, _, _}, gls) ->
- Enum.reduce(LSG.IRC.AlcoologPlugin.user_over_time_gl(account, count), gls, fn({date, gl}, gls) ->
+ Enum.reduce(Nola.IRC.AlcoologPlugin.user_over_time_gl(account, count), gls, fn({date, gl}, gls) ->
u = Map.get(gls, date, %{})
|> Map.put(Map.get(member_names, account.id, account.id), gl)
Map.put(gls, date, u)
end)
end)
dates = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
|> Enum.to_list
|> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
|> Enum.map(&Date.from_erl!(&1))
filled2 = Enum.map(member_names, fn({_, name}) ->
history = (:calendar.date_to_gregorian_days(start_date) .. :calendar.date_to_gregorian_days(DateTime.utc_now |> DateTime.to_date() |> Date.to_erl))
|> Enum.to_list
|> Enum.map(&(:calendar.gregorian_days_to_date(&1)))
|> Enum.map(&Date.from_erl!(&1))
|> Enum.map(fn(date) ->
get_in(gls, [date, name]) #%{date: date, gl: get_in(gls, [date, name])}
end)
if Enum.all?(history, fn(x) -> x == 0 end) do
nil
else
%{name: name, history: history}
end
end)
|> Enum.filter(fn(x) -> x end)
conn
|> put_resp_content_type("application/json")
|> text(Jason.encode!(%{labels: dates, data: filled2}))
end
def minisync(conn, %{"user_id" => user_id, "key" => key, "value" => value}) do
account = IRC.Account.get(user_id)
if account do
- ds = LSG.IRC.AlcoologPlugin.data_state()
- meta = LSG.IRC.AlcoologPlugin.get_user_meta(ds, account.id)
+ ds = Nola.IRC.AlcoologPlugin.data_state()
+ meta = Nola.IRC.AlcoologPlugin.get_user_meta(ds, account.id)
case Float.parse(value) do
{val, _} ->
new_meta = Map.put(meta, String.to_existing_atom(key), val)
- LSG.IRC.AlcoologPlugin.put_user_meta(ds, account.id, new_meta)
+ Nola.IRC.AlcoologPlugin.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/lsg_web/controllers/gpt_controller.ex b/lib/lsg_web/controllers/gpt_controller.ex
index acf9b27..038b235 100644
--- a/lib/lsg_web/controllers/gpt_controller.ex
+++ b/lib/lsg_web/controllers/gpt_controller.ex
@@ -1,33 +1,33 @@
-defmodule LSGWeb.GptController do
- use LSGWeb, :controller
+defmodule NolaWeb.GptController do
+ use NolaWeb, :controller
require Logger
- plug LSGWeb.ContextPlug
+ plug NolaWeb.ContextPlug
def result(conn, params = %{"id" => result_id}) do
- case LSG.IRC.GptPlugin.get_result(result_id) do
+ case Nola.IRC.GptPlugin.get_result(result_id) do
{:ok, result} ->
network = Map.get(params, "network")
- channel = if c = Map.get(params, "chan"), do: LSGWeb.reformat_chan(c)
+ channel = if c = Map.get(params, "chan"), do: NolaWeb.reformat_chan(c)
render(conn, "result.html", network: network, channel: channel, result: result)
{:error, :not_found} ->
conn
|> put_status(404)
|> text("Page not found")
end
end
def prompt(conn, params = %{"id" => prompt_id}) do
- case LSG.IRC.GptPlugin.get_prompt(prompt_id) do
+ case Nola.IRC.GptPlugin.get_prompt(prompt_id) do
{:ok, prompt} ->
network = Map.get(params, "network")
- channel = if c = Map.get(params, "chan"), do: LSGWeb.reformat_chan(c)
+ channel = if c = Map.get(params, "chan"), do: NolaWeb.reformat_chan(c)
render(conn, "prompt.html", network: network, channel: channel, prompt: prompt)
{:error, :not_found} ->
conn
|> put_status(404)
|> text("Page not found")
end
end
end
diff --git a/lib/lsg_web/controllers/icecast_see_controller.ex b/lib/lsg_web/controllers/icecast_see_controller.ex
index 1eecca1..877ad4e 100644
--- a/lib/lsg_web/controllers/icecast_see_controller.ex
+++ b/lib/lsg_web/controllers/icecast_see_controller.ex
@@ -1,41 +1,41 @@
-defmodule LSGWeb.IcecastSseController do
- use LSGWeb, :controller
+defmodule NolaWeb.IcecastSseController do
+ use NolaWeb, :controller
require Logger
@ping_interval 20_000
def sse(conn, _params) do
conn
|> put_resp_header("X-Accel-Buffering", "no")
|> put_resp_header("content-type", "text/event-stream")
|> send_chunked(200)
|> subscribe
|> send_sse_message("ping", "ping")
- |> send_sse_message("icecast", LSG.IcecastAgent.get)
+ |> send_sse_message("icecast", Nola.IcecastAgent.get)
|> sse_loop
end
def subscribe(conn) do
:timer.send_interval(@ping_interval, {:event, :ping})
- {:ok, _} = Registry.register(LSG.BroadcastRegistry, "icecast", [])
+ {:ok, _} = Registry.register(Nola.BroadcastRegistry, "icecast", [])
conn
end
def sse_loop(conn) do
{type, event} = receive do
{:event, :ping} -> {"ping", "ping"}
{:icecast, stats} -> {"icecast", stats}
end
conn
|> send_sse_message(type, event)
|> sse_loop()
end
defp send_sse_message(conn, type, data) do
json = Jason.encode!(%{type => data})
{:ok, conn} = chunk(conn, "event: #{type}\ndata: #{json}\n\n")
conn
end
end
diff --git a/lib/lsg_web/controllers/irc_auth_sse_controller.ex b/lib/lsg_web/controllers/irc_auth_sse_controller.ex
index f370d97..62ee2b5 100644
--- a/lib/lsg_web/controllers/irc_auth_sse_controller.ex
+++ b/lib/lsg_web/controllers/irc_auth_sse_controller.ex
@@ -1,66 +1,66 @@
-defmodule LSGWeb.IrcAuthSseController do
- use LSGWeb, :controller
+defmodule NolaWeb.IrcAuthSseController do
+ use NolaWeb, :controller
require Logger
@ping_interval 20_000
@expire_delay :timer.minutes(3)
def sse(conn, params) do
perks = if uri = Map.get(params, "redirect_to") do
{:redirect, uri}
else
nil
end
token = String.downcase(EntropyString.random_string(65))
conn
|> assign(:token, token)
|> assign(:perks, perks)
|> put_resp_header("X-Accel-Buffering", "no")
|> put_resp_header("content-type", "text/event-stream")
|> send_chunked(200)
|> subscribe()
|> send_sse_message("token", token)
|> sse_loop
end
def subscribe(conn) do
:timer.send_interval(@ping_interval, {:event, :ping})
:timer.send_after(@expire_delay, {:event, :expire})
{:ok, _} = Registry.register(IRC.PubSub, "messages:private", [])
conn
end
def sse_loop(conn) do
{type, event, exit} = receive do
{:event, :ping} -> {"ping", "ping", false}
{:event, :expire} -> {"expire", "expire", true}
{:irc, :text, %{account: account, text: token} = m} ->
if String.downcase(String.trim(token)) == conn.assigns.token do
- path = LSG.AuthToken.new_path(account.id, conn.assigns.perks)
+ path = Nola.AuthToken.new_path(account.id, conn.assigns.perks)
m.replyfun.("ok!")
{"authenticated", path, true}
else
{nil, nil, false}
end
_ -> {nil, nil, false}
end
conn = if type do
send_sse_message(conn, type, event)
else
conn
end
if exit do
conn
else
sse_loop(conn)
end
end
defp send_sse_message(conn, type, data) do
{:ok, conn} = chunk(conn, "event: #{type}\ndata: #{data}\n\n")
conn
end
end
diff --git a/lib/lsg_web/controllers/irc_controller.ex b/lib/lsg_web/controllers/irc_controller.ex
index d518481..90d9853 100644
--- a/lib/lsg_web/controllers/irc_controller.ex
+++ b/lib/lsg_web/controllers/irc_controller.ex
@@ -1,101 +1,101 @@
-defmodule LSGWeb.IrcController do
- use LSGWeb, :controller
+defmodule NolaWeb.IrcController do
+ use NolaWeb, :controller
- plug LSGWeb.ContextPlug
+ plug NolaWeb.ContextPlug
def index(conn, params) do
network = Map.get(params, "network")
- channel = if c = Map.get(params, "chan"), do: LSGWeb.reformat_chan(c)
+ channel = if c = Map.get(params, "chan"), do: NolaWeb.reformat_chan(c)
commands = for mod <- Enum.uniq([IRC.Account.AccountPlugin] ++ IRC.Plugin.enabled()) do
if is_atom(mod) do
identifier = Module.split(mod) |> List.last |> String.replace("Plugin", "") |> Macro.underscore
{identifier, mod.irc_doc()}
end
end
|> Enum.filter(& &1)
|> Enum.filter(fn({_, doc}) -> doc end)
members = cond do
network && channel -> Enum.map(IRC.UserTrack.channel(network, channel), fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple) end)
true ->
IRC.Membership.of_account(conn.assigns.account)
end
render conn, "index.html", network: network, commands: commands, channel: channel, members: members
end
def txt(conn, %{"name" => name}) do
if String.contains?(name, ".txt") do
name = String.replace(name, ".txt", "")
data = data()
if Map.has_key?(data, name) do
lines = Enum.join(data[name], "\n")
text(conn, lines)
else
conn
|> put_status(404)
|> text("Not found")
end
else
do_txt(conn, name)
end
end
def txt(conn, _), do: do_txt(conn, nil)
defp do_txt(conn, nil) do
- doc = LSG.IRC.TxtPlugin.irc_doc()
+ doc = Nola.IRC.TxtPlugin.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}/#{LSGWeb.format_chan(conn.assigns.chan)}"
+ 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(:lsg, :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/lsg_web/controllers/network_controller.ex b/lib/lsg_web/controllers/network_controller.ex
index 537c2f6..800294f 100644
--- a/lib/lsg_web/controllers/network_controller.ex
+++ b/lib/lsg_web/controllers/network_controller.ex
@@ -1,11 +1,11 @@
-defmodule LSGWeb.NetworkController do
- use LSGWeb, :controller
- plug LSGWeb.ContextPlug
+defmodule NolaWeb.NetworkController do
+ use NolaWeb, :controller
+ plug NolaWeb.ContextPlug
def index(conn, %{"network" => network}) do
conn
|> assign(:title, network)
|> render("index.html")
end
end
diff --git a/lib/lsg_web/controllers/open_id_controller.ex b/lib/lsg_web/controllers/open_id_controller.ex
index d5af318..94166eb 100644
--- a/lib/lsg_web/controllers/open_id_controller.ex
+++ b/lib/lsg_web/controllers/open_id_controller.ex
@@ -1,64 +1,64 @@
-defmodule LSGWeb.OpenIdController do
- use LSGWeb, :controller
- plug LSGWeb.ContextPlug, restrict: :public
+defmodule NolaWeb.OpenIdController do
+ use NolaWeb, :controller
+ plug NolaWeb.ContextPlug, restrict: :public
require Logger
def login(conn, _) do
url = OAuth2.Client.authorize_url!(new_client(), scope: "openid", state: Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false))
redirect(conn, external: url)
end
def callback(conn, %{"error" => error_code, "error_description" => error}) do
Logger.warn("OpenId error: #{error_code} #{error}")
render(conn, "error.html", error: error)
end
def callback(conn, %{"code" => code, "state" => state}) do
with \
client = %{token: %OAuth2.AccessToken{access_token: json}} = OAuth2.Client.get_token!(new_client(), state: state, code: code),
{:ok, %{"access_token" => token}} <- Jason.decode(json),
client = %OAuth2.Client{client | token: %OAuth2.AccessToken{access_token: token}},
{:ok, %OAuth2.Response{body: body}} <- OAuth2.Client.get(client, "/userinfo"),
{:ok, %{"sub" => id, "preferred_username" => username}} <- Jason.decode(body)
do
if account = conn.assigns.account do
if !IRC.Account.get_meta(account, "identity-id") do # XXX: And oidc id not linked yet
IRC.Account.put_meta(account, "identity-id", id)
end
IRC.Account.put_meta(account, "identity-username", username)
conn
else
conn
end
conn
|> put_session(:oidc_id, id)
|> put_flash(:info, "Logged in!")
|> redirect(to: Routes.path(conn, "/"))
else
{:error, %OAuth2.Response{status_code: 401}} ->
Logger.error("OpenID: Unauthorized token")
render(conn, "error.html", error: "The token is invalid.")
{:error, %OAuth2.Error{reason: reason}} ->
Logger.error("Error: #{inspect reason}")
render(conn, "error.html", error: reason)
end
end
def callback(conn, _params) do
render(conn, "error.html", error: "Unspecified error.")
end
defp new_client() do
config = Application.get_env(:lsg, :oidc)
OAuth2.Client.new([
strategy: OAuth2.Strategy.AuthCode,
client_id: config[:client_id],
client_secret: config[:client_secret],
site: config[:base_url],
authorize_url: config[:authorize_url],
token_url: config[:token_url],
- redirect_uri: Routes.open_id_url(LSGWeb.Endpoint, :callback)
+ redirect_uri: Routes.open_id_url(NolaWeb.Endpoint, :callback)
])
end
end
diff --git a/lib/lsg_web/controllers/page_controller.ex b/lib/lsg_web/controllers/page_controller.ex
index 94c9c70..2ac4d0a 100644
--- a/lib/lsg_web/controllers/page_controller.ex
+++ b/lib/lsg_web/controllers/page_controller.ex
@@ -1,53 +1,53 @@
-defmodule LSGWeb.PageController do
- use LSGWeb, :controller
+defmodule NolaWeb.PageController do
+ use NolaWeb, :controller
- plug LSGWeb.ContextPlug when action not in [:token]
- plug LSGWeb.ContextPlug, [restrict: :public] when action in [:token]
+ plug NolaWeb.ContextPlug when action not in [:token]
+ plug NolaWeb.ContextPlug, [restrict: :public] when action in [:token]
def token(conn, %{"token" => token}) do
with \
- {:ok, account, perks} <- LSG.AuthToken.lookup(token)
+ {: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)
users = IRC.UserTrack.find_by_account(account)
metas = IRC.Account.get_all_meta(account)
predicates = IRC.Account.get_predicates(account)
conn
|> assign(:title, account.name)
|> render("user.html", users: users, memberships: memberships, metas: metas, predicates: predicates)
end
def irc(conn, _) do
- bot_helps = for mod <- LSG.IRC.env(:handlers) 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, IRC.Account.get(account_id)}
do
assign(conn, :account, account)
else
_ -> conn
end
end
end
diff --git a/lib/lsg_web/controllers/sms_controller.ex b/lib/lsg_web/controllers/sms_controller.ex
index 00c6352..575655c 100644
--- a/lib/lsg_web/controllers/sms_controller.ex
+++ b/lib/lsg_web/controllers/sms_controller.ex
@@ -1,10 +1,10 @@
-defmodule LSGWeb.SmsController do
- use LSGWeb, :controller
+defmodule NolaWeb.SmsController do
+ use NolaWeb, :controller
require Logger
def ovh_callback(conn, %{"senderid" => from, "message" => message}) do
- spawn(fn() -> LSG.IRC.SmsPlugin.incoming(from, String.trim(message)) end)
+ spawn(fn() -> Nola.IRC.SmsPlugin.incoming(from, String.trim(message)) end)
text(conn, "")
end
end
diff --git a/lib/lsg_web/controllers/untappd_controller.ex b/lib/lsg_web/controllers/untappd_controller.ex
index 1c3ceb1..d3a540d 100644
--- a/lib/lsg_web/controllers/untappd_controller.ex
+++ b/lib/lsg_web/controllers/untappd_controller.ex
@@ -1,18 +1,18 @@
-defmodule LSGWeb.UntappdController do
- use LSGWeb, :controller
+defmodule NolaWeb.UntappdController do
+ use NolaWeb, :controller
def callback(conn, %{"code" => code}) do
with \
{:account, account_id} when is_binary(account_id) <- {:account, get_session(conn, :account)},
{:account, account} when not is_nil(account) <- {:account, IRC.Account.get(account_id)},
{:ok, auth_token} <- Untappd.auth_callback(code)
do
IRC.Account.put_meta(account, "untappd-token", auth_token)
text(conn, "OK!")
else
{:account, _} -> text(conn, "Error: account not found")
:error -> text(conn, "Error: untappd authentication failed")
end
end
end
diff --git a/lib/lsg_web/endpoint.ex b/lib/lsg_web/endpoint.ex
index bfd53c8..d8bf962 100644
--- a/lib/lsg_web/endpoint.ex
+++ b/lib/lsg_web/endpoint.ex
@@ -1,62 +1,62 @@
-defmodule LSGWeb.Endpoint do
+defmodule NolaWeb.Endpoint do
use Sentry.PlugCapture
use Phoenix.Endpoint, otp_app: :lsg
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
plug Plug.Static,
at: "/", from: :lsg, gzip: false,
only: ~w(assets css js fonts images favicon.ico robots.txt)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if 42==43 && code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
end
plug Plug.RequestId
plug Plug.Logger
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Jason
plug Sentry.PlugContext
plug Plug.MethodOverride
plug Plug.Head
@session_options [store: :cookie,
key: "_lsg_key",
signing_salt: "+p7K3wrj"]
socket "/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [session: @session_options]]
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug Plug.Session, @session_options
- plug LSGWeb.Router
+ plug NolaWeb.Router
@doc """
Callback invoked for dynamically configuring the endpoint.
It receives the endpoint configuration and checks if
configuration should be loaded from the system environment.
"""
def init(_key, config) do
if config[:load_from_system_env] do
port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
{:ok, Keyword.put(config, :http, [:inet6, port: port])}
else
{:ok, config}
end
end
end
diff --git a/lib/lsg_web/gettext.ex b/lib/lsg_web/gettext.ex
index f38a57d..e9a46e9 100644
--- a/lib/lsg_web/gettext.ex
+++ b/lib/lsg_web/gettext.ex
@@ -1,24 +1,24 @@
-defmodule LSGWeb.Gettext do
+defmodule NolaWeb.Gettext do
@moduledoc """
A module providing Internationalization with a gettext-based API.
By using [Gettext](https://hexdocs.pm/gettext),
your module gains a set of macros for translations, for example:
- import LSGWeb.Gettext
+ import NolaWeb.Gettext
# Simple translation
gettext "Here is the string to translate"
# Plural translation
ngettext "Here is the string to translate",
"Here are the strings to translate",
3
# Domain-based translation
dgettext "errors", "Here is the error message to translate"
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
"""
use Gettext, otp_app: :lsg
end
diff --git a/lib/lsg_web/live/chat_live.ex b/lib/lsg_web/live/chat_live.ex
index e84d880..276b362 100644
--- a/lib/lsg_web/live/chat_live.ex
+++ b/lib/lsg_web/live/chat_live.ex
@@ -1,120 +1,120 @@
-defmodule LSGWeb.ChatLive do
+defmodule NolaWeb.ChatLive do
use Phoenix.LiveView
use Phoenix.HTML
require Logger
def mount(%{"network" => network, "chan" => chan}, %{"account" => account_id}, socket) do
- chan = LSGWeb.reformat_chan(chan)
+ chan = NolaWeb.reformat_chan(chan)
connection = IRC.Connection.get_network(network, chan)
account = IRC.Account.get(account_id)
membership = IRC.Membership.of_account(IRC.Account.get("DRgpD4fLf8PDJMLp8Dtb"))
if account && connection && Enum.member?(membership, {connection.network, chan}) do
{:ok, _} = Registry.register(IRC.PubSub, "#{connection.network}:events", plugin: __MODULE__)
for t <- ["messages", "triggers", "outputs", "events"] do
{:ok, _} = Registry.register(IRC.PubSub, "#{connection.network}/#{chan}:#{t}", plugin: __MODULE__)
end
IRC.PuppetConnection.start(account, connection)
users = IRC.UserTrack.channel(connection.network, chan)
|> Enum.map(fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple) end)
|> Enum.reduce(Map.new, fn(user = %{id: id}, acc) ->
Map.put(acc, id, user)
end)
- backlog = case LSG.IRC.BufferPlugin.select_buffer(connection.network, chan) do
+ backlog = case Nola.IRC.BufferPlugin.select_buffer(connection.network, chan) do
{backlog, _} ->
{backlog, _} = Enum.reduce(backlog, {backlog, nil}, &reduce_contextual_event/2)
Enum.reverse(backlog)
_ -> []
end
socket = socket
|> assign(:connection_id, connection.id)
|> assign(:network, connection.network)
|> assign(:chan, chan)
|> assign(:title, "live")
|> assign(:channel, chan)
|> assign(:account_id, account.id)
|> assign(:backlog, backlog)
|> assign(:users, users)
|> assign(:counter, 0)
{:ok, socket}
else
{:ok, redirect(socket, to: "/")}
end
end
def handle_event("send", %{"message" => %{"text" => text}}, socket) do
account = IRC.Account.get(socket.assigns.account_id)
IRC.send_message_as(account, socket.assigns.network, socket.assigns.channel, text, true)
{:noreply, assign(socket, :counter, socket.assigns.counter + 1)}
end
def handle_info({:irc, :event, event = %{type: :join, user_id: id}}, socket) do
if user = IRC.UserTrack.lookup(id) do
socket = socket
|> assign(:users, Map.put(socket.assigns.users, id, user))
|> append_to_backlog(event)
{:noreply, socket}
else
{:noreply, socket}
end
end
def handle_info({:irc, :event, event = %{type: :nick, user_id: id, nick: nick}}, socket) do
socket = socket
|> assign(:users, update_in(socket.assigns.users, [id, :nick], nick))
|> append_to_backlog(event)
{:noreply, socket}
end
def handle_info({:irc, :event, event = %{type: :quit, user_id: id}}, socket) do
socket = socket
|> assign(:users, Map.delete(socket.assigns.users, id))
|> append_to_backlog(event)
{:noreply, socket}
end
def handle_info({:irc, :event, event = %{type: :part, user_id: id}}, socket) do
socket = socket
|> assign(:users, Map.delete(socket.assigns.users, id))
|> append_to_backlog(event)
{:noreply, socket}
end
def handle_info({:irc, :trigger, _, message}, socket) do
handle_info({:irc, nil, message}, socket)
end
def handle_info({:irc, :text, message}, socket) do
IO.inspect({:live_message, message})
socket = socket
|> append_to_backlog(message)
{:noreply, socket}
end
def handle_info(info, socket) do
Logger.debug("Unhandled info: #{inspect info}")
{:noreply, socket}
end
defp append_to_backlog(socket, line) do
{add, _} = reduce_contextual_event(line, {[], List.last(socket.assigns.backlog)})
assign(socket, :backlog, socket.assigns.backlog ++ add)
end
defp reduce_contextual_event(line, {acc, nil}) do
{[line | acc], line}
end
defp reduce_contextual_event(line, {acc, last}) do
if NaiveDateTime.to_date(last.at) != NaiveDateTime.to_date(line.at) do
{[%{type: :day_changed, date: NaiveDateTime.to_date(line.at), at: nil}, line | acc], line}
else
{[line | acc], line}
end
end
end
diff --git a/lib/lsg_web/lsg_web.ex b/lib/lsg_web/lsg_web.ex
index 3d9ab9a..da622c7 100644
--- a/lib/lsg_web/lsg_web.ex
+++ b/lib/lsg_web/lsg_web.ex
@@ -1,99 +1,99 @@
-defmodule LSGWeb do
+defmodule NolaWeb do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, views, channels and so on.
This can be used in your application as:
- use LSGWeb, :controller
- use LSGWeb, :view
+ use NolaWeb, :controller
+ use NolaWeb, :view
The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below. Instead, define any helper function in modules
and import those modules here.
"""
def format_chan("##") do
"♯♯"
end
def format_chan("#") do
"♯"
end
def format_chan("#"<>chan) do
chan
end
def format_chan(chan = "!"<>_), do: chan
def reformat_chan("♯") do
"#"
end
def reformat_chan("♯♯") do
"##"
end
def reformat_chan(chan = "!"<>_), do: chan
def reformat_chan(chan) do
"#"<>chan
end
def controller do
quote do
- use Phoenix.Controller, namespace: LSGWeb
+ use Phoenix.Controller, namespace: NolaWeb
import Plug.Conn
- import LSGWeb.Router.Helpers
- import LSGWeb.Gettext
- alias LSGWeb.Router.Helpers, as: Routes
+ import NolaWeb.Router.Helpers
+ import NolaWeb.Gettext
+ alias NolaWeb.Router.Helpers, as: Routes
end
end
def view do
quote do
use Phoenix.View, root: "lib/lsg_web/templates",
- namespace: LSGWeb
+ namespace: NolaWeb
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
- import LSGWeb.Router.Helpers
- import LSGWeb.ErrorHelpers
- import LSGWeb.Gettext
+ import NolaWeb.Router.Helpers
+ import NolaWeb.ErrorHelpers
+ import NolaWeb.Gettext
import Phoenix.LiveView.Helpers
- alias LSGWeb.Router.Helpers, as: Routes
+ alias NolaWeb.Router.Helpers, as: Routes
end
end
def router do
quote do
use Phoenix.Router
import Plug.Conn
import Phoenix.Controller
import Phoenix.LiveView.Router
end
end
def channel do
quote do
use Phoenix.Channel
- import LSGWeb.Gettext
+ import NolaWeb.Gettext
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end
diff --git a/lib/lsg_web/router.ex b/lib/lsg_web/router.ex
index 5cc0d4a..5658fda 100644
--- a/lib/lsg_web/router.ex
+++ b/lib/lsg_web/router.ex
@@ -1,85 +1,85 @@
-defmodule LSGWeb.Router do
- use LSGWeb, :router
+defmodule NolaWeb.Router do
+ use NolaWeb, :router
pipeline :browser do
plug :accepts, ["html", "txt"]
plug :fetch_session
plug :fetch_flash
plug :fetch_live_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
- plug :put_root_layout, {LSGWeb.LayoutView, :root}
+ plug :put_root_layout, {NolaWeb.LayoutView, :root}
end
pipeline :api do
plug :accepts, ["json", "sse"]
end
pipeline :matrix_app_service do
plug :accepts, ["json"]
- plug LSG.Matrix.Plug.Auth
- plug LSG.Matrix.Plug.SetConfig
+ plug Nola.Matrix.Plug.Auth
+ plug Nola.Matrix.Plug.SetConfig
end
- scope "/api", LSGWeb do
+ scope "/api", NolaWeb do
pipe_through :api
get "/irc-auth.sse", IrcAuthSseController, :sse
post "/sms/callback/Ovh", SmsController, :ovh_callback, as: :sms
end
- scope "/", LSGWeb do
+ scope "/", NolaWeb do
pipe_through :browser
get "/", PageController, :index
get "/login/irc/:token", PageController, :token, as: :login
get "/login/oidc", OpenIdController, :login
get "/login/oidc/callback", OpenIdController, :callback
get "/api/untappd/callback", UntappdController, :callback, as: :untappd_callback
get "/-", IrcController, :index
get "/-/txt", IrcController, :txt
get "/-/txt/:name", IrcController, :txt
get "/-/gpt/prompt/:id", GptController, :task
get "/-/gpt/result/:id", GptController, :result
get "/-/alcoolog", AlcoologController, :index
get "/-/alcoolog/~/:account_name", AlcoologController, :index
get "/:network", NetworkController, :index
get "/:network/~:nick/alcoolog", AlcoologController, :nick
get "/:network/~:nick/alcoolog/log.json", AlcoologController, :nick_log_json
get "/:network/~:nick/alcoolog/gls.json", AlcoologController, :nick_gls_json
get "/:network/~:nick/alcoolog/volumes.json", AlcoologController, :nick_volumes_json
get "/:network/~:nick/alcoolog/history.json", AlcoologController, :nick_history_json
get "/:network/~:nick/alcoolog/stats.json", AlcoologController, :nick_stats_json
get "/:network/:chan/alcoolog", AlcoologController, :index
get "/:network/:chan/alcoolog/gls.json", AlcoologController, :index_gls_json
get "/:network/:chan/gpt/prompt/:id", GptController, :task
get "/:network/:chan/gpt/result/:id", GptController, :result
put "/api/alcoolog/minisync/:user_id/meta/:key", AlcoologController, :minisync_put_meta
get "/:network/:chan", IrcController, :index
live "/:network/:chan/live", ChatLive
get "/:network/:chan/txt", IrcController, :txt
get "/:network/:chan/txt/:name", IrcController, :txt
get "/:network/:channel/preums", IrcController, :preums
get "/:network/:chan/alcoolog/t/:token", AlcoologController, :token
end
scope "/_matrix/:appservice", MatrixAppServiceWeb.V1, as: :matrix do
pipe_through :matrix_app_service
put "/transactions/:txn_id", TransactionController, :push
get "/users/:user_id", UserController, :query
get "/rooms/*room_alias", RoomController, :query
get "/thirdparty/protocol/:protocol", ThirdPartyController, :query_protocol
get "/thirdparty/user/:protocol", ThirdPartyController, :query_users
get "/thirdparty/location/:protocol", ThirdPartyController, :query_locations
get "/thirdparty/location", ThirdPartyController, :query_location_by_alias
get "/thirdparty/user", ThirdPartyController, :query_user_by_id
end
end
diff --git a/lib/lsg_web/views/alcoolog_view.ex b/lib/lsg_web/views/alcoolog_view.ex
index ed3c9b4..ad52472 100644
--- a/lib/lsg_web/views/alcoolog_view.ex
+++ b/lib/lsg_web/views/alcoolog_view.ex
@@ -1,6 +1,6 @@
-defmodule LSGWeb.AlcoologView do
- use LSGWeb, :view
+defmodule NolaWeb.AlcoologView do
+ use NolaWeb, :view
require Integer
end
diff --git a/lib/lsg_web/views/error_helpers.ex b/lib/lsg_web/views/error_helpers.ex
index 47906f2..25214bd 100644
--- a/lib/lsg_web/views/error_helpers.ex
+++ b/lib/lsg_web/views/error_helpers.ex
@@ -1,40 +1,40 @@
-defmodule LSGWeb.ErrorHelpers do
+defmodule NolaWeb.ErrorHelpers do
@moduledoc """
Conveniences for translating and building error messages.
"""
use Phoenix.HTML
@doc """
Generates tag for inlined form input errors.
"""
def error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn (error) ->
content_tag :span, translate_error(error), class: "help-block"
end)
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# Because error messages were defined within Ecto, we must
# call the Gettext module passing our Gettext backend. We
# also use the "errors" domain as translations are placed
# in the errors.po file.
# Ecto will pass the :count keyword if the error message is
# meant to be pluralized.
# On your own code and templates, depending on whether you
# need the message to be pluralized or not, this could be
# written simply as:
#
# dngettext "errors", "1 file", "%{count} files", count
# dgettext "errors", "is invalid"
#
if count = opts[:count] do
- Gettext.dngettext(LSGWeb.Gettext, "errors", msg, msg, count, opts)
+ Gettext.dngettext(NolaWeb.Gettext, "errors", msg, msg, count, opts)
else
- Gettext.dgettext(LSGWeb.Gettext, "errors", msg, opts)
+ Gettext.dgettext(NolaWeb.Gettext, "errors", msg, opts)
end
end
end
diff --git a/lib/lsg_web/views/error_view.ex b/lib/lsg_web/views/error_view.ex
index 1a7a92d..5cad939 100644
--- a/lib/lsg_web/views/error_view.ex
+++ b/lib/lsg_web/views/error_view.ex
@@ -1,17 +1,17 @@
-defmodule LSGWeb.ErrorView do
- use LSGWeb, :view
+defmodule NolaWeb.ErrorView do
+ use NolaWeb, :view
def render("404.html", _assigns) do
"Page not found"
end
def render("500.html", _assigns) do
"Internal server error"
end
# In case no render clause matches or no
# template is found, let's render it as 500
def template_not_found(_template, assigns) do
render "500.html", assigns
end
end
diff --git a/lib/lsg_web/views/irc_view.ex b/lib/lsg_web/views/irc_view.ex
index 36a9bc4..331d91f 100644
--- a/lib/lsg_web/views/irc_view.ex
+++ b/lib/lsg_web/views/irc_view.ex
@@ -1,3 +1,3 @@
-defmodule LSGWeb.IrcView do
- use LSGWeb, :view
+defmodule NolaWeb.IrcView do
+ use NolaWeb, :view
end
diff --git a/lib/lsg_web/views/layout_view.ex b/lib/lsg_web/views/layout_view.ex
index 720281d..2bffc6f 100644
--- a/lib/lsg_web/views/layout_view.ex
+++ b/lib/lsg_web/views/layout_view.ex
@@ -1,81 +1,81 @@
-defmodule LSGWeb.LayoutView do
- use LSGWeb, :view
+defmodule NolaWeb.LayoutView do
+ use NolaWeb, :view
def liquid_markdown(conn, text) do
context_path = cond do
- conn.assigns[:chan] -> "/#{conn.assigns[:network]}/#{LSGWeb.format_chan(conn.assigns[:chan])}"
+ conn.assigns[:chan] -> "/#{conn.assigns[:network]}/#{NolaWeb.format_chan(conn.assigns[:chan])}"
conn.assigns[:network] -> "/#{conn.assigns[:network]}/-"
true -> "/-"
end
{:ok, ast} = Liquex.parse(text)
context = Liquex.Context.new(%{
"context_path" => context_path
})
{content, _} = Liquex.render(ast, context)
content
|> to_string()
|> Earmark.as_html!()
|> raw()
end
def page_title(conn) do
target = cond do
conn.assigns[:chan] ->
"#{conn.assigns.chan} @ #{conn.assigns.network}"
conn.assigns[:network] -> conn.assigns.network
- true -> Keyword.get(LSG.name())
+ true -> Keyword.get(Nola.name())
end
breadcrumb_title = Enum.map(Map.get(conn.assigns, :breadcrumbs)||[], fn({title, _href}) -> title end)
title = [conn.assigns[:title], breadcrumb_title, target]
|> List.flatten()
|> Enum.uniq()
|> Enum.filter(fn(x) -> x end)
|> Enum.intersperse(" / ")
|> Enum.join()
content_tag(:title, title)
end
def format_time(date, with_relative \\ true) do
alias Timex.Format.DateTime.Formatters
alias Timex.Timezone
date = if is_integer(date) do
date
|> DateTime.from_unix!(:millisecond)
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
else
date
|> DateTime.shift_zone!("Europe/Paris", Tzdata.TimeZoneDatabase)
end
now = DateTime.now!("Europe/Paris", Tzdata.TimeZoneDatabase)
now_week = Timex.iso_week(now)
date_week = Timex.iso_week(date)
{y, w} = now_week
now_last_week = {y, w-1}
now_last_roll = 7-Timex.days_to_beginning_of_week(now)
date_date = DateTime.to_date(date)
now_date = DateTime.to_date(date)
format = cond do
date.year != now.year -> "{D}/{M}/{YYYY} {h24}:{m}"
date_date == now_date -> "{h24}:{m}"
(now_week == date_week) || (date_week == now_last_week && (Date.day_of_week(date) >= now_last_roll)) -> "{WDfull} {h24}:{m}"
(now.year == date.year && now.month == date.month) -> "{WDfull} {D} {h24}:{m}"
true -> "{WDfull} {D} {M} {h24}:{m}"
end
{:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
{:ok, full} = Formatters.Default.lformat(date, "{WDfull} {D} {YYYY} {h24}:{m}", "fr") #"{h24}:{m} {WDfull} {D}", "fr")
{:ok, detail} = Formatters.Default.lformat(date, format, "fr") #"{h24}:{m} {WDfull} {D}", "fr")
content_tag(:time, if(with_relative, do: relative, else: detail), [title: full])
end
end
diff --git a/lib/lsg_web/views/network_view.ex b/lib/lsg_web/views/network_view.ex
index c369ce6..7a24db1 100644
--- a/lib/lsg_web/views/network_view.ex
+++ b/lib/lsg_web/views/network_view.ex
@@ -1,4 +1,4 @@
-defmodule LSGWeb.NetworkView do
- use LSGWeb, :view
+defmodule NolaWeb.NetworkView do
+ use NolaWeb, :view
end
diff --git a/lib/lsg_web/views/open_id_view.ex b/lib/lsg_web/views/open_id_view.ex
index 64d4430..bd8089b 100644
--- a/lib/lsg_web/views/open_id_view.ex
+++ b/lib/lsg_web/views/open_id_view.ex
@@ -1,4 +1,4 @@
-defmodule LSGWeb.OpenIdView do
- use LSGWeb, :view
+defmodule NolaWeb.OpenIdView do
+ use NolaWeb, :view
end
diff --git a/lib/lsg_web/views/page_view.ex b/lib/lsg_web/views/page_view.ex
index 90c384c..1bfaadd 100644
--- a/lib/lsg_web/views/page_view.ex
+++ b/lib/lsg_web/views/page_view.ex
@@ -1,3 +1,3 @@
-defmodule LSGWeb.PageView do
- use LSGWeb, :view
+defmodule NolaWeb.PageView do
+ use NolaWeb, :view
end
diff --git a/lib/untappd.ex b/lib/untappd.ex
index 1f78376..d5ac904 100644
--- a/lib/untappd.ex
+++ b/lib/untappd.ex
@@ -1,94 +1,94 @@
defmodule Untappd do
@env Mix.env
@version Mix.Project.config[:version]
require Logger
def auth_url() do
client_id = Keyword.get(env(), :client_id)
- url = LSGWeb.Router.Helpers.untappd_callback_url(LSGWeb.Endpoint, :callback)
+ url = NolaWeb.Router.Helpers.untappd_callback_url(NolaWeb.Endpoint, :callback)
"https://untappd.com/oauth/authenticate/?client_id=#{client_id}&response_type=code&redirect_url=#{URI.encode(url)}"
end
def auth_callback(code) do
client_id = Keyword.get(env(), :client_id)
client_secret = Keyword.get(env(), :client_secret)
- url = LSGWeb.Router.Helpers.untappd_callback_url(LSGWeb.Endpoint, :callback)
+ url = NolaWeb.Router.Helpers.untappd_callback_url(NolaWeb.Endpoint, :callback)
params = %{
"client_id" => client_id,
"client_secret" => client_secret,
"response_type" => code,
"redirect_url" => url,
"code" => code
}
case HTTPoison.get("https://untappd.com/oauth/authorize", headers(), params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
json = Poison.decode!(body)
{:ok, get_in(json, ["response", "access_token"])}
error ->
Logger.error("Untappd auth callback failed: #{inspect error}")
:error
end
end
def maybe_checkin(account, beer_id) do
if token = IRC.Account.get_meta(account, "untappd-token") do
checkin(token, beer_id)
else
{:error, :no_token}
end
end
def checkin(token, beer_id) do
params = get_params(token: token)
|> Map.put("timezone", "CEST")
|> Map.put("bid", beer_id)
form_params = params
|> Enum.into([])
case HTTPoison.post("https://api.untappd.com/v4/checkin/add", {:form, form_params}, headers(), params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
body = Jason.decode!(body)
|> Map.get("response")
{:ok, body}
{:ok, resp = %HTTPoison.Response{status_code: code, body: body}} ->
Logger.warn "Untappd checkin error: #{inspect resp}"
{:error, {:http_error, code}}
{:error, error} -> {:error, {:http_error, error}}
end
end
def search_beer(query, params \\ []) do
params = get_params(params)
|> Map.put("q", query)
|> Map.put("limit", 10)
#|> Map.put("sort", "name")
case HTTPoison.get("https://api.untappd.com/v4/search/beer", headers(), params: params) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, Jason.decode!(body)}
error ->
Logger.error("Untappd search error: #{inspect error}")
end
end
def get_params(params) do
auth = %{"client_id" => Keyword.get(env(), :client_id), "client_secret" => Keyword.get(env(), :client_secret)}
if token = Keyword.get(params, :token) do
Map.put(auth, "access_token", token)
else
auth
end
end
def headers(extra \\ []) do
client_id = Keyword.get(env(), :client_id)
extra
++ [
{"user-agent", "dmzbot (#{client_id}; #{@version}-#{@env})"}
]
end
def env() do
Application.get_env(:lsg, :untappd)
end
end

File Metadata

Mime Type
text/x-diff
Expires
Fri, Mar 14, 4:51 PM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
33621
Default Alt Text
(373 KB)

Event Timeline