summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhref <href@random.sh>2021-09-04 05:41:09 +0200
committerhref <href@random.sh>2021-09-04 05:41:09 +0200
commit735a8dd998b2c0aebde9c96daefc7aa2c223218e (patch)
tree4a165897c8557065dfab401455943524087f79af
parentchat_live improvements (diff)
matrix appservice, puppet improvements
-rw-r--r--lib/irc.ex6
-rw-r--r--lib/irc/connection.ex24
-rw-r--r--lib/irc/puppet_connection.ex48
-rw-r--r--lib/irc/user_track.ex43
-rw-r--r--lib/lsg/application.ex6
-rw-r--r--lib/lsg/telegram_room.ex36
-rw-r--r--lib/lsg_matrix/matrix.ex158
-rw-r--r--lib/lsg_matrix/plug.ex25
-rw-r--r--lib/lsg_matrix/room.ex178
-rw-r--r--lib/lsg_web/router.ex22
-rw-r--r--mix.exs1
-rw-r--r--mix.lock8
12 files changed, 503 insertions, 52 deletions
diff --git a/lib/irc.ex b/lib/irc.ex
index 7f0d7e1..e063244 100644
--- a/lib/irc.ex
+++ b/lib/irc.ex
@@ -8,8 +8,10 @@ defmodule IRC do
:sender,
:channel,
:trigger,
- :replyfun,
- :at]
+ :replyfun,
+ :at,
+ {:meta, %{}}
+ ]
end
defmodule Trigger do
defstruct [:type, :trigger, :args]
diff --git a/lib/irc/connection.ex b/lib/irc/connection.ex
index eff5930..a0cdc27 100644
--- a/lib/irc/connection.ex
+++ b/lib/irc/connection.ex
@@ -113,7 +113,6 @@ defmodule IRC.Connection do
spec = [{{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[{:==, :"$1", {:const, network}}], [:"$_"]}]
results = Enum.map(:dets.select(dets(), spec), fn(object) -> from_tuple(object) end)
- |> IO.inspect()
if channel do
Enum.find(results, fn(conn) -> Enum.member?(conn.channels, channel) end)
else
@@ -277,14 +276,17 @@ defmodule IRC.Connection do
# Received something in a channel
def handle_info({:received, text, sender, chan}, state) 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: network(state), account: account, sender: sender, channel: chan, replyfun: reply_fun, trigger: extract_trigger(text)}
- message = case IRC.UserTrack.messaged(message) do
- :ok -> message
- {:ok, message} -> message
+ user = IRC.UserTrack.find_by_nick(network(state), sender.nick)
+ 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: network(state), account: account, sender: sender, channel: chan, replyfun: reply_fun, trigger: extract_trigger(text)}
+ message = case IRC.UserTrack.messaged(message) do
+ :ok -> message
+ {:ok, message} -> message
+ end
+ publish(message, ["#{message.network}/#{chan}:messages"])
end
- publish(message, ["#{message.network}/#{chan}:messages"])
{:noreply, state}
end
@@ -443,13 +445,9 @@ defmodule IRC.Connection do
|> 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()}}
+ {:irc, :out, %IRC.Message{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)
- case :global.whereis_name({LSG.TelegramRoom, network, target}) do
- pid when is_pid(pid) -> send(pid, {:raw, text})
- _ -> :ok
- end
end
defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:kick, reason}) do
diff --git a/lib/irc/puppet_connection.ex b/lib/irc/puppet_connection.ex
index 59bce1b..da6cc93 100644
--- a/lib/irc/puppet_connection.ex
+++ b/lib/irc/puppet_connection.ex
@@ -35,7 +35,7 @@ defmodule IRC.PuppetConnection do
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, channel, text})
+ 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
@@ -49,7 +49,7 @@ defmodule IRC.PuppetConnection do
else
pid
end
- GenServer.cast(pid, {:send_message, channel, text})
+ GenServer.cast(pid, {:send_message, self(), channel, text})
end
def start(account = %IRC.Account{}, connection = %IRC.Connection{}) do
@@ -103,11 +103,11 @@ defmodule IRC.PuppetConnection do
{:noreply, %{state | buffer: []}}
end
- def handle_cast(cast = {:send_message, channel, text}, state = %{connected: false, buffer: buffer}) do
+ 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, channel, text}, state = %{connected: true}) do
+ 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]
@@ -116,6 +116,20 @@ defmodule IRC.PuppetConnection do
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 = 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)
@@ -147,11 +161,8 @@ defmodule IRC.PuppetConnection do
def handle_info({:connected, server, port}, state) do
Logger.info("#{inspect(self())} Connected to #{server}:#{port} #{inspect state}")
{_, backoff} = :backoff.succeed(state.backoff)
- 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)
- nick = "#{base_nick}[p]"
- ExIRC.Client.logon(state.client, "", nick, base_nick, "#{base_nick}'s puppet")
+ 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
@@ -159,6 +170,8 @@ defmodule IRC.PuppetConnection do
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
@@ -176,4 +189,21 @@ defmodule IRC.PuppetConnection 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/irc/user_track.ex b/lib/irc/user_track.ex
index 4b1ee67..1b51266 100644
--- a/lib/irc/user_track.ex
+++ b/lib/irc/user_track.ex
@@ -20,7 +20,7 @@ defmodule IRC.UserTrack do
def clear_network(network) do
op(fn(ets) ->
spec = [
- {{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
+ {{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
{:==, :"$1", {:const, network}}
], [:"$_"]}
@@ -61,22 +61,22 @@ defmodule IRC.UserTrack do
defmodule Id, do: use EntropyString
defmodule User do
- defstruct [:id, :account, :network, :nick, {:nicks, []}, :username, :host, :realname, {:privileges, %{}}, {:last_active, %{}}]
+ defstruct [:id, :account, :network, :nick, {:nicks, []}, :username, :host, :realname, {:privileges, %{}}, {:last_active, %{}}, {:options, %{}}]
def to_tuple(u = %__MODULE__{}) do
- {u.id || IRC.UserTrack.Id.large_id, u.network, u.account, String.downcase(u.nick), u.nick, u.nicks || [], u.username, u.host, u.realname, u.privileges, u.last_active}
+ {u.id || IRC.UserTrack.Id.large_id, u.network, u.account, String.downcase(u.nick), u.nick, u.nicks || [], u.username, u.host, u.realname, u.privileges, u.last_active, u.options}
end
#tuple size: 11
- def from_tuple({id, network, account, _downcased_nick, nick, nicks, username, host, realname, privs, last_active}) do
- struct = %__MODULE__{id: id, account: account, network: network, nick: nick, nicks: nicks, username: username, host: host, realname: realname, privileges: privs, last_active: last_active}
+ def from_tuple({id, network, account, _downcased_nick, nick, nicks, username, host, realname, privs, last_active, opts}) do
+ struct = %__MODULE__{id: id, account: account, network: network, nick: nick, nicks: nicks, username: username, host: host, realname: realname, privileges: privs, last_active: last_active, options: opts}
end
end
def find_by_account(%IRC.Account{id: id}) do
#iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
spec = [
- {{:_, :_, :"$2", :_, :_, :_, :_, :_, :_, :_, :_},
+ {{:_, :_, :"$2", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
{:==, :"$2", {:const, id}}
], [:"$_"]}
@@ -91,7 +91,7 @@ defmodule IRC.UserTrack do
def find_by_account(network, %IRC.Account{id: id}) do
#iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
spec = [
- {{:_, :"$1", :"$2", :_, :_, :_, :_, :_, :_, :_, :_},
+ {{:_, :"$1", :"$2", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
{:andalso, {:==, :"$1", {:const, network}},
{:==, :"$2", {:const, id}}}
@@ -100,7 +100,10 @@ defmodule IRC.UserTrack do
case :ets.select(@ets, spec) do
results = [_r | _] ->
results
- |> Enum.sort_by(fn({_, _, _, _, _, _, _, _, _, _, actives}) ->
+ |> Enum.reject(fn({_, net, _, _, _, _, _, _, _, _, actives, opts}) -> net == "matrix" end)
+ |> Enum.reject(fn({_, net, _, _, _, _, _, _, _, _, actives, opts}) -> net == "telegram" end)
+ |> Enum.reject(fn({_, _, _, _, _, _, _, _, _, _, actives, opts}) -> Map.get(opts, :puppet) end)
+ |> Enum.sort_by(fn({_, _, _, _, _, _, _, _, _, _, actives, _}) ->
Map.get(actives, nil)
end, {:desc, NaiveDateTime})
|> List.first
@@ -117,14 +120,14 @@ defmodule IRC.UserTrack do
def merge_account(old_id, new_id) do
#iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
spec = [
- {{:_, :_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_},
+ {{:_, :_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
[
{:==, :"$1", {:const, old_id}}
], [:"$_"]}
]
- Enum.each(:ets.select(@ets, spec), fn({id, net, _, downcased_nick, nick, nicks, username, host, realname, privs, active}) ->
+ Enum.each(:ets.select(@ets, spec), fn({id, net, _, downcased_nick, nick, nicks, username, host, realname, privs, active, opts}) ->
Storage.op(fn(ets) ->
- :ets.insert(@ets, {id, net, new_id, downcased_nick, nick, nicks, username, host, realname, privs, active})
+ :ets.insert(@ets, {id, net, new_id, downcased_nick, nick, nicks, username, host, realname, privs, active, opts})
end)
end)
end
@@ -139,7 +142,7 @@ defmodule IRC.UserTrack do
end
def find_by_nick(network, nick) do
- case :ets.match(@ets, {:"$1", network, :_, String.downcase(nick), :_, :_, :_, :_, :_, :_, :_}) do
+ case :ets.match(@ets, {:"$1", network, :_, String.downcase(nick), :_, :_, :_, :_, :_, :_, :_, :_}) do
[[id]] -> lookup(id)
_ ->
nil
@@ -165,13 +168,24 @@ defmodule IRC.UserTrack do
end
def channel(network, channel) do
- Enum.filter(to_list(), fn({_, network, _, _, _, _, _, _, _, channels, _}) ->
+ Enum.filter(to_list(), fn({_, network, _, _, _, _, _, _, _, channels, _, _}) ->
Map.get(channels, channel)
end)
end
# TODO
- def connected(sender = %{nick: nick}) do
+ def connected(network, nick, user, host, account_id, opts \\ %{}) do
+ if account = IRC.Account.get(account_id) do
+ user = %User{id: IRC.UserTrack.Id.large_id, account: account_id, network: network, nick: nick, username: user, host: host, privileges: %{}, options: opts}
+ Storage.op(fn(ets) ->
+ :ets.insert(ets, User.to_tuple(user))
+ end)
+
+ IRC.Connection.publish_event(network, %{type: :connect, user_id: user.id, account_id: user.account})
+ :ok
+ else
+ :error
+ end
end
def joined(c, s), do: joined(c,s,[])
@@ -302,5 +316,4 @@ defmodule IRC.UserTrack do
defp userchans(%{privileges: privileges}) do
for({chan, _} <- privileges, do: chan)
end
-
end
diff --git a/lib/lsg/application.ex b/lib/lsg/application.ex
index cdd7915..5dd9d9c 100644
--- a/lib/lsg/application.ex
+++ b/lib/lsg/application.ex
@@ -6,6 +6,9 @@ defmodule LSG.Application do
def start(_type, _args) do
import Supervisor.Spec
+ :ok = LSG.Matrix.setup()
+ :ok = LSG.TelegramRoom.setup()
+
# Define workers and child supervisors to be supervised
children = [
# Start the endpoint when the application starts
@@ -19,6 +22,7 @@ defmodule LSG.Application do
{GenMagic.Pool, [name: LSG.GenMagic, pool_size: 2]},
#worker(LSG.Icecast, []),
] ++ LSG.IRC.application_childs
+ ++ LSG.Matrix.application_childs
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
@@ -26,6 +30,8 @@ defmodule LSG.Application do
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)
sup
end
diff --git a/lib/lsg/telegram_room.ex b/lib/lsg/telegram_room.ex
index 1eeec8f..f7e42c6 100644
--- a/lib/lsg/telegram_room.ex
+++ b/lib/lsg/telegram_room.ex
@@ -3,6 +3,20 @@ defmodule LSG.TelegramRoom do
@behaviour Telegram.ChatBot
alias Telegram.Api
+ def dets() do
+ (LSG.data_path() <> "/telegram-rooms.dets") |> String.to_charlist()
+ end
+
+ def setup() do
+ {:ok, _} = :dets.open_file(dets(), [])
+ :ok
+ end
+
+ def after_start() do
+ rooms = :dets.foldl(fn({id, _, _}, acc) -> [id | acc] end, [], dets())
+ for id <- rooms, do: Telegram.Bot.ChatBot.Chat.Session.Supervisor.start_child(LSG.Telegram, id)
+ end
+
@impl Telegram.ChatBot
def init(id) do
token = Keyword.get(Application.get_env(:lsg, :telegram, []), :key)
@@ -11,12 +25,13 @@ defmodule LSG.TelegramRoom do
[net, chan] = String.split(chat["title"], "/", parts: 2)
case IRC.Connection.get_network(net, chan) do
%IRC.Connection{} ->
- :global.register_name({__MODULE__, net, chan}, self())
{: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__)
err ->
Logger.warn("Did not found telegram match for #{id} \"#{chat["title"]}\"")
end
+ :dets.insert(dets(), {id, net, chan})
{:ok, %{id: id, net: net, chan: chan}}
end
@@ -48,22 +63,17 @@ defmodule LSG.TelegramRoom do
handle_info({:irc, nil, message}, state)
end
- def handle_info({:irc, _, %IRC.Message{sender: %{nick: nick}, text: text}}, state) do
- LSG.Telegram.send_message(state.id, "<#{nick}> #{text}")
- {:ok, state}
- end
-
- def handle_info({:raw, lines}, state) when is_list(lines) do
- formatted = for l <- lines, into: <<>>, do: l <> "\n"
- LSG.Telegram.send_message(state.id, formatted)
+ 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)
+ end
{:ok, state}
end
- def handle_info({:raw, line}, state) do
- handle_info({:raw, [line]}, state)
- end
-
def handle_info(info, state) do
+ Logger.info("UNhandled #{inspect info}")
{:ok, state}
end
diff --git a/lib/lsg_matrix/matrix.ex b/lib/lsg_matrix/matrix.ex
new file mode 100644
index 0000000..bea6d8b
--- /dev/null
+++ b/lib/lsg_matrix/matrix.ex
@@ -0,0 +1,158 @@
+defmodule LSG.Matrix do
+ require Logger
+ alias Polyjuice.Client
+
+ @behaviour MatrixAppService.Adapter.Room
+ @behaviour MatrixAppService.Adapter.Transaction
+ @behaviour MatrixAppService.Adapter.User
+
+ def dets(part) do
+ (LSG.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?("@_dev."<>_), do: true
+ def myself?(_), do: false
+
+
+ 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: "_dev.#{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]),
+ ]
+ 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)
+ 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: "#{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}
+ _ -> {: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)
+ :ok
+ error -> error
+ end
+ end
+
+ @impl MatrixAppService.Adapter.Transaction
+ def new_event(event = %MatrixAppService.Event{}) do
+ Logger.debug("New matrix event: #{inspect event}")
+ if room_id = event.room_id, do: LSG.Matrix.Room.start_and_send_matrix_event(room_id, event)
+ :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
new file mode 100644
index 0000000..5d5b603
--- /dev/null
+++ b/lib/lsg_matrix/plug.ex
@@ -0,0 +1,25 @@
+defmodule LSG.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
new file mode 100644
index 0000000..31d1b06
--- /dev/null
+++ b/lib/lsg_matrix/room.ex
@@ -0,0 +1,178 @@
+defmodule LSG.Matrix.Room do
+ require Logger
+ alias LSG.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}
+ 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
+ end
+ end
+ 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
+ {:ok, state} = Matrix.lookup_room(room_id)
+ 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}}
+ 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]
+ end
+ else
+ # XXX: The user left, remove from IRC.Memberships ?
+ acc
+ end
+ end)
+ 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.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(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_web/router.ex b/lib/lsg_web/router.ex
index 5fcf0a8..dc49c9f 100644
--- a/lib/lsg_web/router.ex
+++ b/lib/lsg_web/router.ex
@@ -15,6 +15,12 @@ defmodule LSGWeb.Router do
plug :accepts, ["json", "sse"]
end
+ pipeline :matrix_app_service do
+ plug :accepts, ["json"]
+ plug LSG.Matrix.Plug.Auth
+ plug LSG.Matrix.Plug.SetConfig
+ end
+
scope "/api", LSGWeb do
pipe_through :api
get "/irc-auth.sse", IrcAuthSseController, :sse
@@ -50,4 +56,20 @@ defmodule LSGWeb.Router do
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/mix.exs b/mix.exs
index f754cd5..2d5b037 100644
--- a/mix.exs
+++ b/mix.exs
@@ -70,6 +70,7 @@ defmodule LSG.Mixfile do
{:html_entities, "0.4.0", override: true},
{:file_size, "~> 3.0"},
{:ex2ms, "~> 1.0"},
+ {:matrix_app_service, git: "https://gitlab.com/kazarma/matrix_app_service.ex", branch: "master"},
]
end
end
diff --git a/mix.lock b/mix.lock
index 450594e..834de3d 100644
--- a/mix.lock
+++ b/mix.lock
@@ -5,15 +5,18 @@
"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"},
"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"},
@@ -35,11 +38,13 @@
"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"},
+ "matrix_app_service": {:git, "https://gitlab.com/kazarma/matrix_app_service.ex", "79ec2d33a749eb3245a39156601323232b8f4c9e", [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"},
@@ -48,6 +53,7 @@
"oauther": {:hex, :oauther, "1.1.1", "7d8b16167bb587ecbcddd3f8792beb9ec3e7b65c1f8ebd86b8dd25318d535752", [:mix], [], "hexpm", "9374f4302045321874cccdc57eb975893643bd69c3b22bf1312dab5f06e5788e"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"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"},
@@ -58,6 +64,8 @@
"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://gitlab.com/kazarma/polyjuice_client.git", "99ab68b955a99169645fd5ede690c0757e69f812", [branch: "kazarma"]},
+ "polyjuice_util": {:hex, :polyjuice_util, "0.1.0", "69901959c143245b47829c8302d0605dff6c0e1c3b116730c162982e0f512ee0", [:mix], [], "hexpm", "af5d1f614f52ce1da59a1f5a7c49249a2dbfda279d99d52d1b4e83e84c19a8d5"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"retry": {:hex, :retry, "0.14.1", "722d1b0cf87096b71213f5801d99fface7ca76adc83fc9dbf3e1daee952aef10", [:mix], [], "hexpm", "b3a609f286f6fe4f6b2c15f32cd4a8a60427d78d05d7b68c2dd9110981111ae0"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},