Packages

Utility functions for the Matrix.org communications protocol

Current section

Files

Jump to
polyjuice_util lib polyjuice util uri.ex
Raw

lib/polyjuice/util/uri.ex

# Copyright 2021 Hubert Chathi <hubert@uhoreg.ca>
#
# 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
<<?%, esc_sigil::binary-size(2), rest::binary>> ->
<<String.to_integer(esc_sigil, 16), rest::binary>>
_ ->
location
end
case location do
<<?@, _rest::binary>> ->
{:ok,
%Polyjuice.Util.URI{
type: :user,
identifier: URI.decode(location),
query_params: query
}}
<<?\#, _rest::binary>> ->
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
<<?!, _rest::binary>> ->
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: <<?@, unprefixed::binary>>} ->
"matrix:u/#{URI.encode_www_form(unprefixed)}#{query_part}"
%{
type: :event,
identifier: {<<?!, unprefixed_room::binary>>, <<?$, unprefixed_event::binary>>}
} ->
"matrix:roomid/#{URI.encode_www_form(unprefixed_room)}/e/#{
URI.encode_www_form(unprefixed_event)
}#{query_part}"
%{
type: type,
identifier: {<<?\#, unprefixed_room::binary>>, <<?$, unprefixed_event::binary>>}
}
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: <<?!, unprefixed_room::binary>>} ->
"matrix:roomid/#{URI.encode_www_form(unprefixed_room)}#{query_part}"
%{type: :room_alias, identifier: <<?\#, unprefixed_room::binary>>} ->
"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