webfinger/finger: only accept authority of query domain

And permit refetching once(!) unless initial query
was already designated as the canonical authority.
(Only once to not get stuck in loops)

Fixes an oversight in c80aec05de.
The argument for why subjects from both authorities can be accepted
hinged on the assumption that only paths under direct control of the
domain operator are involved since both webfinger and host-meta
are /.well-known/ paths.
However, HTTP redirects or the LRDD schema inside the initial domains
host-meta may point at _any_ path on another domain, including e.g.
paths containing user uploads, thus enabling third-parties to
illegitimately claim handles from urelated domains, _if_ the victim
domain can be made to serve attacker-prepared JSON (e.g. via user
uploads or (media) proxies).

With trust being limited to initial domain and refetches we do not need
to make guesses about whether and when being redirected to indicates
authorisation of the final domain. It requires more fetch requests in
no-FEP-2c59 setups, but makes it more robust.
As a side effect current FEP-less Mastodon setups should happen to work.
This commit is contained in:
Oneric 2026-03-21 00:00:00 +00:00
commit eb361dd456
5 changed files with 388 additions and 77 deletions

View file

@ -47,9 +47,10 @@ Akkoma strongly encourages ActivityPub implementations to include
a FEP-2c59-compliant WebFinger backlink in their actor documents.
Without FEP-2c59 and if different domains are used for ActivityPub and the Webfinger subject,
Akkoma relies on the presence of an host-meta LRDD template on the ActivityPub domain
or a HTTP redirect from the ActivityPub domains `/.well-known/webfinger` to an equivalent endpoint
on the domain used in the `subject` to discover and validate the domain association.
Akkoma relies on either the presence of an host-meta LRDD template on the ActivityPub domain
or a working WebFinger endpoint on the ActivityPub domain. Additionally all WebFinger endpoints
related to the ActivityPub and canonical WebFinger domain SHOULD also respond to queries about
an alternative acct URI constructed with the WebFinger domain passed as the resource.
Without FEP-2c59 Akkoma may not become aware of changes to the
preferred WebFinger `subject` domain for already discovered users.

View file

