Page MenuHomePhabricator

No OneTemporary

diff --git a/lib/irc/connection.ex b/lib/irc/connection.ex
index fcb6c5e..1e94003 100644
--- a/lib/irc/connection.ex
+++ b/lib/irc/connection.ex
@@ -1,517 +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 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{at: NaiveDateTime.utc_now(), text: text, network: state.network, account: account, sender: sender, channel: chan, replyfun: reply_fun, trigger: extract_trigger(text)}
+ 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{text: text, network: state.network, account: account, sender: sender, replyfun: reply_fun, trigger: extract_trigger(text)}
+ 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{network: network, channel: target, text: line, sender: %ExIRC.SenderInfo{nick: state.conn.nick}, at: NaiveDateTime.utc_now(), meta: %{self: true}}}
+ {: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 78b0611..fbad6e1 100644
--- a/lib/irc/irc.ex
+++ b/lib/irc/irc.ex
@@ -1,77 +1,79 @@
defmodule IRC do
- @derive {Poison.Encoder, except: [:replyfun]}
defmodule Message do
- defstruct [:text,
+ @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} <- Application.get_env(:lsg, :irc, [])[:admins]||[] do
admin_part_match?(n, nick) && admin_part_match?(u, user) && admin_part_match?(h, host)
end
|> Enum.any?
end
defp admin_part_match?(:_, _), do: true
defp admin_part_match?(a, a), do: true
defp admin_part_match?(_, _), do: false
@max_chars 440
def splitlong(string, max_chars \\ 440)
def splitlong(string, max_chars) when is_list(string) do
Enum.map(string, fn(s) -> splitlong(s, max_chars) end)
|> List.flatten()
end
def splitlong(string, max_chars) do
string
|> String.codepoints
|> Enum.chunk_every(max_chars)
|> Enum.map(&Enum.join/1)
end
def splitlong_with_prefix(string, prefix, max_chars \\ 440) do
prefix = "#{prefix} "
max_chars = max_chars - (length(String.codepoints(prefix)))
string
|> String.codepoints
|> Enum.chunk_every(max_chars)
|> Enum.map(fn(line) -> prefix <> Enum.join(line) end)
end
end
diff --git a/lib/irc/puppet_connection.ex b/lib/irc/puppet_connection.ex
index 4604b04..f12cbf7 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)
- 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
+ #ipv6 = if @env == :prod do
+ # subnet = LSG.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}
- _ ->
- {to_charlist(conn.host), []}
- end
+ #{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{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 = %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_irc/logger_plugin.ex b/lib/lsg_irc/logger_plugin.ex
index 667f714..e5307bc 100644
--- a/lib/lsg_irc/logger_plugin.ex
+++ b/lib/lsg_irc/logger_plugin.ex
@@ -1,60 +1,69 @@
defmodule LSG.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, "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(m = %IRC.Message{}) do
- %IRC.Message{m | replyfun: nil}
+ 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
- anything
+ %{"_id" => FlakeId.get(),
+ "type" => "object",
+ "object" => anything}
end
end
diff --git a/lib/lsg_irc/sms_plugin.ex b/lib/lsg_irc/sms_plugin.ex
index b183f7d..be1611f 100644
--- a/lib/lsg_irc/sms_plugin.ex
+++ b/lib/lsg_irc/sms_plugin.ex
@@ -1,164 +1,165 @@
defmodule LSG.IRC.SmsPlugin do
@moduledoc """
## sms
* **!sms `<nick>` `<message>`** envoie un SMS.
"""
def short_irc_doc, do: false
def irc_doc, do: @moduledoc
require Logger
def incoming(from, "enable "<>key) do
key = String.trim(key)
account = IRC.Account.find_meta_account("sms-validation-code", String.downcase(key))
if account do
net = IRC.Account.get_meta(account, "sms-validation-target")
IRC.Account.put_meta(account, "sms-number", from)
IRC.Account.delete_meta(account, "sms-validation-code")
IRC.Account.delete_meta(account, "sms-validation-number")
IRC.Account.delete_meta(account, "sms-validation-target")
IRC.Connection.broadcast_message(net, account, "SMS Number #{from} added!")
send_sms(from, "Yay! Number linked to account #{account.name}")
end
end
def incoming(from, message) do
account = IRC.Account.find_meta_account("sms-number", from)
if account do
reply_fun = fn(text) ->
send_sms(from, text)
end
trigger_text = if Enum.any?(IRC.Connection.triggers(), fn({trigger, _}) -> String.starts_with?(message, trigger) end) do
message
else
"!"<>message
end
message = %IRC.Message{
+ 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)
}
- IO.puts("converted sms to message: #{inspect message}")
+ 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),
"smsResponse" => %{
"cgiUrl" => LSGWeb.Router.Helpers.sms_url(LSGWeb.Endpoint, :ovh_callback),
"responseType" => "cgi"
}
} |> Poison.encode!()
headers = [{"content-type", "application/json"}] ++ sign("PUT", url, body)
options = []
case HTTPoison.put(url, body, headers, options) do
{:ok, %HTTPoison.Response{status_code: 200}} ->
:ok
error -> error
end
end
defp sign(method, url, body) do
ts = DateTime.utc_now() |> DateTime.to_unix()
as = env(:app_secret)
ck = env(:consumer_key)
sign = Enum.join([as, ck, String.upcase(method), url, body, ts], "+")
sign_hex = :crypto.hash(:sha, sign) |> Base.encode16(case: :lower)
headers = [{"X-OVH-Application", env(:app_key)}, {"X-OVH-Timestamp", ts},
{"X-OVH-Signature", "$1$"<>sign_hex}, {"X-Ovh-Consumer", ck}]
end
def parse_number(num) do
{:error, :todo}
end
defp env() do
Application.get_env(:lsg, :sms)
end
defp env(key) do
Keyword.get(env(), key)
end
end
diff --git a/lib/lsg_telegram/telegram.ex b/lib/lsg_telegram/telegram.ex
index 63940dc..748a456 100644
--- a/lib/lsg_telegram/telegram.ex
+++ b/lib/lsg_telegram/telegram.ex
@@ -1,232 +1,233 @@
defmodule LSG.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.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, %{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: 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}),
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}"
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: 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/mix.exs b/mix.exs
index 08f26a1..330bcc0 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,95 +1,96 @@
defmodule LSG.Mixfile do
use Mix.Project
def project do
[
app: :lsg,
- version: version("0.2.4"),
+ version: version("0.2.6"),
elixir: "~> 1.4",
elixirc_paths: elixirc_paths(Mix.env),
compilers: [:phoenix, :gettext] ++ Mix.compilers,
start_permanent: Mix.env == :prod,
deps: deps()
]
end
def application do
[
mod: {LSG.Application, []},
extra_applications: [:logger, :runtime_tools]
]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
defp aliases do
[
"assets.deploy": ["make -C assets", "phx.digest"]
]
end
defp deps do
[
{:phoenix, "~> 1.6.0-rc.0", override: true},
{:phoenix_pubsub, "~> 2.0"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:phoenix_html, "~> 3.0"},
{:phoenix_live_view, "~> 0.16.0"},
{:phoenix_live_dashboard, "~> 0.5"},
{:telemetry, "~> 1.0.0", override: true},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 0.5"},
{:plug_cowboy, "~> 2.0"},
{:cowlib, "~> 2.9.1", override: true},
{:plug, "~> 1.7"},
{:gettext, "~> 0.11"},
{:httpoison, "~> 1.8", override: true},
{:jason, "~> 1.0"},
{:poison, "~> 4.0", override: true},
{:floki, "~> 0.19.3"},
{:ecto, "~> 3.4"},
{:exirc, git: "https://git.random.sh/ircbot/exirc.git", branch: "fix-who-nick"},
{:distillery, "~> 2.0"},
{:earmark, "~> 1.2"},
{:oauther, "~> 1.1"},
{:extwitter, "~> 0.12.0"},
{:entropy_string, "~> 1.0.0"},
{:abacus, "~> 0.3.3"},
{:ex_chain, github: "eljojo/ex_chain"},
{:timex, "~> 3.6"},
{:muontrap, "~> 0.5.1"},
{:tzdata, "~> 1.0"},
{:nimble_csv, "~> 0.7.0"},
{:backoff, git: "https://github.com/ferd/backoff", branch: "master"},
{:telegram, git: "https://github.com/hrefhref/telegram.git", branch: "master"},
{:ex_aws, "~> 2.0"},
{:ex_aws_s3, "~> 2.0"},
{:gen_magic, git: "https://github.com/hrefhref/gen_magic", branch: "develop"},
{:liquex, "~> 0.3"},
{:html_entities, "0.4.0", override: true},
{:file_size, "~> 3.0"},
{:ex2ms, "~> 1.0"},
{:polyjuice_client, git: "https://git.random.sh/ircbot/polyjuice_client.git", branch: "master", override: true},
{:matrix_app_service, git: "https://git.random.sh/ircbot/matrix_app_service.ex.git", branch: "master"},
{:sentry, "~> 8.0.5"},
{:logger_json, "~> 4.3"},
{:oauth2, "~> 2.0"},
{:powerdnsex, git: "https://git.random.sh/ircbot/powerdnsex.git", branch: "master"},
{:pfx, "~> 0.7.0"},
+ {:flake_id, "~> 0.1.0"}
]
end
defp version(v) do
{describe, 0} = System.cmd("git", ~w(describe --dirty --broken --all --tags --long))
[_, rest] = String.split(describe, "/")
info = String.trim(rest)
env = cond do
Mix.env() == :prod -> ""
true -> "." <> to_string(Mix.env())
end
v <> "+" <> info <> env
end
end
diff --git a/mix.lock b/mix.lock
index 9fcde78..3c79c79 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,87 +1,90 @@
%{
"abacus": {:hex, :abacus, "0.3.3", "f2f11e23073f5e16af36ac425cd9fa9a338695e2f8014684239fa14a9171d5f6", [:mix], [], "hexpm", "a41110183de16eda239f2187e7bb0c91c50658a8ae8254b85352287eb7034d88"},
"artificery": {:hex, :artificery, "0.4.3", "0bc4260f988dcb9dda4b23f9fc3c6c8b99a6220a331534fdf5bf2fd0d4333b02", [:mix], [], "hexpm", "12e95333a30e20884e937abdbefa3e7f5e05609c2ba8cf37b33f000b9ffc0504"},
"backoff": {:git, "https://github.com/ferd/backoff", "4b8c02d038de1055481b0193665944e11fec337e", [branch: "master"]},
+ "base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"},
"castore": {:hex, :castore, "0.1.11", "c0665858e0e1c3e8c27178e73dffea699a5b28eb72239a3b2642d208e8594914", [:mix], [], "hexpm", "91b009ba61973b532b84f7c09ce441cba7aa15cb8b006cf06c6f4bba18220081"},
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
+ "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
"date_time_parser": {:hex, :date_time_parser, "1.1.1", "cd7a04eb8f413a63cfb16892575d08a23651de1118c95278c13f84c105247901", [:mix], [{:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:timex, ">= 3.2.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2ede6de7994c1589bcf118954999ed6ff5de97415b33827ea5b30804c7e512ef"},
"db_connection": {:hex, :db_connection, "2.4.0", "d04b1b73795dae60cead94189f1b8a51cc9e1f911c234cc23074017c43c031e5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad416c21ad9f61b3103d254a71b63696ecadb6a917b36f563921e0de00d7d7c8"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm", "bbc7008b0161a6f130d8d903b5b3232351fccc9c31a991f8fcbf2a12ace22995"},
"earmark": {:hex, :earmark, "1.4.15", "2c7f924bf495ec1f65bd144b355d0949a05a254d0ec561740308a54946a67888", [:mix], [{:earmark_parser, ">= 1.4.13", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "3b1209b85bc9f3586f370f7c363f6533788fb4e51db23aa79565875e7f9999ee"},
"earmark_parser": {:hex, :earmark_parser, "1.4.15", "b29e8e729f4aa4a00436580dcc2c9c5c51890613457c193cc8525c388ccb2f06", [:mix], [], "hexpm", "044523d6438ea19c1b8ec877ec221b008661d3c27e3b848f4c879f500421ca5c"},
"ecto": {:hex, :ecto, "3.7.1", "a20598862351b29f80f285b21ec5297da1181c0442687f9b8329f0445d228892", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d36e5b39fc479e654cffd4dbe1865d9716e4a9b6311faff799b6f90ab81b8638"},
"ecto_sql": {:hex, :ecto_sql, "3.7.0", "2fcaad4ab0c8d76a5afbef078162806adbe709c04160aca58400d5cbbe8eeac6", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a26135dfa1d99bf87a928c464cfa25bba6535a4fe761eefa56077a4febc60f70"},
"elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
"entropy_string": {:hex, :entropy_string, "1.0.7", "61a5a989e78fd2798e35a17a98a17f81fb504e8d4ba620bcd4f19063eb782943", [:mix], [], "hexpm", "c497fc9cf6bae2075c4c985e66b4306baa4cb19f142d97e0aa1d7a993ae3bb47"},
"ex2ms": {:hex, :ex2ms, "1.6.1", "66d472eb14da43087c156e0396bac3cc7176b4f24590a251db53f84e9a0f5f72", [:mix], [], "hexpm", "a7192899d84af03823a8ec2f306fa858cbcce2c2e7fd0f1c49e05168fb9c740e"},
"ex_aws": {:hex, :ex_aws, "2.2.4", "b6b9a73468205c67851f6c195429b435741ec3d5f45be4cfdaa8f54a62491f15", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc9af9199d869c0532819f87678a547c7dd7c57088d932b43dcbdbc315886601"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.3.0", "5dfe50116bad048240bae7cd9418bfe23296542ff72a01b9138113a1cd31451c", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "0b13b11478825d62d2f6e57ae763695331be06f2216468f31bb304316758b096"},
"ex_chain": {:git, "https://github.com/eljojo/ex_chain.git", "09d88a10613b6acc33340c9fa1b3540493e431b8", []},
"exirc": {:git, "https://git.random.sh/ircbot/exirc.git", "ae1de0025dc0184697c0a440eb3cbbf505b154b6", [branch: "fix-who-nick"]},
"extwitter": {:hex, :extwitter, "0.12.4", "8e69a55ca4c3ad1caa0fa4585ce33bbf4d636fd56210c961e36d109d0848c1d9", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:oauther, "~> 1.1", [hex: :oauther, repo: "hexpm", optional: false]}], "hexpm", "1df46ffb49b196225afbf665a1a0f1a5fdd144b11bf1394509ea055f38c3343d"},
"file_size": {:hex, :file_size, "3.0.1", "ad447a69442a82fc701765a73992d7b1110136fa0d4a9d3190ea685d60034dcd", [:mix], [{:decimal, ">= 1.0.0 and < 3.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:number, "~> 1.0", [hex: :number, repo: "hexpm", optional: false]}], "hexpm", "64dd665bc37920480c249785788265f5d42e98830d757c6a477b3246703b8e20"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
+ "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
"floki": {:hex, :floki, "0.19.3", "652d1447767f783bd6cae1d882fd2145f25db28c6841ab87659225b468cff101", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "1c8da482a0848c55a1d22af49ce6547790077adac2a04cf265e1f26583781adb"},
"gen_magic": {:git, "https://github.com/hrefhref/gen_magic", "48a12cca10305c8d357fe16b10fd7ead9b64a56a", [branch: "develop"]},
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
"gun": {:hex, :gun, "1.3.3", "cf8b51beb36c22b9c8df1921e3f2bc4d2b1f68b49ad4fbc64e91875aa14e16b4", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "3106ce167f9c9723f849e4fb54ea4a4d814e3996ae243a1c828b256e749041e0"},
"hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"},
"html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm", "3e3d7156a272950373ce5a4018b1490bea26676f8d6a7d409f6fac8568b8cb9a"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "abfb393ad888d57700f4d0f119c2643c8a9d98856f9b8a92001be7efad1419d6"},
"httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"liquex": {:hex, :liquex, "0.6.1", "2e07fc177dfb2ecafe326f11bd641373f3f6b62704a0231832d8634e162e852a", [:mix], [{:date_time_parser, "~> 1.1", [hex: :date_time_parser, repo: "hexpm", optional: false]}, {:html_entities, "~> 0.5.1", [hex: :html_entities, repo: "hexpm", optional: false]}, {:html_sanitize_ex, "~> 1.3.0", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:timex, "~> 3.6", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2ec6c68fce04e10ca1fd3874d146991cf1b44adc0c8451615873263944353772"},
"logger_json": {:hex, :logger_json, "4.3.0", "41aaaab2c2e1c071bfddbcc5a3f567884fdf312d222c7f1a7e3de6ab667774f7", [:mix], [{:ecto, "~> 2.1 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "001bbc34d7c451cfeed298c8384cb3aab10b364db2eb095c466c7a1a28bee6e0"},
"matrix_app_service": {:git, "https://git.random.sh/ircbot/matrix_app_service.ex.git", "5a4efc102f97abad6b60fab7bf761acef6acc78d", [branch: "master"]},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mochiweb": {:hex, :mochiweb, "2.22.0", "f104d6747c01a330c38613561977e565b788b9170055c5241ac9dd6e4617cba5", [:rebar3], [], "hexpm", "cbbd1fd315d283c576d1c8a13e0738f6dafb63dc840611249608697502a07655"},
"muontrap": {:hex, :muontrap, "0.5.1", "98fe96d0e616ee518860803a37a29eb23ffc2ca900047cb1bb7fd37521010093", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3c11b7f151b202148912c73cbdd633b76fa68fabc26cc441c9d6d140e22290dc"},
"mutex": {:hex, :mutex, "1.1.3", "d7e19f96fe19d6d97583bf12ca1ec182bbf14619b7568592cc461135de1c3b81", [:mix], [], "hexpm", "2b83b92784add2611c23dd527073b5e8dfe3c9c6c94c5bf9e3081b5c41c3ff3e"},
"nimble_csv": {:hex, :nimble_csv, "0.7.0", "52f23ce46eee304d063d1716e19e45ea544bd751536bc53e5d41cb7fc0ca9405", [:mix], [], "hexpm", "e7051e7a95b5c4f26512af5805c320ee9185e752d949f048bf318fedef86cccc"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"nimble_pool": {:hex, :nimble_pool, "0.2.4", "1db8e9f8a53d967d595e0b32a17030cdb6c0dc4a451b8ac787bf601d3f7704c3", [:mix], [], "hexpm", "367e8071e137b787764e6a9992ccb57b276dc2282535f767a07d881951ebeac6"},
"nimble_strftime": {:hex, :nimble_strftime, "0.1.1", "b988184d1bd945bc139b2c27dd00a6c0774ec94f6b0b580083abd62d5d07818b", [:mix], [], "hexpm", "89e599c9b8b4d1203b7bb5c79eb51ef7c6a28fbc6228230b312f8b796310d755"},
"number": {:hex, :number, "1.0.3", "932c8a2d478a181c624138958ca88a78070332191b8061717270d939778c9857", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "dd397bbc096b2ca965a6a430126cc9cf7b9ef7421130def69bcf572232ca0f18"},
"oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"},
"oauther": {:hex, :oauther, "1.1.1", "7d8b16167bb587ecbcddd3f8792beb9ec3e7b65c1f8ebd86b8dd25318d535752", [:mix], [], "hexpm", "9374f4302045321874cccdc57eb975893643bd69c3b22bf1312dab5f06e5788e"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"pfx": {:hex, :pfx, "0.7.0", "551ead4c303d6e4943d315bba349ee2a7cecf05a5311d8a8e6a2661cc9e64951", [:mix], [], "hexpm", "4497f1625c0b71d5749bebca0acf564ae60e5ea374645088c7c57079165379ae"},
"phoenix": {:hex, :phoenix, "1.6.0-rc.0", "87dc1bb400588019a878ecf32c2d229c7d7f31a520c574860a059934663ffa70", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2a0d344d2a2f654a9300b2b09dbf9c3821762e1364e26fce12d76fcd498b92c0"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
"phoenix_html": {:hex, :phoenix_html, "3.0.2", "0d71bd7dfa5fad2103142206e25e16accd64f41bcbd0002af3f0da17e530968d", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d6c6e85d9bef8d52a5a66fcccd15529651f379eaccbf10500343a17f6f814f82"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.5.0", "3282d8646e1bfc1ef1218f508d9fcefd48cf47f9081b7667bd9b281b688a49cf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.6", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.16.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "609740be43de94ae0abd2c4300ff0356a6e8a9487bf340e69967643a59fa7ec8"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.16.1", "a17652e936718b6b6b52ef64d4b9860bc30c41b9a491e25f2b49a70604efa436", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "94bbc572471ad151b756b38dd10acbf91e0bcc132ad8b78240baa0dcf77cea74"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"},
"plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},
"plug_cowboy": {:hex, :plug_cowboy, "2.5.1", "7cc96ff645158a94cf3ec9744464414f02287f832d6847079adfe0b58761cbd0", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "107d0a5865fa92bcb48e631cc0729ae9ccfa0a9f9a1bd8f01acb513abf1c2d64"},
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
"poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"},
"polyjuice_client": {:git, "https://git.random.sh/ircbot/polyjuice_client.git", "92c949be2def3cd0280cbc78849b109d34c8fcaa", [branch: "master"]},
"polyjuice_util": {:hex, :polyjuice_util, "0.1.0", "69901959c143245b47829c8302d0605dff6c0e1c3b116730c162982e0f512ee0", [:mix], [], "hexpm", "af5d1f614f52ce1da59a1f5a7c49249a2dbfda279d99d52d1b4e83e84c19a8d5"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"powerdnsex": {:git, "https://git.random.sh/ircbot/powerdnsex.git", "1dad0c28ac0af45f0b5b1171af2a117fc6b341bf", [branch: "master"]},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"retry": {:hex, :retry, "0.14.1", "722d1b0cf87096b71213f5801d99fface7ca76adc83fc9dbf3e1daee952aef10", [:mix], [], "hexpm", "b3a609f286f6fe4f6b2c15f32cd4a8a60427d78d05d7b68c2dd9110981111ae0"},
"sentry": {:hex, :sentry, "8.0.5", "5ca922b9238a50c7258b52f47364b2d545beda5e436c7a43965b34577f1ef61f", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "4972839fdbf52e886d7b3e694c8adf421f764f2fa79036b88fb4742049bd4b7c"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.6", "45866d958d9ae51cfe8fef0050ab8054d25cba23ace43b88046092aa2c714645", [:make], [], "hexpm", "72b2fc8a8e23d77eed4441137fefa491bbf4a6dc52e9c0045f3f8e92e66243b5"},
"telegram": {:git, "https://github.com/hrefhref/telegram.git", "21c81460a633b656d2de8aa33beff49c3bb87670", [branch: "master"]},
"telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
"tesla": {:hex, :tesla, "1.4.3", "f5a494e08fb1abe4fd9c28abb17f3d9b62b8f6fc492860baa91efb1aab61c8a0", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "e0755bb664bf4d664af72931f320c97adbf89da4586670f4864bf259b5750386"},
"timex": {:hex, :timex, "3.7.6", "502d2347ec550e77fdf419bc12d15bdccd31266bb7d925b30bf478268098282f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "a296327f79cb1ec795b896698c56e662ed7210cc9eb31f0ab365eb3a62e2c589"},
"tzdata": {:hex, :tzdata, "1.1.0", "72f5babaa9390d0f131465c8702fa76da0919e37ba32baa90d93c583301a8359", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "18f453739b48d3dc5bcf0e8906d2dc112bb40baafe2c707596d89f3c8dd14034"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jul 6, 3:39 AM (9 h, 54 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
49804
Default Alt Text
(76 KB)

Event Timeline