Federate client profile requests
This commit is contained in:
parent
213980a5be
commit
f565383468
5 changed files with 125 additions and 42 deletions
|
@ -174,9 +174,9 @@ defmodule Architex do
|
||||||
@doc """
|
@doc """
|
||||||
Validate a changeset's field where the reason for invalidation is not needed.
|
Validate a changeset's field where the reason for invalidation is not needed.
|
||||||
"""
|
"""
|
||||||
@spec validate_change_simple(Ecto.Changeset.t(), atom(), (term() -> boolean())) ::
|
@spec validate_change_truthy(Ecto.Changeset.t(), atom(), (term() -> boolean())) ::
|
||||||
Ecto.Changeset.t()
|
Ecto.Changeset.t()
|
||||||
def validate_change_simple(changeset, field, func) do
|
def validate_change_truthy(changeset, field, func) do
|
||||||
augmented_func = fn _, val ->
|
augmented_func = fn _, val ->
|
||||||
if func.(val), do: [], else: [{field, "invalid"}]
|
if func.(val), do: [], else: [{field, "invalid"}]
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@ defmodule ArchitexWeb.Client.ProfileController do
|
||||||
|
|
||||||
alias Architex.{Repo, Account}
|
alias Architex.{Repo, Account}
|
||||||
alias Architex.Types.UserId
|
alias Architex.Types.UserId
|
||||||
|
alias ArchitexWeb.Federation.HTTPClient
|
||||||
alias Plug.Conn
|
alias Plug.Conn
|
||||||
alias Ecto.Changeset
|
alias Ecto.Changeset
|
||||||
|
|
||||||
|
@ -33,9 +34,16 @@ defmodule ArchitexWeb.Client.ProfileController do
|
||||||
put_error(conn, :not_found, "User was not found.")
|
put_error(conn, :not_found, "User was not found.")
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
# TODO: Use federation to lookup information.
|
case HTTPClient.client(domain) |> HTTPClient.query_profile(user_id) do
|
||||||
|
{:ok, response} ->
|
||||||
|
conn
|
||||||
|
|> put_status(200)
|
||||||
|
|> json(response)
|
||||||
|
|
||||||
|
{:error, _, _} ->
|
||||||
put_error(conn, :not_found, "User was not found.")
|
put_error(conn, :not_found, "User was not found.")
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
:error ->
|
:error ->
|
||||||
put_error(conn, :not_found, "User ID is invalid.")
|
put_error(conn, :not_found, "User ID is invalid.")
|
||||||
|
@ -77,9 +85,16 @@ defmodule ArchitexWeb.Client.ProfileController do
|
||||||
put_error(conn, :not_found, "User was not found.")
|
put_error(conn, :not_found, "User was not found.")
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
# TODO: Use federation to lookup information.
|
case HTTPClient.client(domain) |> HTTPClient.query_profile(user_id, Atom.to_string(property_key)) do
|
||||||
|
{:ok, response} ->
|
||||||
|
conn
|
||||||
|
|> put_status(200)
|
||||||
|
|> json(response)
|
||||||
|
|
||||||
|
{:error, _, _} ->
|
||||||
put_error(conn, :not_found, "User was not found.")
|
put_error(conn, :not_found, "User was not found.")
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
:error ->
|
:error ->
|
||||||
put_error(conn, :not_found, "User ID is invalid.")
|
put_error(conn, :not_found, "User ID is invalid.")
|
||||||
|
@ -108,7 +123,12 @@ defmodule ArchitexWeb.Client.ProfileController do
|
||||||
update_property(conn, :avatar_url, avatar_url, user_id)
|
update_property(conn, :avatar_url, avatar_url, user_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp update_property(%Conn{assigns: %{account: account}} = conn, property_key, property, user_id) do
|
defp update_property(
|
||||||
|
%Conn{assigns: %{account: account}} = conn,
|
||||||
|
property_key,
|
||||||
|
property,
|
||||||
|
user_id
|
||||||
|
) do
|
||||||
if Account.get_mxid(account) == user_id do
|
if Account.get_mxid(account) == user_id do
|
||||||
account
|
account
|
||||||
|> Changeset.change([{property_key, property}])
|
|> Changeset.change([{property_key, property}])
|
||||||
|
|
|
@ -38,15 +38,27 @@ defmodule ArchitexWeb.Federation.QueryController do
|
||||||
Action for GET /_matrix/federation/v1/query/profile.
|
Action for GET /_matrix/federation/v1/query/profile.
|
||||||
"""
|
"""
|
||||||
def profile(conn, params) do
|
def profile(conn, params) do
|
||||||
with {:ok, %ProfileRequest{user_id: %UserId{localpart: localpart, domain: domain}}} <-
|
with {:ok,
|
||||||
|
%ProfileRequest{user_id: %UserId{localpart: localpart, domain: domain}, field: field}} <-
|
||||||
ProfileRequest.validate(params) do
|
ProfileRequest.validate(params) do
|
||||||
if domain == Architex.server_name() do
|
if domain == Architex.server_name() do
|
||||||
case Repo.one(from a in Account, where: a.localpart == ^localpart) do
|
case Repo.one(from a in Account, where: a.localpart == ^localpart) do
|
||||||
%Account{} ->
|
%Account{displayname: displayname, avatar_url: avatar_url} ->
|
||||||
# TODO: Return displayname and avatar_url when we implement them.
|
data = %{}
|
||||||
|
|
||||||
|
data =
|
||||||
|
if field == "displayname" or (field == nil and displayname != nil),
|
||||||
|
do: Map.put(data, :displayname, displayname),
|
||||||
|
else: data
|
||||||
|
|
||||||
|
data =
|
||||||
|
if field == "avatar_url" or (field == nil and avatar_url != nil),
|
||||||
|
do: Map.put(data, :avatar_url, avatar_url),
|
||||||
|
else: data
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_status(200)
|
|> put_status(200)
|
||||||
|> json(%{})
|
|> json(data)
|
||||||
|
|
||||||
nil ->
|
nil ->
|
||||||
put_error(conn, :not_found, "User does not exist.")
|
put_error(conn, :not_found, "User does not exist.")
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
defmodule ArchitexWeb.Federation.HTTPClient do
|
defmodule ArchitexWeb.Federation.HTTPClient do
|
||||||
|
@moduledoc """
|
||||||
|
This module provides functions to interact with other homeservers
|
||||||
|
using the Matrix federation API.
|
||||||
|
"""
|
||||||
|
# TODO: Investigate request timeouts.
|
||||||
use Tesla
|
use Tesla
|
||||||
|
|
||||||
alias ArchitexWeb.Endpoint
|
alias ArchitexWeb.Endpoint
|
||||||
|
@ -6,11 +11,27 @@ defmodule ArchitexWeb.Federation.HTTPClient do
|
||||||
alias ArchitexWeb.Federation.Middleware.SignRequest
|
alias ArchitexWeb.Federation.Middleware.SignRequest
|
||||||
alias ArchitexWeb.Router.Helpers, as: RouteHelpers
|
alias ArchitexWeb.Router.Helpers, as: RouteHelpers
|
||||||
|
|
||||||
# TODO: Maybe create database-backed homeserver struct to pass to client function.
|
@type t :: schema_response() | map_response()
|
||||||
# TODO: Fix error propagation.
|
|
||||||
|
@type schema_response ::
|
||||||
|
{:ok, struct()}
|
||||||
|
| {:error, :status, Tesla.Env.t()}
|
||||||
|
| {:error, :validation, Ecto.Changeset.t()}
|
||||||
|
| {:error, :request, any()}
|
||||||
|
|
||||||
|
@type map_response ::
|
||||||
|
{:ok, map()}
|
||||||
|
| {:error, :status, Tesla.Env.t()}
|
||||||
|
| {:error, :validation, Ecto.Changeset.t()}
|
||||||
|
| {:error, :request, any()}
|
||||||
|
|
||||||
@adapter {Tesla.Adapter.Finch, name: ArchitexWeb.HTTPClient}
|
@adapter {Tesla.Adapter.Finch, name: ArchitexWeb.HTTPClient}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get a Tesla client for the given server name, to be used for
|
||||||
|
interacting with other homeservers.
|
||||||
|
"""
|
||||||
|
@spec client(String.t()) :: Tesla.Client.t()
|
||||||
def client(server_name) do
|
def client(server_name) do
|
||||||
Tesla.client(
|
Tesla.client(
|
||||||
[
|
[
|
||||||
|
@ -23,6 +44,10 @@ defmodule ArchitexWeb.Federation.HTTPClient do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get the signing keys of a homeserver.
|
||||||
|
"""
|
||||||
|
@spec get_signing_keys(Tesla.Client.t()) :: {:ok, GetSigningKeys.t()} | :error
|
||||||
def get_signing_keys(client) do
|
def get_signing_keys(client) do
|
||||||
path = RouteHelpers.key_path(Endpoint, :get_signing_keys)
|
path = RouteHelpers.key_path(Endpoint, :get_signing_keys)
|
||||||
|
|
||||||
|
@ -53,40 +78,61 @@ defmodule ArchitexWeb.Federation.HTTPClient do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get the profile of a user.
|
||||||
|
"""
|
||||||
|
@spec query_profile(Tesla.Client.t(), String.t(), String.t() | nil) :: map_response()
|
||||||
def query_profile(client, user_id, field \\ nil) do
|
def query_profile(client, user_id, field \\ nil) do
|
||||||
path = RouteHelpers.query_path(Endpoint, :profile) |> Tesla.build_url(user_id: user_id)
|
path = RouteHelpers.query_path(Endpoint, :profile) |> Tesla.build_url(user_id: user_id)
|
||||||
path = if field, do: Tesla.build_url(path, field: field), else: path
|
path = if field, do: Tesla.build_url(path, field: field), else: path
|
||||||
|
|
||||||
Tesla.get(client, path)
|
tesla_request(:get, client, path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_event(client, event_id) do
|
# def get_event(client, event_id) do
|
||||||
path = RouteHelpers.event_path(Endpoint, :event, event_id)
|
# path = RouteHelpers.event_path(Endpoint, :event, event_id)
|
||||||
|
|
||||||
Tesla.get(client, path)
|
# Tesla.get(client, path)
|
||||||
|
# end
|
||||||
|
|
||||||
|
# def get_state(client, room_id, event_id) do
|
||||||
|
# path =
|
||||||
|
# RouteHelpers.event_path(Endpoint, :state, room_id) |> Tesla.build_url(event_id: event_id)
|
||||||
|
|
||||||
|
# Tesla.get(client, path)
|
||||||
|
# end
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Tesla.get(client, path)
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Perform a Tesla request and validate the response with the given
|
||||||
|
# Ecto schema struct.
|
||||||
|
@spec tesla_request(atom(), Tesla.Client.t(), String.t(), module()) :: t()
|
||||||
|
defp tesla_request(method, client, path, request_schema \\ nil) do
|
||||||
|
case Tesla.request(client, url: path, method: method) do
|
||||||
|
{:ok, %Tesla.Env{status: status} = env} when status != 200 ->
|
||||||
|
{:error, :status, env}
|
||||||
|
|
||||||
|
{:ok, %Tesla.Env{body: body}} ->
|
||||||
|
if request_schema do
|
||||||
|
case apply(request_schema, :parse, [body]) do
|
||||||
|
{:ok, response} ->
|
||||||
|
{:ok, response}
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
{:error, :validation, changeset}
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_state(client, room_id, event_id) do
|
|
||||||
path =
|
|
||||||
RouteHelpers.event_path(Endpoint, :state, room_id) |> Tesla.build_url(event_id: event_id)
|
|
||||||
|
|
||||||
Tesla.get(client, path)
|
|
||||||
end
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
Tesla.get(client, path)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp tesla_request(method, client, path, request_schema) do
|
|
||||||
with {:ok, %Tesla.Env{body: body}} <- Tesla.request(client, url: path, method: method),
|
|
||||||
%Ecto.Changeset{valid?: true} = cs <- apply(request_schema, :changeset, [body]) do
|
|
||||||
{:ok, Ecto.Changeset.apply_changes(cs)}
|
|
||||||
else
|
else
|
||||||
_ -> :error
|
{:ok, body}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, tesla_error} ->
|
||||||
|
{:error, :request, tesla_error}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
defmodule ArchitexWeb.Federation.Request.GetSigningKeys do
|
defmodule ArchitexWeb.Federation.Request.GetSigningKeys do
|
||||||
use Ecto.Schema
|
use ArchitexWeb.Request
|
||||||
|
|
||||||
import Ecto.Changeset
|
@type t :: %__MODULE__{
|
||||||
|
server_name: String.t(),
|
||||||
|
verify_keys: %{optional(String.t()) => %{String.t() => String.t()}},
|
||||||
|
old_verify_keys: %{optional(String.t()) => map()},
|
||||||
|
signatures: %{optional(String.t()) => %{optional(String.t()) => String.t()}},
|
||||||
|
valid_until_ts: integer()
|
||||||
|
}
|
||||||
|
|
||||||
@primary_key false
|
@primary_key false
|
||||||
embedded_schema do
|
embedded_schema do
|
||||||
|
@ -12,17 +18,16 @@ defmodule ArchitexWeb.Federation.Request.GetSigningKeys do
|
||||||
field :valid_until_ts, :integer
|
field :valid_until_ts, :integer
|
||||||
end
|
end
|
||||||
|
|
||||||
def changeset(params) do
|
def changeset(data, params) do
|
||||||
# TODO: There must be a better way to validate embedded maps?
|
data
|
||||||
%__MODULE__{}
|
|
||||||
|> cast(params, [:server_name, :verify_keys, :old_verify_keys, :signatures, :valid_until_ts])
|
|> cast(params, [:server_name, :verify_keys, :old_verify_keys, :signatures, :valid_until_ts])
|
||||||
|> validate_required([:server_name, :verify_keys, :valid_until_ts])
|
|> validate_required([:server_name, :verify_keys, :valid_until_ts])
|
||||||
|> Architex.validate_change_simple(:verify_keys, fn map ->
|
|> Architex.validate_change_truthy(:verify_keys, fn map ->
|
||||||
Enum.all?(map, fn {_, map} ->
|
Enum.all?(map, fn {_, map} ->
|
||||||
is_map_key(map, "key")
|
is_map_key(map, "key")
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|> Architex.validate_change_simple(:old_verify_keys, fn map ->
|
|> Architex.validate_change_truthy(:old_verify_keys, fn map ->
|
||||||
Enum.all?(map, fn
|
Enum.all?(map, fn
|
||||||
{_, %{"key" => key, "expired_ts" => expired_ts}}
|
{_, %{"key" => key, "expired_ts" => expired_ts}}
|
||||||
when is_binary(key) and is_integer(expired_ts) ->
|
when is_binary(key) and is_integer(expired_ts) ->
|
||||||
|
|
Loading…
Reference in a new issue