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 """
|
||||
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()
|
||||
def validate_change_simple(changeset, field, func) do
|
||||
def validate_change_truthy(changeset, field, func) do
|
||||
augmented_func = fn _, val ->
|
||||
if func.(val), do: [], else: [{field, "invalid"}]
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@ defmodule ArchitexWeb.Client.ProfileController do
|
|||
|
||||
alias Architex.{Repo, Account}
|
||||
alias Architex.Types.UserId
|
||||
alias ArchitexWeb.Federation.HTTPClient
|
||||
alias Plug.Conn
|
||||
alias Ecto.Changeset
|
||||
|
||||
|
@ -33,8 +34,15 @@ defmodule ArchitexWeb.Client.ProfileController do
|
|||
put_error(conn, :not_found, "User was not found.")
|
||||
end
|
||||
else
|
||||
# TODO: Use federation to lookup information.
|
||||
put_error(conn, :not_found, "User was not found.")
|
||||
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.")
|
||||
end
|
||||
end
|
||||
|
||||
:error ->
|
||||
|
@ -77,8 +85,15 @@ defmodule ArchitexWeb.Client.ProfileController do
|
|||
put_error(conn, :not_found, "User was not found.")
|
||||
end
|
||||
else
|
||||
# TODO: Use federation to lookup information.
|
||||
put_error(conn, :not_found, "User was not found.")
|
||||
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.")
|
||||
end
|
||||
end
|
||||
|
||||
:error ->
|
||||
|
@ -108,7 +123,12 @@ defmodule ArchitexWeb.Client.ProfileController do
|
|||
update_property(conn, :avatar_url, avatar_url, user_id)
|
||||
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
|
||||
account
|
||||
|> Changeset.change([{property_key, property}])
|
||||
|
|
|
@ -38,15 +38,27 @@ defmodule ArchitexWeb.Federation.QueryController do
|
|||
Action for GET /_matrix/federation/v1/query/profile.
|
||||
"""
|
||||
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
|
||||
if domain == Architex.server_name() do
|
||||
case Repo.one(from a in Account, where: a.localpart == ^localpart) do
|
||||
%Account{} ->
|
||||
# TODO: Return displayname and avatar_url when we implement them.
|
||||
%Account{displayname: displayname, avatar_url: avatar_url} ->
|
||||
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
|
||||
|> put_status(200)
|
||||
|> json(%{})
|
||||
|> json(data)
|
||||
|
||||
nil ->
|
||||
put_error(conn, :not_found, "User does not exist.")
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
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
|
||||
|
||||
alias ArchitexWeb.Endpoint
|
||||
|
@ -6,11 +11,27 @@ defmodule ArchitexWeb.Federation.HTTPClient do
|
|||
alias ArchitexWeb.Federation.Middleware.SignRequest
|
||||
alias ArchitexWeb.Router.Helpers, as: RouteHelpers
|
||||
|
||||
# TODO: Maybe create database-backed homeserver struct to pass to client function.
|
||||
# TODO: Fix error propagation.
|
||||
@type t :: schema_response() | map_response()
|
||||
|
||||
@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}
|
||||
|
||||
@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
|
||||
Tesla.client(
|
||||
[
|
||||
|
@ -23,6 +44,10 @@ defmodule ArchitexWeb.Federation.HTTPClient do
|
|||
)
|
||||
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
|
||||
path = RouteHelpers.key_path(Endpoint, :get_signing_keys)
|
||||
|
||||
|
@ -53,40 +78,61 @@ defmodule ArchitexWeb.Federation.HTTPClient do
|
|||
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
|
||||
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
|
||||
|
||||
Tesla.get(client, path)
|
||||
tesla_request(:get, client, path)
|
||||
end
|
||||
|
||||
def get_event(client, event_id) do
|
||||
path = RouteHelpers.event_path(Endpoint, :event, event_id)
|
||||
# def get_event(client, event_id) do
|
||||
# path = RouteHelpers.event_path(Endpoint, :event, event_id)
|
||||
|
||||
Tesla.get(client, path)
|
||||
end
|
||||
# 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)
|
||||
# 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
|
||||
# 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)
|
||||
# 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
|
||||
# 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
|
||||
_ -> :error
|
||||
# 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
|
||||
else
|
||||
{:ok, body}
|
||||
end
|
||||
|
||||
{:error, tesla_error} ->
|
||||
{:error, :request, tesla_error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
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
|
||||
embedded_schema do
|
||||
|
@ -12,17 +18,16 @@ defmodule ArchitexWeb.Federation.Request.GetSigningKeys do
|
|||
field :valid_until_ts, :integer
|
||||
end
|
||||
|
||||
def changeset(params) do
|
||||
# TODO: There must be a better way to validate embedded maps?
|
||||
%__MODULE__{}
|
||||
def changeset(data, params) do
|
||||
data
|
||||
|> cast(params, [:server_name, :verify_keys, :old_verify_keys, :signatures, :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} ->
|
||||
is_map_key(map, "key")
|
||||
end)
|
||||
end)
|
||||
|> Architex.validate_change_simple(:old_verify_keys, fn map ->
|
||||
|> Architex.validate_change_truthy(:old_verify_keys, fn map ->
|
||||
Enum.all?(map, fn
|
||||
{_, %{"key" => key, "expired_ts" => expired_ts}}
|
||||
when is_binary(key) and is_integer(expired_ts) ->
|
||||
|
|
Loading…
Reference in a new issue