diff options
author | href <href@random.sh> | 2021-09-04 05:41:09 +0200 |
---|---|---|
committer | href <href@random.sh> | 2021-09-04 05:41:09 +0200 |
commit | 735a8dd998b2c0aebde9c96daefc7aa2c223218e (patch) | |
tree | 4a165897c8557065dfab401455943524087f79af /lib | |
parent | chat_live improvements (diff) |
matrix appservice, puppet improvements
Diffstat (limited to '')
-rw-r--r-- | lib/irc.ex | 6 | ||||
-rw-r--r-- | lib/irc/connection.ex | 24 | ||||
-rw-r--r-- | lib/irc/puppet_connection.ex | 48 | ||||
-rw-r--r-- | lib/irc/user_track.ex | 43 | ||||
-rw-r--r-- | lib/lsg/application.ex | 6 | ||||
-rw-r--r-- | lib/lsg/telegram_room.ex | 36 | ||||
-rw-r--r-- | lib/lsg_matrix/matrix.ex | 158 | ||||
-rw-r--r-- | lib/lsg_matrix/plug.ex | 25 | ||||
-rw-r--r-- | lib/lsg_matrix/room.ex | 178 | ||||
-rw-r--r-- | lib/lsg_web/router.ex | 22 |
10 files changed, 494 insertions, 52 deletions
@@ -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 |