# Copyright 2021 Hubert Chathi # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Polyjuice.Util.URI do @doc """ Functions for using Matrix URIs Supports `matrix.to`, `matrix:`, and `mxc:` URIs, and supports referencing users, rooms (by ID or alias), events (using room ID or room alias), and media (`mxc:` URIs only). """ import URI, only: [encode_www_form: 1] @typedoc """ For users, room IDs and room aliases, `identifier` is a string. For events, it is of the form `{room_id_or_alias, event_id}`. And for media, it is of the form `{server, id}`. """ @type t() :: %__MODULE__{ type: :user | :event | :event_by_room_alias | :room_id | :room_alias | :media, identifier: String.t() | {String.t(), String.t()}, query_params: list({String.t(), String.t()}) } @enforce_keys [:type, :identifier] defstruct [:type, :identifier, query_params: []] @doc ~S""" Parse a Matrix URI. ## Examples iex> Polyjuice.Util.URI.parse("matrix:u/hubert:uhoreg.ca") {:ok, %Polyjuice.Util.URI{ type: :user, identifier: "@hubert:uhoreg.ca", query_params: [] }} iex> Polyjuice.Util.URI.parse("https://matrix.to/#/%40hubert:uhoreg.ca") {:ok, %Polyjuice.Util.URI{ type: :user, identifier: "@hubert:uhoreg.ca", query_params: [] }} """ @spec parse(uri :: String.t() | URI.t()) :: {:ok, Polyjuice.Util.URI.t()} | {:error, atom} def parse(uri) when is_binary(uri) do parse(URI.parse(uri)) end def parse(%URI{scheme: "mxc", host: host, path: path}) do path = String.trim_leading(path, "/") {:ok, %Polyjuice.Util.URI{ type: :media, identifier: {host, URI.decode(path)}, query_params: [] }} end def parse(%URI{scheme: scheme, host: "matrix.to", fragment: fragment}) when scheme == "https" or scheme == "http" do fragment = String.trim_leading(fragment, "/") {location, query} = case String.split(fragment, "?", parts: 2) do [l, q] -> {l, URI.query_decoder(q) |> Enum.to_list()} [l] -> {l, []} end location = case location do <> -> <> _ -> location end case location do <> -> {:ok, %Polyjuice.Util.URI{ type: :user, identifier: URI.decode(location), query_params: query }} <> -> case String.split(location, "/", parts: 2) do [_] -> {:ok, %Polyjuice.Util.URI{ type: :room_alias, identifier: URI.decode(location), query_params: query }} [room, event] -> {:ok, %Polyjuice.Util.URI{ type: :event_by_room_alias, identifier: {URI.decode(room), URI.decode(event)}, query_params: query }} end <> -> case String.split(location, "/", parts: 2) do [_] -> {:ok, %Polyjuice.Util.URI{ type: :room_id, identifier: URI.decode(location), query_params: query }} [room, event] -> {:ok, %Polyjuice.Util.URI{ type: :event, identifier: {URI.decode(room), URI.decode(event)}, query_params: query }} end _ -> {:error, :unknown_sigil} end end def parse(%URI{scheme: "matrix", path: path, query: query}) do [type, identifier | rest] = String.split(path, "/") query = if query, do: URI.query_decoder(query) |> Enum.to_list(), else: [] case type do "u" -> if rest == [] do {:ok, %Polyjuice.Util.URI{ type: :user, identifier: "@" <> URI.decode(identifier), query_params: query }} else {:error, :unknown_format} end "r" -> case rest do ["e", event] -> {:ok, %Polyjuice.Util.URI{ type: :event_by_room_alias, identifier: {"#" <> URI.decode(identifier), "$" <> URI.decode(event)}, query_params: query }} [] -> {:ok, %Polyjuice.Util.URI{ type: :room_alias, identifier: "#" <> URI.decode(identifier), query_params: query }} _ -> {:error, :unknown_format} end "roomid" -> case rest do ["e", event] -> {:ok, %Polyjuice.Util.URI{ type: :event, identifier: {"!" <> URI.decode(identifier), "$" <> URI.decode(event)}, query_params: query }} [] -> {:ok, %Polyjuice.Util.URI{ type: :room_id, identifier: "!" <> URI.decode(identifier), query_params: query }} _ -> {:error, :unknown_format} end _ -> {:error, :unknown_format} end end def parse(_, _) do {:error, :unknown_format} end @doc ~S""" Generate a Matrix URI. `format` indicates what URI format to generate. Currently, it defaults to `matrix_to`; in the future when the `matrix` format is supported by more clients, the default may change to that. If you require a certain format, you should specify it rather than relying on the default behaviour. `:media` URIs will always be `mxc:` URIs, regardless of the `format`. ## Examples iex> Polyjuice.Util.URI.to_string( ...> %Polyjuice.Util.URI{type: :user, identifier: "@hubert:uhoreg.ca"}, ...> :matrix ...> ) "matrix:u/hubert%3Auhoreg.ca" iex> Polyjuice.Util.URI.to_string( ...> %Polyjuice.Util.URI{type: :user, identifier: "@hubert:uhoreg.ca"}, ...> :matrix_to ...> ) "https://matrix.to/#/%40hubert%3Auhoreg.ca" """ @spec to_string(uri :: Polyjuice.Util.URI.t(), format :: :matrix | :matrix_to | nil) :: String.t() def to_string(uri, format \\ :matrix_to) def to_string(%Polyjuice.Util.URI{} = uri, nil) do Polyjuice.Util.URI.to_string(uri, :matrix_to) end def to_string(%Polyjuice.Util.URI{type: :media, identifier: {server, id}}, _) when is_binary(server) and is_binary(id) do "mxc://#{encode_www_form(server)}/#{encode_www_form(id)}" end def to_string(%Polyjuice.Util.URI{} = uri, :matrix) do query_part = case uri.query_params do nil -> "" [] -> "" x when is_list(x) -> [??, URI.encode_query(uri.query_params)] end case uri do %{type: :user, identifier: <>} -> "matrix:u/#{URI.encode_www_form(unprefixed)}#{query_part}" %{ type: :event, identifier: {<>, <>} } -> "matrix:roomid/#{URI.encode_www_form(unprefixed_room)}/e/#{ URI.encode_www_form(unprefixed_event) }#{query_part}" %{ type: type, identifier: {<>, <>} } when type == :event or type == :event_by_room_alias -> "matrix:r/#{URI.encode_www_form(unprefixed_room)}/e/#{ URI.encode_www_form(unprefixed_event) }#{query_part}" %{type: :room_id, identifier: <>} -> "matrix:roomid/#{URI.encode_www_form(unprefixed_room)}#{query_part}" %{type: :room_alias, identifier: <>} -> "matrix:r/#{URI.encode_www_form(unprefixed_room)}#{query_part}" end end def to_string(%Polyjuice.Util.URI{} = uri, :matrix_to) do query_part = case uri.query_params do nil -> "" [] -> "" x when is_list(x) -> [??, URI.encode_query(uri.query_params)] end case uri.identifier do identifier when is_binary(identifier) -> "https://matrix.to/#/#{URI.encode_www_form(identifier)}#{query_part}" {first, second} -> "https://matrix.to/#/#{URI.encode_www_form(first)}/#{URI.encode_www_form(second)}#{ query_part }" end end defimpl String.Chars do def to_string(uri) do Polyjuice.Util.URI.to_string(uri) end end end