@ -154,11 +154,9 @@ defmodule Pleroma.Web.WebFinger.Finger do
query_uri = make_finger_uri(domain, resource)
resp = HTTP.Backoff.get(query_uri, [{"accept", "application/xrd+xml,application/jrd+json"}])
with {:ok, %{url: resolved_uri, status: status} = resp_data} when status in 200..299 <- resp,
with {:ok, %{status: status} = resp_data} when status in 200..299 <- resp,
{_, {:ok, parsed_data}} <- {:parse, parse_finger_response(resp_data)} do
resolved_domain = URI.parse(resolved_uri).host
{:ok, resolved_domain, parsed_data}
{:ok, parsed_data}
else
{:ok, %Tesla.Env{} = env} -> {:error, map_fetch_error_reason(env)}
{:parse, {:error, _} = error} -> error
@ -180,6 +178,47 @@ defmodule Pleroma.Web.WebFinger.Finger do
end
end
# Parsed WebFinger response data with the subject acct URI (and thus parsed_subject and normalised_subject)
# being verified to be authorised by the domain in which authority it lies
# (i.e. make sure an "acct:user@domain.example" is acknowlledged by domain.example)
# Does NOT verify the actor pointed at in "ap_id" agrees to the handle!
defp finger_data_with_domainauth(domain, resource, allow_refetch \\ true) do
with {:ok, %{"subject" => finger_subject} = preparsed_data} <-
finger_unverified_data(domain, resource),
handle <- normalise_webfinger_handle(finger_subject),
{nick_user, nick_domain} <- parse_handle(handle),
{_, false} <- {:no_domain, nick_domain == nil},
# We cannot reliably accepted redirects as auth for the final domain.
# Thus only accepted result if matching the _initial_ domain, else allow a single refetch
# (traversing longer redirect chains may risk getting stuck in loops)
{_, true, _} <-
{:domainauth, nick_domain == domain, {nick_domain, resource_from_mention(handle)}} do
parsed_data =
preparsed_data
|> Map.put("normalised_subject", handle)
|> Map.put("parsed_subject", {nick_user, nick_domain})
{:ok, parsed_data}
else
{:domainauth, _, {nick_domain, new_resource}} ->
if allow_refetch do
finger_data_with_domainauth(nick_domain, new_resource)
else
Logger.error(
"Spoofed WebFinger response: #{inspect(domain)} responded with subject from #{inspect(nick_domain)} when no alias was expected!"
)
{:error, :finger_domain_spoof}
end
{:no_domain, _} ->
{:error, :no_domain}
error ->
error
end
end
@doc """
Discovers and verifies the WebFinger handle of an ActivityPub actor for use as a nickname.
If the actor or instance does not use WebFinger or just temporarily unavailable no value
@ -199,15 +238,10 @@ defmodule Pleroma.Web.WebFinger.Finger do
with {_, false} <- {:no_domain, domain == nil || ap_domain == nil},
{_, false} <- {:matching_domain, domain == ap_domain},
# We check for an exact match to the preferred handle which will ALWAYS
# belong to the initial query domain, thus we do not need to consider the final domain here.
# If the query domain delegates to another domain via host-meta or HTTP redirects on
# ./well-known/ paths (which ought to be directly controlled by the operator),
# this clearly indicates consent of the query domain to allow the final domain to manage this data
{_, {:ok, _, %{"ap_id" => fingered_ap_id, "subject" => finger_subject}}} <-
{:query, finger_unverified_data(domain, ap_id)},
# Since we already query the preferred domain, no refetches ought to be necessary
{_, {:ok, %{"ap_id" => fingered_ap_id, "normalised_subject" => finger_handle}}} <-
{:query, finger_data_with_domainauth(domain, ap_id, false)},
{_, false} <- {:fingered_data_mismatch, ap_id != fingered_ap_id},
finger_handle <- normalise_webfinger_handle(finger_subject),
{_, false} <- {:fingered_data_mismatch, preferred_handle != finger_handle} do
{:ok, preferred_handle}
else
@ -226,18 +260,15 @@ defmodule Pleroma.Web.WebFinger.Finger do
ap_domain = URI.parse(ap_id).host
with {_, false} <- {:no_domain, ap_domain == nil},
{_, {:ok, finger_domain, %{"ap_id" => fingered_ap_id, "subject" => finger_subject}}} <-
{:query, finger_unverified_data(ap_domain, ap_id)},
{_,
{:ok,
%{
"ap_id" => fingered_ap_id,
"normalised_subject" => handle,
"parsed_subject" => {nick_user, _}
}}} <-
{:query, finger_data_with_domainauth(ap_domain, ap_id)},
{_, false} <- {:fingered_data_mismatch, fingered_ap_id != ap_id},
handle <- normalise_webfinger_handle(finger_subject),
{nick_user, nick_domain} <- parse_handle(handle),
# Mastodon in its infinite wisdom encourages setups for custom WebFinger domains,
# such that the actual WebFinger response is _never_ served directly from the domain used in handles.
# Unlike in domain authority checks for AP IDs, here only fixed /.well-known URLs are queried,
# thus a redirect on this endpoint can be considered an approval from the redirecting domain
# (but not the redirected-to domain!) and it should be safe to accept both domain authorities here.
{_, false} <-
{:finger_domain_spoof, nick_domain != finger_domain && nick_domain != ap_domain},
ap_name <- actor_data["preferredUsername"],
{_, false} <- {:fingered_data_mismatch, ap_name != nil && ap_name != nick_user} do
{:ok, handle}
@ -305,16 +336,17 @@ defmodule Pleroma.Web.WebFinger.Finger do
resource = resource_from_mention(mention_handle)
with {_, false} <- {:invalid_handle, qname == nil || qdomain == nil},
{_, {:ok, finger_domain, %{"ap_id" => fingered_ap_id, "subject" => finger_subject}}} <-
{:query, finger_unverified_data(qdomain, resource)},
handle <- normalise_webfinger_handle(finger_subject),
{nick_user, nick_domain} <- parse_handle(handle),
# see comment in finger_actor for why both domains can and need to be accepted
{_, false} <-
{:finger_domain_spoof, nick_domain != finger_domain && nick_domain != qdomain},
{_,
{:ok,
%{
"ap_id" => fingered_ap_id,
"normalised_subject" => handle,
"parsed_subject" => {nick_user, nick_domain}
}}} <-
{:query, finger_data_with_domainauth(qdomain, resource)},
{_, {:ok, data}} <-
{:fetch, Fetcher.fetch_and_contain_remote_object_from_id(fingered_ap_id)} do
verify_ap_data_from_finger(data, handle, finger_domain, nick_user)
verify_ap_data_from_finger(data, handle, nick_domain, nick_user)
else
{:query, error} -> error
{:fetch, error} -> error
@ -339,7 +371,7 @@ defmodule Pleroma.Web.WebFinger.Finger do
end
with {_, domain} when is_binary(domain) <- {:domain, domain},
{:ok, _, data} <- finger_unverified_data(domain, resource) do
{:ok, data} <- finger_unverified_data(domain, resource) do
{:ok, data}
else
{:domain, _} -> {:error, :no_domain}

View file

@ -56,11 +56,17 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
end
describe "finger_mention/1" do
test "accepts content in authority of final domain" do
test "follows subjects to canonical domain to verify" do
# Not FEP-2c59, but otherwise one possible sane setup
Tesla.Mock.mock(fn
%{url: "https://fedi.example.com/.well-known/webfinger?resource=" <> rsrc}
when rsrc in ["acct:user@fedi.example.com", "https://fedi.example.com/users/user"] ->
%{url: url}
when url in [
"https://fedi.example.com/.well-known/webfinger?resource=" <>
"acct:user@fedi.example.com",
"https://fedi.example.com/.well-known/webfinger?resource=" <>
"https://fedi.example.com/users/user",
"https://example.com/.well-known/webfinger?resource=" <> "acct:user@example.com"
] ->
{:ok,
%Tesla.Env{
status: 200,
@ -97,10 +103,110 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
{:ok, "user@example.com", _} = Finger.finger_mention("user@fedi.example.com")
end
test "rejects spoof attempt via redirect to untrusted cross-domain path when handle does not exist on authorative domain" do
Tesla.Mock.mock(fn
%{
url:
"https://stinkyplace.example/.well-known/webfinger?resource=acct:doppelgaenger@stinkyplace.example"
} ->
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/webfinger/pleroma-webfinger.json")
|> String.replace("{{domain}}", "shinyplace.example")
|> String.replace("{{nickname}}", "doppelgaenger")
|> String.replace("{{subdomain}}", "stinkyplace.example"),
headers: [{"content-type", "application/jrd+json"}],
url: "https://shinyplace.example/user-uploads/123/webfinger.json"
}}
%{url: url}
when url in [
"https://stinkyplace.example/.well-known/host-meta",
"https://shinyplace.example/.well-known/host-meta",
"https://shinyplace.example/.well-known/webfinger?resource=acct:doppelgaenger@shinyplace.example"
] ->
{:ok, %Tesla.Env{status: 404, url: url}}
end)
assert {:error, :not_found} =
Finger.finger_mention("@doppelgaenger@stinkyplace.example")
end
test "only uses data from authorative domain when refetching" do
Tesla.Mock.mock(fn
%{
url:
"https://stinkyplace.example/.well-known/webfinger?resource=acct:doppelgaenger@stinkyplace.example"
} ->
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/webfinger/pleroma-webfinger.json")
|> String.replace("{{domain}}", "shinyplace.example")
|> String.replace("{{nickname}}", "doppelgaenger")
|> String.replace("{{subdomain}}", "stinkyplace.example"),
headers: [{"content-type", "application/jrd+json"}],
url: "https://shinyplace.example/user-uploads/123/webfinger.json"
}}
%{
url:
"https://shinyplace.example/.well-known/webfinger?resource=acct:doppelgaenger@shinyplace.example" =
url
} ->
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/webfinger/pleroma-webfinger.json")
|> String.replace("{{domain}}", "shinyplace.example")
|> String.replace("{{nickname}}", "doppelgaenger")
|> String.replace("{{subdomain}}", "shinyplace.example"),
headers: [{"content-type", "application/jrd+json"}],
url: url
}}
%{url: "https://shinyplace.example/users/doppelgaenger" = url} ->
{:ok,
%Tesla.Env{
status: 200,
url: url,
headers: [{"content-type", "application/activity+json"}],
body: """
{
"id": "#{url}",
"type": "Service",
"inbox": "#{url}/inbox",
"outbox": "#{url}/inbox"
}
"""
}}
%{url: url}
when url in [
"https://stinkyplace.example/.well-known/host-meta",
"https://shinyplace.example/.well-known/host-meta"
] ->
{:ok, %Tesla.Env{status: 404, url: url}}
end)
{:ok, "doppelgaenger@shinyplace.example",
%{"id" => "https://shinyplace.example/users/doppelgaenger"}} =
Finger.finger_mention("@doppelgaenger@stinkyplace.example")
end
test "accepts content in authority of query domain" do
# Early 2026 Mastodon style
Tesla.Mock.mock(fn
%{url: "https://example.com/.well-known/webfinger?resource=acct:user@example.com"} ->
%{url: url}
when url in [
"https://example.com/.well-known/webfinger?resource=acct:user@example.com",
"https://fedi.example.com/.well-known/webfinger?resource=acct:user@example.com",
"https://fedi.example.com/.well-known/webfinger?resource=https://fedi.example.com/users/user"
] ->
{:ok,
%Tesla.Env{
status: 200,
@ -114,11 +220,15 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|> String.replace("{{subdomain}}", "fedi.example.com")
}}
%{url: "https://fedi.example.com/.well-known/host-meta"} ->
%{url: url}
when url in [
"https://example.com/.well-known/host-meta",
"https://fedi.example.com/.well-known/host-meta"
] ->
{:ok,
%Tesla.Env{
status: 404,
url: "https://example.com/.well-known/host-meta"
url: url
}}
%{url: "https://fedi.example.com/users/user"} ->
@ -137,7 +247,7 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
{:ok, "user@example.com", _} = Finger.finger_mention("user@example.com")
end
test "errors when being served content from unrelated third-party domain" do
test "errors when being served content from unrelated third-party domain which is not registered" do
Tesla.Mock.mock(fn
%{url: "https://example.com/.well-known/webfinger?resource=acct:user@example.com"} ->
{:ok,
@ -152,15 +262,19 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|> String.replace("{{subdomain}}", "fedi.example.org")
}}
%{url: "https://example.com/.well-known/host-meta"} ->
%{url: url}
when url in [
"https://example.com/.well-known/host-meta",
"https://shinyplace.example/.well-known/webfinger?resource=acct:user@shinyplace.example"
] ->
{:ok,
%Tesla.Env{
status: 404,
url: "https://example.com/.well-known/host-meta"
url: url
}}
end)
{:error, :finger_domain_spoof} = Finger.finger_mention("user@example.com")
{:error, :not_found} = Finger.finger_mention("user@example.com")
end
test "should use the webfinger property to look up the webfinger data for an actor" do
@ -207,11 +321,13 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
{:ok, "user@example.com", _data} = Finger.finger_mention("@user@example.com")
end
test "allows HTTP redirects to serve as webfinger domain delegation" do
test "works with HTTP redirects on .well-known webfinger path" do
Tesla.Mock.mock(fn
%{
url: "https://example.com/.well-known/webfinger?resource=acct:user@example.com"
} ->
%{url: url}
when url in [
"https://example.com/.well-known/webfinger?resource=acct:user@example.com",
"https://somewhere-else.com/.well-known/webfinger?resource=acct:another-user@somewhere-else.com"
] ->
{:ok,
%Tesla.Env{
status: 200,
@ -252,11 +368,13 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
{:ok, "another-user@somewhere-else.com", _data} = Finger.finger_mention("@user@example.com")
end
test "should reject a cross-domain webfinger if the final actor has an incorrect webfinger property" do
test "should reject a cross-domain webfinger if the final actor has an incorrect webfinger property even if domain agrees to handle" do
Tesla.Mock.mock(fn
%{
url: "https://example.com/.well-known/webfinger?resource=acct:user@example.com"
} ->
%{url: url}
when url in [
"https://example.com/.well-known/webfinger?resource=acct:user@example.com",
"https://somewhere-else.com/.well-known/webfinger?resource=acct:another-user@somewhere-else.com"
] ->
{:ok,
%Tesla.Env{
status: 200,
@ -300,9 +418,11 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
test "should refetch the initial actor if no backlink exists on the final actor" do
Tesla.Mock.mock(fn
# first, the initial webfinger we fetch points to somewhere-else.com
%{
url: "https://example.com/.well-known/webfinger?resource=acct:user@example.com"
} ->
%{url: url}
when url in [
"https://example.com/.well-known/webfinger?resource=acct:user@example.com",
"https://somewhere-else.com/.well-known/webfinger?resource=acct:another-user@somewhere-else.com"
] ->
{:ok,
%Tesla.Env{
status: 200,
@ -545,7 +665,8 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
|> String.replace("{{nickname}}", "user")
|> String.replace("{{subdomain}}", "social.example.com"),
headers: [{"content-type", "application/jrd+json"}],
url: "https://oops-this-was-a-redirect/somewhere"
url:
"https://oops-this-was-a-redirect-to-a-trusted-path/.well-known/my-static-webfinger"
}}
%{url: "https://example.com/.well-known/host-meta"} ->
@ -566,6 +687,43 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
})
end
test "refuses domain mismatches early with webfinger backlink" do
Tesla.Mock.mock(fn
# we should finger the webfinger property
%{
url:
"https://example.com/.well-known/webfinger?resource=https://social.example.com/users/user"
} ->
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/webfinger/pleroma-webfinger.json")
|> String.replace("{{domain}}", "broken.example.com")
|> String.replace("{{nickname}}", "user")
|> String.replace("{{subdomain}}", "social.example.com"),
headers: [{"content-type", "application/jrd+json"}],
url: "https://broken.example.com/.well-known/webfinger"
}}
%{url: "https://example.com/.well-known/host-meta"} ->
{:ok,
%Tesla.Env{
status: 200,
url: "https://example.com/.well-known/host-meta",
body:
File.read!("test/fixtures/webfinger/masto-host-meta.xml")
|> String.replace("{{domain}}", "example.com")
}}
end)
assert {:error, :finger_domain_spoof} =
Finger.finger_actor(%{
"id" => "https://social.example.com/users/user",
"webfinger" => "user@example.com"
})
end
test "can discover nick from WebFinger query alone if actor contains no hints" do
Tesla.Mock.mock(fn
# we should finger the ID directly
@ -644,6 +802,101 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
"id" => "https://example.com/users/user"
})
end
require Logger
test "enusures final WebFinger response actually links back to inital actor (with FEP-2c59)" do
Tesla.Mock.mock(fn
%{
url:
"https://shinyplace.example/.well-known/webfinger?resource=https://example.com/users/user" =
url
} ->
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/webfinger/masto-webfinger.json")
|> String.replace("{{domain}}", "shinyplace.example")
|> String.replace("{{nickname}}", "user")
|> String.replace("{{subdomain}}", "example.com")
|> String.replace("{{apid}}", "https://example.com/users/not-this-user"),
headers: [{"content-type", "application/jrd+json"}],
url: url
}}
%{url: "https://shinyplace.example/.well-known/host-meta" = url} ->
{:ok,
%Tesla.Env{
status: 200,
url: url,
body:
File.read!("test/fixtures/webfinger/masto-host-meta.xml")
|> String.replace("{{domain}}", "shinyplace.example")
}}
end)
assert {:error, :fingered_data_mismatch} =
Finger.finger_actor(%{
"id" => "https://example.com/users/user",
"webfinger" => "user@shinyplace.example"
})
end
test "enusures final WebFinger response actually links back to inital acotr (without FEP-2c59)" do
Tesla.Mock.mock(fn
%{
url:
"https://example.com/.well-known/webfinger?resource=https://example.com/users/user" =
url
} ->
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/webfinger/masto-webfinger.json")
# Indicates different WebFinger domain is preferred
|> String.replace("{{domain}}", "shinyplace.example")
|> String.replace("{{nickname}}", "user")
|> String.replace("{{subdomain}}", "example.com")
# This still matches the initially fingered actor
|> String.replace("{{apid}}", "https://example.com/users/user"),
headers: [{"content-type", "application/jrd+json"}],
url: url
}}
%{
url:
"https://shinyplace.example/.well-known/webfinger?resource=acct:user@shinyplace.example" =
url
} ->
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/webfinger/masto-webfinger.json")
|> String.replace("{{domain}}", "shinyplace.example")
|> String.replace("{{nickname}}", "user")
|> String.replace("{{subdomain}}", "shinyplace.example")
# different AP id than initially fingered actor were trying to enrich here!
|> String.replace("{{apid}}", "https://shinyplace.com/users/user"),
headers: [{"content-type", "application/jrd+json"}],
url: url
}}
%{url: url}
when url in [
"https://example.com/.well-known/host-meta",
"https://shinyplace.example/.well-known/host-meta"
] ->
{:ok, %Tesla.Env{status: 404, url: url}}
end)
assert {:error, :fingered_data_mismatch} =
Finger.finger_actor(%{
"id" => "https://example.com/users/user"
})
end
end
describe "finger_raw_data/1" do
@ -803,10 +1056,27 @@ defmodule Pleroma.Web.WebFinger.FingerTest do
Tesla.Mock.json(fake_webfinger, url: url)
%{url: "https://bad.com/.well-known/host-meta"} ->
{:ok, %Tesla.Env{status: 404}}
# the AP id from fake WebFInger response
%{url: "https://bad.com/webfingertest" = url} ->
{:ok,
%Tesla.Env{
status: 200,
url: url,
headers: [{"content-type", "application/activity+json"}],
body: """
{
"id": "#{url}",
"type": "Service",
"inbox": "#{url}/inbox",
"outbox": "#{url}/inbox"
}
"""
}}
%{url: url} ->
{:ok, %Tesla.Env{status: 404, url: url}}
end)
assert {:error, :finger_domain_spoof} = Finger.finger_mention("meanie@bad.com")
assert {:error, :not_found} = Finger.finger_mention("meanie@bad.com")
end
end

View file

@ -1715,7 +1715,11 @@ defmodule HttpRequestMock do
end
Macros.mock_masto_webfinger(
"https://sub.mastodon.example/.well-known/webfinger?resource=acct:a@mastodon.example",
[
"https://sub.mastodon.example/.well-known/webfinger?resource=https://sub.mastodon.example/users/a",
"https://sub.mastodon.example/.well-known/webfinger?resource=acct:a@mastodon.example",
"https://mastodon.example/.well-known/webfinger?resource=acct:a@mastodon.example"
],
"a",
"mastodon.example",
"sub.mastodon.example"
@ -1774,12 +1778,12 @@ defmodule HttpRequestMock do
}}
end
def get(
"https://sub.pleroma.example/.well-known/webfinger?resource=acct:a@pleroma.example" = url,
_,
_,
_
) do
def get(url, _, _, _)
when url in [
"https://sub.pleroma.example/.well-known/webfinger?resource=https://sub.pleroma.example/users/a",
"https://sub.pleroma.example/.well-known/webfinger?resource=acct:a@pleroma.example",
"https://pleroma.example/.well-known/webfinger?resource=acct:a@pleroma.example"
] do
{:ok,
%Tesla.Env{
status: 200,

View file

@ -3,16 +3,20 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule HttpRequestMockMacros do
defmacro mock_masto_webfinger(url, nick, webfinger_domain, ap_domain \\ nil, ap_id \\ nil) do
quote do
def get(unquote(url) = url, _, _, _) do
webfinger_response_masto(
url,
unquote(nick),
unquote(webfinger_domain),
unquote(ap_domain),
unquote(ap_id)
)
defmacro mock_masto_webfinger(urls, nick, webfinger_domain, ap_domain \\ nil, ap_id \\ nil) do
urls = if is_binary(urls), do: [urls], else: urls
for url <- urls do
quote do
def get(unquote(url) = url, _, _, _) do
webfinger_response_masto(
url,
unquote(nick),
unquote(webfinger_domain),
unquote(ap_domain),
unquote(ap_id)
)
end
end
end
end