summaryrefslogtreecommitdiff
path: root/lib/nola_matrix
diff options
context:
space:
mode:
authorJordan Bracco <href@random.sh>2022-12-20 00:21:54 +0000
committerJordan Bracco <href@random.sh>2022-12-20 19:29:41 +0100
commit2d83df8b32bff7f0028923bb5b64dc0b55f20d03 (patch)
tree1207e67b5b15f540963db05e7be89f3ca950e724 /lib/nola_matrix
parentNola rename, the end. pt 6. Refs T77. (diff)
Nola rename: The Big Move, Refs T77
Diffstat (limited to 'lib/nola_matrix')
-rw-r--r--lib/nola_matrix/matrix.ex169
-rw-r--r--lib/nola_matrix/plug.ex25
-rw-r--r--lib/nola_matrix/room.ex196
3 files changed, 390 insertions, 0 deletions
diff --git a/lib/nola_matrix/matrix.ex b/lib/nola_matrix/matrix.ex
new file mode 100644
index 0000000..9334816
--- /dev/null
+++ b/lib/nola_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/nola_matrix/plug.ex b/lib/nola_matrix/plug.ex
new file mode 100644
index 0000000..c64ed11
--- /dev/null
+++ b/lib/nola_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/nola_matrix/room.ex b/lib/nola_matrix/room.ex
new file mode 100644
index 0000000..c790760
--- /dev/null
+++ b/lib/nola_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