diff options
author | Jordan Bracco <href@random.sh> | 2022-12-20 02:19:42 +0000 |
---|---|---|
committer | Jordan Bracco <href@random.sh> | 2022-12-20 19:29:41 +0100 |
commit | 9958e90eb5eb5a2cc171c40860745e95a96bd429 (patch) | |
tree | b49cdb1d0041b9c0a81a14950d38c0203896f527 /lib/matrix | |
parent | Rename to Nola (diff) |
Actually do not prefix folders with nola_ refs T77
Diffstat (limited to 'lib/matrix')
-rw-r--r-- | lib/matrix/matrix.ex | 169 | ||||
-rw-r--r-- | lib/matrix/plug.ex | 25 | ||||
-rw-r--r-- | lib/matrix/room.ex | 196 |
3 files changed, 390 insertions, 0 deletions
diff --git a/lib/matrix/matrix.ex b/lib/matrix/matrix.ex new file mode 100644 index 0000000..9334816 --- /dev/null +++ b/lib/matrix/matrix.ex @@ -0,0 +1,169 @@ +defmodule Nola.Matrix do + require Logger + alias Polyjuice.Client + + @behaviour MatrixAppService.Adapter.Room + @behaviour MatrixAppService.Adapter.Transaction + @behaviour MatrixAppService.Adapter.User + @env Mix.env + + def dets(part) do + (Nola.data_path() <> "/matrix-#{to_string(part)}.dets") |> String.to_charlist() + end + + def setup() do + {:ok, _} = :dets.open_file(dets(:rooms), []) + {:ok, _} = :dets.open_file(dets(:room_aliases), []) + {:ok, _} = :dets.open_file(dets(:users), []) + :ok + end + + def myself?("@_dev:random.sh"), do: true + def myself?("@_bot:random.sh"), do: true + def myself?("@_dev."<>_), do: true + def myself?("@_bot."<>_), do: true + def myself?(_), do: false + + def mxc_to_http(mxc = "mxc://"<>_) do + uri = URI.parse(mxc) + %URI{uri | scheme: "https", path: "/_matrix/media/r0/download/#{uri.authority}#{uri.path}"} + |> URI.to_string() + end + + def get_or_create_matrix_user(id) do + if mxid = lookup_user(id) do + mxid + else + opts = [ + type: "m.login.application_service", + inhibit_login: true, + device_id: "APP_SERVICE", + initial_device_display_name: "Application Service", + username: if(@env == :dev, do: "_dev.#{id}", else: "_bot.#{id}") + ] + Logger.debug("Registering user for #{id}") + {:ok, %{"user_id" => mxid}} = Polyjuice.Client.LowLevel.register(client(), opts) + :dets.insert(dets(:users), {id, mxid}) + end + end + + def lookup_user(id) do + case :dets.lookup(dets(:users), id) do + [{_, matrix_id}] -> matrix_id + _ -> nil + end + end + + def user_name("@"<>name) do + [username, _] = String.split(name, ":", parts: 2) + username + end + + def application_childs() do + import Supervisor.Spec + [ + supervisor(Nola.Matrix.Room.Supervisor, [], [name: IRC.PuppetConnection.Supervisor]), + ] + end + + def after_start() do + rooms = :dets.foldl(fn({id, _, _, _}, acc) -> [id | acc] end, [], dets(:rooms)) + for room <- rooms, do: Nola.Matrix.Room.start(room) + end + + def lookup_room(room) do + case :dets.lookup(dets(:rooms), room) do + [{_, network, channel, opts}] -> {:ok, Map.merge(opts, %{network: network, channel: channel})} + _ -> {:error, :no_such_room} + end + end + + def lookup_room_alias(room_alias) do + case :dets.lookup(dets(:room_aliases), room_alias) do + [{_, room_id}] -> {:ok, room_id} + _ -> {:error, :no_such_room_alias} + end + end + + def lookup_or_create_room(room_alias) do + case lookup_room_alias(room_alias) do + {:ok, room_id} -> {:ok, room_id} + {:error, :no_such_room_alias} -> create_room(room_alias) + end + end + + def create_room(room_alias) do + Logger.debug("Matrix: creating room #{inspect room_alias}") + localpart = localpart(room_alias) + with {:ok, network, channel} <- extract_network_channel_from_localpart(localpart), + %IRC.Connection{} <- IRC.Connection.get_network(network, channel), + room = [visibility: :public, room_alias_name: localpart, name: if(network == "random", do: channel, else: "#{network}/#{channel}")], + {:ok, %{"room_id" => room_id}} <- Client.Room.create_room(client(), room) do + Logger.info("Matrix: created room #{room_alias} #{room_id}") + :dets.insert(dets(:rooms), {room_id, network, channel, %{}}) + :dets.insert(dets(:room_aliases), {room_alias, room_id}) + {:ok, room_id} + else + nil -> {:error, :no_such_network_channel} + error -> error + end + end + + def localpart(room_alias) do + [<<"#", localpart :: binary>>, _] = String.split(room_alias, ":", parts: 2) + localpart + end + + def extract_network_channel_from_localpart(localpart) do + s = localpart + |> String.replace("dev.", "") + |> String.split("/", parts: 2) + + case s do + [network, channel] -> {:ok, network, channel} + [channel] -> {:ok, "random", channel} + _ -> {:error, :invalid_localpart} + end + end + + @impl MatrixAppService.Adapter.Room + def query_alias(room_alias) do + case lookup_or_create_room(room_alias) do + {:ok, room_id} -> + Nola.Matrix.Room.start(room_id) + :ok + error -> error + end + end + + @impl MatrixAppService.Adapter.Transaction + def new_event(event = %MatrixAppService.Event{}) do + Logger.debug("New matrix event: #{inspect event}") + if event.room_id do + Nola.Matrix.Room.start_and_send_matrix_event(event.room_id, event) + end + :noop + end + + @impl MatrixAppService.Adapter.User + def query_user(user_id) do + Logger.warn("Matrix lookup user: #{inspect user_id}") + :error + end + + def client(opts \\ []) do + base_url = Application.get_env(:matrix_app_service, :base_url) + access_token = Application.get_env(:matrix_app_service, :access_token) + default_opts = [ + access_token: access_token, + device_id: "APP_SERVICE", + application_service: true, + user_id: nil + ] + opts = Keyword.merge(default_opts, opts) + + Polyjuice.Client.LowLevel.create(base_url, opts) + end + + +end diff --git a/lib/matrix/plug.ex b/lib/matrix/plug.ex new file mode 100644 index 0000000..c64ed11 --- /dev/null +++ b/lib/matrix/plug.ex @@ -0,0 +1,25 @@ +defmodule Nola.Matrix.Plug do + + defmodule Auth do + def init(state) do + state + end + + def call(conn, _) do + hs = Application.get_env(:matrix_app_service, :homeserver_token) + MatrixAppServiceWeb.AuthPlug.call(conn, hs) + end + end + + defmodule SetConfig do + def init(state) do + state + end + + def call(conn, _) do + config = Application.get_all_env(:matrix_app_service) + MatrixAppServiceWeb.SetConfigPlug.call(conn, config) + end + end + +end diff --git a/lib/matrix/room.ex b/lib/matrix/room.ex new file mode 100644 index 0000000..c790760 --- /dev/null +++ b/lib/matrix/room.ex @@ -0,0 +1,196 @@ +defmodule Nola.Matrix.Room do + require Logger + alias Nola.Matrix + alias Polyjuice.Client + import Matrix, only: [client: 0, client: 1, user_name: 1, myself?: 1] + + defmodule Supervisor do + use DynamicSupervisor + + def start_link() do + DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__) + end + + def start_child(room_id) do + spec = %{id: room_id, start: {Nola.Matrix.Room, :start_link, [room_id]}, restart: :transient} + DynamicSupervisor.start_child(__MODULE__, spec) + end + + @impl true + def init(_init_arg) do + DynamicSupervisor.init( + strategy: :one_for_one, + max_restarts: 10, + max_seconds: 1 + ) + end + end + + def start(room_id) do + __MODULE__.Supervisor.start_child(room_id) + end + + def start_link(room_id) do + GenServer.start_link(__MODULE__, [room_id], name: name(room_id)) + end + + def start_and_send_matrix_event(room_id, event) do + pid = if pid = whereis(room_id) do + pid + else + case __MODULE__.start(room_id) do + {:ok, pid} -> pid + {:error, {:already_started, pid}} -> pid + :ignore -> nil + end + end + if(pid, do: send(pid, {:matrix_event, event})) + end + + def whereis(room_id) do + {:global, name} = name(room_id) + case :global.whereis_name(name) do + :undefined -> nil + pid -> pid + end + end + + def name(room_id) do + {:global, {__MODULE__, room_id}} + end + + def init([room_id]) do + case Matrix.lookup_room(room_id) do + {:ok, state} -> + Logger.metadata(matrix_room: room_id) + + {:ok, _} = Registry.register(IRC.PubSub, "#{state.network}:events", plugin: __MODULE__) + for t <- ["messages", "triggers", "outputs", "events"] do + {:ok, _} = Registry.register(IRC.PubSub, "#{state.network}/#{state.channel}:#{t}", plugin: __MODULE__) + end + + state = state + |> Map.put(:id, room_id) + Logger.info("Started Matrix room #{room_id}") + {:ok, state, {:continue, :update_state}} + error -> + Logger.info("Received event for nonexistent room #{inspect room_id}: #{inspect error}") + :ignore + end + end + + def handle_continue(:update_state, state) do + {:ok, s} = Client.Room.get_state(client(), state.id) + members = Enum.reduce(s, [], fn(s, acc) -> + if s["type"] == "m.room.member" do + if s["content"]["membership"] == "join" do + [s["user_id"] | acc] + else + # XXX: The user left, remove from IRC.Memberships ? + acc + end + else + acc + end + end) + |> Enum.filter(& &1) + + for m <- members, do: IRC.UserTrack.joined(state.id, %{network: "matrix", nick: m, user: m, host: "matrix."}, [], true) + + accounts = IRC.UserTrack.channel(state.network, state.channel) + |> Enum.filter(& &1) + |> Enum.map(fn(tuple) -> IRC.UserTrack.User.from_tuple(tuple).account end) + |> Enum.uniq() + |> Enum.each(fn(account_id) -> + introduce_irc_account(account_id, state) + end) + + {:noreply, state} + end + + def handle_info({:irc, :text, message}, state), do: handle_irc(message, state) + def handle_info({:irc, :out, message}, state), do: handle_irc(message, state) + def handle_info({:irc, :trigger, _, message}, state), do: handle_irc(message, state) + def handle_info({:irc, :event, event}, state), do: handle_irc(event, state) + def handle_info({:matrix_event, event}, state) do + if myself?(event.user_id) do + {:noreply, state} + else + handle_matrix(event, state) + end + end + + def handle_irc(message = %IRC.Message{account: account}, state) do + unless Map.get(message.meta, :puppet) && Map.get(message.meta, :from) == self() do + opts = if Map.get(message.meta, :self) || is_nil(account) do + [] + else + mxid = Matrix.get_or_create_matrix_user(account.id) + [user_id: mxid] + end + Client.Room.send_message(client(opts),state.id, message.text) + end + {:noreply, state} + end + + def handle_irc(%{type: :join, account_id: account_id}, state) do + introduce_irc_account(account_id, state) + {:noreply, state} + end + + def handle_irc(%{type: quit_or_part, account_id: account_id}, state) when quit_or_part in [:quit, :part] do + mxid = Matrix.get_or_create_matrix_user(account_id) + Client.Room.leave(client(user_id: mxid), state.id) + {:noreply, state} + end + + + def handle_irc(event, state) do + Logger.warn("Skipped irc event #{inspect event}") + {:noreply, state} + end + + def handle_matrix(event = %{type: "m.room.member", user_id: user_id, content: %{"membership" => "join"}}, state) do + _account = get_account(event, state) + IRC.UserTrack.joined(state.id, %{network: "matrix", nick: user_id, user: user_id, host: "matrix."}, [], true) + {:noreply, state} + end + + def handle_matrix(event = %{type: "m.room.member", user_id: user_id, content: %{"membership" => "leave"}}, state) do + IRC.UserTrack.parted(state.id, %{network: "matrix", nick: user_id}) + {:noreply, state} + end + + def handle_matrix(event = %{type: "m.room.message", user_id: user_id, content: %{"msgtype" => "m.text", "body" => text}}, state) do + IRC.send_message_as(get_account(event, state), state.network, state.channel, text, true) + {:noreply, state} + end + + def handle_matrix(event, state) do + Logger.warn("Skipped matrix event #{inspect event}") + {:noreply, state} + end + + def get_account(%{user_id: user_id}, %{id: id}) do + IRC.Account.find_by_nick("matrix", user_id) + end + + defp introduce_irc_account(account_id, state) do + mxid = Matrix.get_or_create_matrix_user(account_id) + account = IRC.Account.get(account_id) + user = IRC.UserTrack.find_by_account(state.network, account) + base_nick = if(user, do: user.nick, else: account.name) + case Client.Profile.put_displayname(client(user_id: mxid), base_nick) do + :ok -> :ok + error -> + Logger.warn("Failed to update profile for #{mxid}: #{inspect error}") + end + case Client.Room.join(client(user_id: mxid), state.id) do + {:ok, _} -> :ok + error -> + Logger.warn("Failed to join room for #{mxid}: #{inspect error}") + end + :ok + end + +end |