# Portions of this file are derived from Pleroma: # Copyright © 2017-2018 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only # Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/web_finger/web_finger.ex defmodule Mobilizon.Federation.WebFinger do @moduledoc """ Performs the WebFinger requests and responses (JSON only). """ alias Mobilizon.Actors alias Mobilizon.Actors.Actor alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.WebFinger.XmlBuilder alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Router.Helpers, as: Routes require Jason require Logger @http_options [ follow_redirect: true, ssl: [{:versions, [:"tlsv1.2"]}] ] def host_meta do base_url = Endpoint.url() { :XRD, %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, { :Link, %{ rel: "lrdd", type: "application/xrd+xml", template: "#{base_url}/.well-known/webfinger?resource={uri}" } } } |> XmlBuilder.to_doc() end def webfinger(resource, "JSON") do host = Endpoint.host() regex = ~r/(acct:)?(?\w+)@#{host}/ with %{"name" => name} <- Regex.named_captures(regex, resource), %Actor{} = actor <- Actors.get_local_actor_by_name(name) do {:ok, represent_actor(actor, "JSON")} else _e -> case ActivityPub.get_or_fetch_actor_by_url(resource) do {:ok, %Actor{} = actor} when not is_nil(actor) -> {:ok, represent_actor(actor, "JSON")} _e -> {:error, "Couldn't find actor"} end end end @spec represent_actor(Actor.t()) :: struct() def represent_actor(actor), do: represent_actor(actor, "JSON") @spec represent_actor(Actor.t(), String.t()) :: struct() def represent_actor(actor, "JSON") do %{ "subject" => "acct:#{actor.preferred_username}@#{Endpoint.host()}", "aliases" => [actor.url], "links" => [ %{"rel" => "self", "type" => "application/activity+json", "href" => actor.url}, %{ "rel" => "https://webfinger.net/rel/profile-page/", "type" => "text/html", "href" => actor.url }, %{ "rel" => "http://ostatus.org/schema/1.0/subscribe", "template" => "#{Routes.page_url(Endpoint, :interact, uri: nil)}{uri}" } ] } end defp webfinger_from_json(doc) do data = Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data -> case {link["type"], link["rel"]} do {"application/activity+json", "self"} -> Map.put(data, "url", link["href"]) _ -> Logger.debug(fn -> "Unhandled type: #{inspect(link["type"])}" end) data end end) {:ok, data} end def finger(actor) do actor = String.trim_leading(actor, "@") domain = case String.split(actor, "@") do [_name, domain] -> domain _e -> URI.parse(actor).host end address = "http://#{domain}/.well-known/webfinger?resource=acct:#{actor}" Logger.debug(inspect(address)) with false <- is_nil(domain), {:ok, %{} = response} <- Tesla.get( address, headers: [ {"accept", "application/json, application/activity+json, application/jrd+json"} ], opts: @http_options ), %{status: status, body: body} when status in 200..299 <- response, {:ok, doc} <- Jason.decode(body) do webfinger_from_json(doc) else e -> Logger.debug(fn -> "Couldn't finger #{actor}" end) Logger.debug(fn -> inspect(e) end) {:error, e} end end end