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.Actor, as: ActorModel alias Mobilizon.Federation.ActivityPub.Utils alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Service.HTTP.RemoteMediaDownloaderClient alias Mobilizon.Service.RichMedia.Parser alias Mobilizon.Web.Upload @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()) :: {:ok, map()} def as_to_model_data(%{"type" => type} = data) when type in @allowed_types do avatar = download_picture(get_in(data, ["icon", "url"]), get_in(data, ["icon", "name"]), "avatar") banner = download_picture(get_in(data, ["image", "url"]), get_in(data, ["image", "name"]), "banner") %{ 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"], members_url: data["members"], resources_url: data["resources"], todos_url: data["todos"], events_url: data["events"], posts_url: data["posts"], discussions_url: data["discussions"], shared_inbox_url: data["endpoints"]["sharedInbox"], domain: URI.parse(data["id"]).host, manually_approves_followers: data["manuallyApprovesFollowers"], type: data["type"], visibility: if(Map.get(data, "discoverable", false) == true, do: :public, else: :unlisted), openness: data["openness"] } end def as_to_model_data(_), do: :error @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, "members" => actor.members_url, "resources" => actor.resources_url, "todos" => actor.todos_url, "posts" => actor.posts_url, "events" => actor.events_url, "discussions" => actor.discussions_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, "publicKey" => %{ "id" => "#{actor.url}#main-key", "owner" => actor.url, "publicKeyPem" => if(is_nil(actor.domain) and not is_nil(actor.keys), do: Utils.pem_to_public_key_pem(actor.keys), else: actor.keys ) } } actor_data = if actor.type == :Group do Map.put(actor_data, "members", actor.members_url) else actor_data end actor_data = if is_nil(actor.avatar) do actor_data else Map.put(actor_data, "icon", %{ "type" => "Image", "mediaType" => actor.avatar.content_type, "url" => actor.avatar.url }) end if is_nil(actor.banner) do actor_data else Map.put(actor_data, "image", %{ "type" => "Image", "mediaType" => actor.banner.content_type, "url" => actor.banner.url }) end end @spec download_picture(String.t() | nil, String.t(), String.t()) :: map() 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]) end end end