Add some documentation

This commit is contained in:
Pim Kunis 2021-08-22 15:19:48 +02:00
parent a44595bb99
commit 9d40f8bc8b
5 changed files with 162 additions and 25 deletions

View file

@ -1,4 +1,8 @@
defmodule MatrixServer do
@moduledoc """
Utility functions used throughout the project.
"""
alias MatrixServer.EncodableMap
@random_string_alphabet Enum.into(?a..?z, []) ++ Enum.into(?A..?Z, [])
@ -6,30 +10,49 @@ defmodule MatrixServer do
@ipv4_regex ~r/^(?<hostname>[^:]+)(?<port>:\d{1,5})?$/
@dns_regex ~r/^[[:alnum:]-.]{1,255}$/
@doc """
Get the full MXID for a user's localpart, using the homeserver's
server name as the domain.
"""
@spec get_mxid(String.t()) :: String.t()
def get_mxid(localpart) when is_binary(localpart) do
"@#{localpart}:#{server_name()}"
end
@doc """
Get the homeserver's server name.
"""
@spec server_name() :: String.t()
def server_name do
Application.get_env(:matrix_server, :server_name)
end
@doc """
Get a regex to match a user's localpart.
"""
@spec localpart_regex() :: Regex.t()
def localpart_regex, do: ~r/^([a-z0-9\._=\/])+$/
@spec random_string(pos_integer()) :: String.t()
def random_string(length), do: random_string(length, @random_string_alphabet)
@doc """
Get a random string of `length` length and using `alphabet`'s characters.
"""
@spec random_string(pos_integer(), Enum.t()) :: String.t()
def random_string(length, alphabet) when length >= 1 do
def random_string(length, alphabet \\ @random_string_alphabet) when length >= 1 do
for _ <- 1..length, into: "", do: <<Enum.random(alphabet)>>
end
@doc """
Get the homeserver's default room version.
"""
@spec default_room_version() :: String.t()
def default_room_version, do: "7"
@doc """
Get the domain from an ID.
Return `nil` if no domain was found.
No validation on the domain is performed whatsoever.
"""
@spec get_domain(String.t()) :: String.t() | nil
def get_domain(id) do
case String.split(id, ":", parts: 2) do
@ -38,6 +61,12 @@ defmodule MatrixServer do
end
end
@doc """
Get the localpart from a MXID.
Return `nil` if no localpart was found.
No validation on the localpart is performed whatsoever.
"""
@spec get_localpart(String.t()) :: String.t() | nil
def get_localpart(id) do
with [part, _] <- String.split(id, ":", parts: 2),
@ -48,9 +77,12 @@ defmodule MatrixServer do
end
end
# https://elixirforum.com/t/22709/9
@doc """
Check whether the given list contains duplicates.
"""
@spec has_duplicates?(list()) :: boolean()
def has_duplicates?(list) do
# https://elixirforum.com/t/22709/9
list
|> Enum.reduce_while(%MapSet{}, fn x, acc ->
if MapSet.member?(acc, x), do: {:halt, false}, else: {:cont, MapSet.put(acc, x)}
@ -58,15 +90,25 @@ defmodule MatrixServer do
|> is_boolean()
end
# https://matrix.org/docs/spec/appendices#unpadded-base64
@doc """
Encode the given string using unpadded base64.
Unpadded base64 is the same as base64, except the "=" padding is removed.
"""
@spec encode_unpadded_base64(String.t()) :: String.t()
def encode_unpadded_base64(data) do
# https://matrix.org/docs/spec/appendices#unpadded-base64
data
|> Base.encode64()
|> String.trim_trailing("=")
end
# Decode (possibly unpadded) base64.
@doc """
Decode the given base64 string.
The base64 is allowed to be unpadded, as specified in `encode_unpadded_base64`.
Return `:error` on decode error.
"""
@spec decode_base64(String.t()) :: {:ok, String.t()} | :error
def decode_base64(data) when is_binary(data) do
rem = rem(String.length(data), 4)
@ -74,6 +116,13 @@ defmodule MatrixServer do
Base.decode64(padded_data)
end
@doc """
Encode the given map as canonical JSON.
See [the Matrix docs](https://matrix.org/docs/spec/appendices#canonical-json) for explanation
for canonical JSON.
Return an error if the map could not be encoded.
"""
@spec encode_canonical_json(map()) :: {:ok, String.t()} | {:error, Jason.EncodeError.t()}
def encode_canonical_json(object) do
object
@ -81,9 +130,13 @@ defmodule MatrixServer do
|> Jason.encode()
end
# https://stackoverflow.com/questions/41523762/41671211
@doc """
Convert a struct to a map, removing any schema or association fields,
as well as fields that have `nil` values.
"""
@spec to_serializable_map(struct()) :: map()
def to_serializable_map(struct) do
# https://stackoverflow.com/questions/41523762/41671211
association_fields =
if Kernel.function_exported?(struct.__struct__, :__schema__, 1) do
struct.__struct__.__schema__(:associations)
@ -101,6 +154,9 @@ defmodule MatrixServer do
|> Enum.into(%{})
end
@doc """
Serialize and encode the given struct.
"""
@spec serialize_and_encode(struct()) :: {:ok, String.t()} | {:error, Jason.EncodeError.t()}
def serialize_and_encode(struct) do
struct
@ -108,6 +164,11 @@ defmodule MatrixServer do
|> encode_canonical_json()
end
@doc """
Add a signature to the given map under the `:signatures` key.
If the map has no `:signatures` key, it is created.
"""
@spec add_signature(map(), String.t(), String.t()) :: map()
def add_signature(object, key_id, sig) when not is_map_key(object, :signatures) do
Map.put(object, :signatures, %{MatrixServer.server_name() => %{key_id => sig}})
@ -120,6 +181,9 @@ defmodule MatrixServer do
%{object | signatures: new_sigs}
end
@doc """
Validate a changeset's field where the reason for invalidation is not needed.
"""
@spec validate_change_simple(Ecto.Changeset.t(), atom(), (term() -> boolean())) ::
Ecto.Changeset.t()
def validate_change_simple(changeset, field, func) do
@ -130,8 +194,11 @@ defmodule MatrixServer do
Ecto.Changeset.validate_change(changeset, field, augmented_func)
end
# Returns a Boolean whether the signature is valid.
# Also returns false on ArgumentError.
@doc """
Return a Boolean whether the given signature is valid.
Returns `false` if `:enacl` throws an exception.
"""
@spec sign_verify(binary(), String.t(), binary()) :: boolean()
def sign_verify(sig, text, key) do
try do
@ -141,6 +208,9 @@ defmodule MatrixServer do
end
end
@doc """
Get the earliest of the two given `DateTime`s.
"""
@spec min_datetime(DateTime.t(), DateTime.t()) :: DateTime.t()
def min_datetime(datetime1, datetime2) do
if DateTime.compare(datetime1, datetime2) == :gt do
@ -150,6 +220,13 @@ defmodule MatrixServer do
end
end
@doc """
Encode the given string using URL-safe base64.
URL-safe base64 is the same was unpadded base64, except "+" is replaced by "-"
and "/" is replaced by "_".
See [the Matrix docs](https://spec.matrix.org/unstable/rooms/v4/) for more details.
"""
@spec encode_url_safe_base64(String.t()) :: String.t()
def encode_url_safe_base64(data) do
data
@ -158,6 +235,12 @@ defmodule MatrixServer do
|> String.replace("/", "_")
end
@doc """
Check whether the given domain (including port) is valid.
The domain could be a DNS name, IPv4 or IPv6 address.
See [the Matrix docs](https://matrix.org/docs/spec/appendices#server-name) for more details.
"""
@spec valid_domain?(String.t()) :: boolean()
def valid_domain?(domain) do
if String.starts_with?(domain, "[") do

View file

@ -1,4 +1,10 @@
defmodule MatrixServer.KeyServer do
@moduledoc """
A GenServer holding the homeserver's keys, and responsible for signing objects.
Currently, it only supports one key pair that cannot expire.
"""
use GenServer
# TODO: only support one signing key for now.
@ -10,11 +16,22 @@ defmodule MatrixServer.KeyServer do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Sign the given object using the homeserver's signing keys.
Return the signature and the key ID used.
On error, return `:error`.
"""
@spec sign_object(map()) :: {:ok, String.t(), String.t()} | :error
def sign_object(object) do
GenServer.call(__MODULE__, {:sign_object, object})
end
@doc """
Get the homeserver's signing keys.
Return a list of tuples, each holding the key ID and the key itself.
"""
@spec get_own_signing_keys() :: list({String.t(), binary()})
def get_own_signing_keys() do
GenServer.call(__MODULE__, :get_own_signing_keys)

View file

@ -1,4 +1,11 @@
defmodule MatrixServer.RoomServer do
@moduledoc """
A GenServer to hold and manipulate the state of a Matrix room.
Each RoomServer corresponds to one Matrix room that the homeserver participates in.
The RoomServers are supervised by a DynamicSupervisor RoomServer.Supervisor.
"""
use GenServer
import Ecto.Query
@ -18,17 +25,16 @@ defmodule MatrixServer.RoomServer do
GenServer.start_link(__MODULE__, opts, name: name)
end
@spec get_room_server(Room.t()) :: {:error, :not_found} | DynamicSupervisor.on_start_child()
def get_room_server(%Room{id: room_id}), do: get_room_server(room_id)
@doc """
Get the PID of the RoomServer for a room.
# Get room server pid, or spin one up for the room.
# If the room does not exist, return an error.
If the given room has no running RoomServer yet, it is created.
If the given room does not exist, an error is returned.
"""
@spec get_room_server(String.t()) :: {:error, :not_found} | DynamicSupervisor.on_start_child()
def get_room_server(room_id) do
# TODO: Might be wise to use a transaction here to prevent race conditions.
case Repo.one(from r in Room, where: r.id == ^room_id) do
nil ->
{:error, :not_found}
%Room{state: serialized_state_set} ->
case Registry.lookup(@registry, room_id) do
[{pid, _}] ->
@ -43,9 +49,19 @@ defmodule MatrixServer.RoomServer do
DynamicSupervisor.start_child(@supervisor, {__MODULE__, opts})
end
nil ->
{:error, :not_found}
end
end
@doc """
Create a new Matrix room.
The new room is created with the given `account` as creator.
Events are inserted into the new room depending on the input `input` and according
to the [Matrix documentation](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-createroom).
"""
@spec create_room(
pid(),
MatrixServer.Account.t(),
@ -55,16 +71,30 @@ defmodule MatrixServer.RoomServer do
GenServer.call(pid, {:create_room, account, input})
end
@spec server_in_room(pid(), String.t()) :: boolean()
def server_in_room(pid, domain) do
GenServer.call(pid, {:server_in_room, domain})
@doc """
Check whether the given server participates in a room.
Check whether any participant of the room has a server name matching
the given `domain`.
"""
@spec server_in_room?(pid(), String.t()) :: boolean()
def server_in_room?(pid, domain) do
GenServer.call(pid, {:server_in_room?, domain})
end
@doc """
Get the state of a room, before the given event was inserted.
Return a list of all state events and the auth chain.
"""
@spec get_state_at_event(pid(), Event.t()) :: {[Event.t()], [Event.t()]}
def get_state_at_event(pid, event) do
GenServer.call(pid, {:get_state_at_event, event})
end
@doc """
Same as `get_state_at_event/2`, except returns the lists as event IDs.
"""
@spec get_state_ids_at_event(pid(), Event.t()) :: {[String.t()], [String.t()]}
def get_state_ids_at_event(pid, event) do
GenServer.call(pid, {:get_state_ids_at_event, event})
@ -101,7 +131,7 @@ defmodule MatrixServer.RoomServer do
end
end
def handle_call({:server_in_room, domain}, _from, %{state_set: state_set} = state) do
def handle_call({:server_in_room?, domain}, _from, %{state_set: state_set} = state) do
result =
Enum.any?(state_set, fn
{{"m.room.member", user_id}, %Event{content: %{"membership" => "join"}}} ->

View file

@ -18,7 +18,7 @@ defmodule MatrixServerWeb.Federation.EventController do
%Event{room: room} = event ->
case RoomServer.get_room_server(room) do
{:ok, pid} ->
if RoomServer.server_in_room(pid, origin) do
if RoomServer.server_in_room?(pid, origin) do
data = Transaction.new([event])
conn
@ -57,7 +57,13 @@ defmodule MatrixServerWeb.Federation.EventController do
def state_ids(conn, _), do: put_error(conn, :missing_param)
@spec get_state_or_state_ids(Plug.Conn.t(), :state | :state_ids, String.t(), String.t(), String.t()) :: Plug.Conn.t()
@spec get_state_or_state_ids(
Plug.Conn.t(),
:state | :state_ids,
String.t(),
String.t(),
String.t()
) :: Plug.Conn.t()
defp get_state_or_state_ids(conn, state_or_state_ids, origin, event_id, room_id) do
query =
Event
@ -68,7 +74,7 @@ defmodule MatrixServerWeb.Federation.EventController do
%Event{room: room} = event ->
case RoomServer.get_room_server(room) do
{:ok, pid} ->
if RoomServer.server_in_room(pid, origin) do
if RoomServer.server_in_room?(pid, origin) do
{state_events, auth_chain} =
case state_or_state_ids do
:state -> RoomServer.get_state_at_event(pid, event)

View file

@ -75,7 +75,8 @@ defmodule MatrixServerWeb.Federation.HTTPClient do
def get_state_ids(client, room_id, event_id) do
path =
RouteHelpers.event_path(Endpoint, :state_ids, room_id) |> Tesla.build_url(event_id: event_id)
RouteHelpers.event_path(Endpoint, :state_ids, room_id)
|> Tesla.build_url(event_id: event_id)
Tesla.get(client, path)
end