debianize-mobilizon/lib/federation/activity_stream/converter/actor.ex
Thomas Citharel 5b337f952a
refactor(activitypub): handle failure finding public key in actor keys
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
2023-12-06 08:25:02 +01:00

225 lines
7.4 KiB
Elixir

defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
@moduledoc """
Actor converter.
This module allows to convert events from ActivityStream format to our own
internal one, and back.
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor, as: ActorModel
alias Mobilizon.Addresses.Address
alias Mobilizon.Federation.ActivityPub.Utils
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityStream.Converter.Address, as: AddressConverter
alias Mobilizon.Medias.File
alias Mobilizon.Service.HTTP.RemoteMediaDownloaderClient
alias Mobilizon.Service.RichMedia.Parser
alias Mobilizon.Web.Upload
import Mobilizon.Federation.ActivityStream.Converter.Utils, only: [get_address: 1]
import Mobilizon.Federation.ActivityPub.Utils, only: [create_full_domain_string: 1]
@behaviour Converter
defimpl Convertible, for: ActorModel do
alias Mobilizon.Federation.ActivityStream.Converter.Actor, as: ActorConverter
defdelegate model_to_as(actor), to: ActorConverter
end
@allowed_types ["Application", "Group", "Organization", "Person", "Service"]
@doc """
Converts an AP object data to our internal data structure.
"""
@impl Converter
@spec as_to_model_data(map()) :: map() | {:error, :actor_not_allowed_type}
def as_to_model_data(%{"type" => type} = data) when type in @allowed_types do
avatar =
download_picture(
get_picture(data, ["icon", "url"]),
get_picture(data, ["icon", "name"]),
"avatar"
)
banner =
download_picture(
get_picture(data, ["image", "url"]),
get_picture(data, ["image", "name"]),
"banner"
)
address = get_address(data["location"])
%{
url: data["id"],
avatar: avatar,
banner: banner,
name: data["name"],
preferred_username: data["preferredUsername"],
summary: data["summary"] || "",
keys: data["publicKey"]["publicKeyPem"],
inbox_url: data["inbox"],
outbox_url: data["outbox"],
following_url: data["following"],
followers_url: data["followers"],
domain: data["id"] |> URI.new!() |> create_full_domain_string(),
manually_approves_followers: data["manuallyApprovesFollowers"],
type: data["type"],
visibility: if(Map.get(data, "discoverable", false) == true, do: :public, else: :unlisted),
openness: data["openness"],
physical_address_id: if(address, do: address.id, else: nil)
}
|> add_endpoints_to_model(data)
end
def as_to_model_data(_), do: {:error, :actor_not_allowed_type}
defp add_endpoints_to_model(actor, data) do
# TODO: Remove fallbacks in 3.0
endpoints = %{
members_url: get_in(data, ["endpoints", "members"]) || data["members"],
resources_url: get_in(data, ["endpoints", "resources"]) || data["resources"],
todos_url: get_in(data, ["endpoints", "todos"]) || data["todos"],
events_url: get_in(data, ["endpoints", "events"]) || data["events"],
posts_url: get_in(data, ["endpoints", "posts"]) || data["posts"],
discussions_url: get_in(data, ["endpoints", "discussions"]) || data["discussions"],
shared_inbox_url: data["endpoints"]["sharedInbox"]
}
Map.merge(actor, endpoints)
end
@doc """
Convert an actor struct to an ActivityStream representation.
"""
@impl Converter
@spec model_to_as(ActorModel.t()) :: map()
def model_to_as(%ActorModel{} = actor) do
actor_data = %{
"id" => actor.url,
"type" => actor.type,
"preferredUsername" => actor.preferred_username,
"name" => actor.name,
"summary" => actor.summary || "",
"following" => actor.following_url,
"followers" => actor.followers_url,
"inbox" => actor.inbox_url,
"outbox" => actor.outbox_url,
"url" => actor.url,
"endpoints" => %{
"sharedInbox" => actor.shared_inbox_url
},
"discoverable" => actor.visibility == :public,
"openness" => actor.openness,
"manuallyApprovesFollowers" => actor.manually_approves_followers
}
actor_data
|> add_keys(actor)
|> add_endpoints(actor)
|> maybe_add_members(actor)
|> maybe_add_avatar_picture(actor)
|> maybe_add_banner_picture(actor)
|> maybe_add_physical_address(actor)
end
@spec add_keys(map(), ActorModel.t()) :: map()
defp add_keys(actor_data, %ActorModel{} = actor) do
keys =
if is_nil(actor.domain) and not is_nil(actor.keys) do
case Utils.pem_to_public_key_pem(actor.keys) do
{:error, :no_publickey_found} ->
raise "No publickey found in private keys"
public_key when is_binary(public_key) ->
public_key
end
else
actor.keys
end
Map.put(actor_data, "publicKey", %{
"id" => "#{actor.url}#main-key",
"owner" => actor.url,
"publicKeyPem" => keys
})
end
defp add_endpoints(%{"endpoints" => endpoints} = actor_data, %ActorModel{} = actor) do
new_endpoints = %{
"members" => actor.members_url,
"resources" => actor.resources_url,
"todos" => actor.todos_url,
"posts" => actor.posts_url,
"events" => actor.events_url,
"discussions" => actor.discussions_url
}
endpoints = Map.merge(endpoints, new_endpoints)
actor_data
|> Map.merge(new_endpoints)
|> Map.put("endpoints", endpoints)
end
@spec download_picture(String.t() | nil, String.t(), String.t()) :: map() | nil
defp download_picture(nil, _name, _default_name), do: nil
defp download_picture(url, name, default_name) do
with {:ok, %{body: body, status: code, headers: response_headers}}
when code in 200..299 <- RemoteMediaDownloaderClient.get(url),
name <- name || Parser.get_filename_from_response(response_headers, url) || default_name,
{:ok, file} <- Upload.store(%{body: body, name: name}) do
Map.take(file, [:content_type, :name, :url, :size])
else
_ -> nil
end
end
defp maybe_add_members(actor_data, %ActorModel{type: :Group, members_url: members_url} = group) do
actor_data
|> Map.put("members", members_url)
|> Map.put("memberCount", Actors.count_members_for_group(group))
end
defp maybe_add_members(actor_data, %ActorModel{}), do: actor_data
@spec maybe_add_avatar_picture(map(), ActorModel.t()) :: map()
defp maybe_add_avatar_picture(actor_data, %ActorModel{avatar: %File{} = avatar}) do
Map.put(actor_data, "icon", %{
"type" => "Image",
"mediaType" => avatar.content_type,
"url" => avatar.url
})
end
defp maybe_add_avatar_picture(res, %ActorModel{avatar: _}), do: res
@spec maybe_add_banner_picture(map(), ActorModel.t()) :: map()
defp maybe_add_banner_picture(actor_data, %ActorModel{banner: %File{} = banner}) do
Map.put(actor_data, "image", %{
"type" => "Image",
"mediaType" => banner.content_type,
"url" => banner.url
})
end
defp maybe_add_banner_picture(res, %ActorModel{banner: _}), do: res
@spec maybe_add_physical_address(map(), ActorModel.t()) :: map()
defp maybe_add_physical_address(res, %ActorModel{
physical_address: %Address{} = physical_address
}) do
Map.put(res, "location", AddressConverter.model_to_as(physical_address))
end
defp maybe_add_physical_address(res, _), do: res
defp get_picture(nil, _keys), do: nil
defp get_picture(url, _keys) when is_binary(url), do: url
defp get_picture(data, [key | rest] = keys) when is_map(data) and is_list(keys),
do: get_picture(data[key], rest)
end