attachments_cleanup: 1,
new_users_digest: 1,
mute_expire: 5,
- search_indexing: 10
+ search_indexing: 10,
+ nodeinfo_fetcher: 1
],
plugins: [
Oban.Plugins.Pruner,
config :web_push_encryption, http_client: Pleroma.HTTP.WebPush
-config :pleroma, :instances_favicons, enabled: false
+config :pleroma, :instances_favicons, enabled: true
+config :pleroma, :instances_nodeinfo, enabled: true
config :floki, :html_parser, Floki.HTMLParser.FastHtml
}
]
},
+ %{
+ group: :pleroma,
+ key: :instances_nodeinfo,
+ type: :group,
+ description: "Control favicons for instances",
+ children: [
+ %{
+ key: :enabled,
+ type: :boolean,
+ description: "Allow/disallow getting instance nodeinfo"
+ }
+ ]
+ },
%{
group: :ex_aws,
key: :s3,
# Reduce recompilation time
# https://dashbit.co/blog/speeding-up-re-compilation-of-elixir-projects
config :phoenix, :plug_init_mode, :runtime
+config :pleroma, :instances_favicons, enabled: false
+config :pleroma, :instances_nodeinfo, enabled: false
if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs"
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
build_cachex("failed_proxy_url", limit: 2500),
build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
- build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500)
+ build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500),
+ build_cachex("instances", default_ttl: :timer.hours(24), limit: 2500)
]
end
defmodule Pleroma.Instances.Instance do
@moduledoc "Instance."
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
alias Pleroma.Instances
alias Pleroma.Instances.Instance
alias Pleroma.Repo
field(:host, :string)
field(:unreachable_since, :naive_datetime_usec)
field(:favicon, :string)
- field(:favicon_updated_at, :naive_datetime)
+ field(:metadata_updated_at, :naive_datetime)
+ field(:nodeinfo, :map, default: %{})
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
- |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at])
+ |> cast(params, [:host, :unreachable_since, :favicon, :nodeinfo, :metadata_updated_at])
|> validate_required([:host])
|> unique_constraint(:host)
end
defp parse_datetime(datetime), do: datetime
- def get_or_update_favicon(%URI{host: host} = instance_uri) do
- existing_record = Repo.get_by(Instance, %{host: host})
+ def needs_update(nil), do: true
+
+ def needs_update(%Instance{metadata_updated_at: nil}), do: true
+
+ def needs_update(%Instance{metadata_updated_at: metadata_updated_at}) do
now = NaiveDateTime.utc_now()
+ NaiveDateTime.diff(now, metadata_updated_at) > 86_400
+ end
- if existing_record && existing_record.favicon_updated_at &&
- NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do
- existing_record.favicon
+ def local do
+ %Instance{
+ host: Pleroma.Web.Endpoint.host(),
+ favicon: Pleroma.Web.Endpoint.url() <> "/favicon.png",
+ nodeinfo: Pleroma.Web.Nodeinfo.NodeinfoController.raw_nodeinfo()
+ }
+ end
+
+ def update_metadata(%URI{host: host} = uri) do
+ Logger.info("Checking metadata for #{host}")
+ existing_record = Repo.get_by(Instance, %{host: host})
+
+ if reachable?(host) do
+ do_update_metadata(uri, existing_record)
else
- favicon = scrape_favicon(instance_uri)
+ {:discard, :unreachable}
+ end
+ end
- if existing_record do
- existing_record
- |> changeset(%{favicon: favicon, favicon_updated_at: now})
- |> Repo.update()
+ defp do_update_metadata(%URI{host: host} = uri, existing_record) do
+ if existing_record do
+ if needs_update(existing_record) do
+ Logger.info("Updating metadata for #{host}")
+ favicon = scrape_favicon(uri)
+ nodeinfo = scrape_nodeinfo(uri)
+
+ {:ok, instance} =
+ existing_record
+ |> changeset(%{
+ host: host,
+ favicon: favicon,
+ nodeinfo: nodeinfo,
+ metadata_updated_at: NaiveDateTime.utc_now()
+ })
+ |> Repo.update()
+
+ @cachex.put(:instances_cache, "instances:#{host}", instance)
else
+ {:discard, "Does not require update"}
+ end
+ else
+ favicon = scrape_favicon(uri)
+ nodeinfo = scrape_nodeinfo(uri)
+
+ Logger.info("Creating metadata for #{host}")
+
+ {:ok, instance} =
%Instance{}
- |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now})
+ |> changeset(%{
+ host: host,
+ favicon: favicon,
+ nodeinfo: nodeinfo,
+ metadata_updated_at: NaiveDateTime.utc_now()
+ })
|> Repo.insert()
- end
- favicon
+ @cachex.put(:instances_cache, "instances:#{host}", instance)
end
- rescue
- e ->
- Logger.warn("Instance.get_or_update_favicon(\"#{host}\") error: #{inspect(e)}")
+ end
+
+ def get_favicon(%URI{host: host}) do
+ existing_record = Repo.get_by(Instance, %{host: host})
+
+ if existing_record do
+ existing_record.favicon
+ else
nil
+ end
end
- defp scrape_favicon(%URI{} = instance_uri) do
- try do
- with {_, true} <- {:reachable, reachable?(instance_uri.host)},
- {:ok, %Tesla.Env{body: html}} <-
- Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], []),
- {_, [favicon_rel | _]} when is_binary(favicon_rel) <-
- {:parse,
- html |> Floki.parse_document!() |> Floki.attribute("link[rel=icon]", "href")},
- {_, favicon} when is_binary(favicon) <-
- {:merge, URI.merge(instance_uri, favicon_rel) |> to_string()} do
- favicon
- else
- {:reachable, false} ->
- Logger.debug(
- "Instance.scrape_favicon(\"#{to_string(instance_uri)}\") ignored unreachable host"
- )
+ defp scrape_nodeinfo(%URI{} = instance_uri) do
+ with true <- Pleroma.Config.get([:instances_nodeinfo, :enabled]),
+ {_, true} <- {:reachable, reachable?(instance_uri.host)},
+ {:ok, %Tesla.Env{status: 200, body: body}} <-
+ Tesla.get(
+ "https://#{instance_uri.host}/.well-known/nodeinfo",
+ headers: [{"Accept", "application/json"}]
+ ),
+ {:ok, json} <- Jason.decode(body),
+ {:ok, %{"links" => links}} <- {:ok, json},
+ {:ok, %{"href" => href}} <-
+ {:ok,
+ Enum.find(links, &(&1["rel"] == "http://nodeinfo.diaspora.software/ns/schema/2.0"))},
+ {:ok, %Tesla.Env{body: data}} <-
+ Pleroma.HTTP.get(href, [{"accept", "application/json"}], []),
+ {:length, true} <- {:length, String.length(data) < 50_000},
+ {:ok, nodeinfo} <- Jason.decode(data) do
+ nodeinfo
+ else
+ {:reachable, false} ->
+ Logger.debug(
+ "Instance.scrape_nodeinfo(\"#{to_string(instance_uri)}\") ignored unreachable host"
+ )
- nil
+ nil
- _ ->
- nil
- end
- rescue
- e ->
- Logger.warn(
- "Instance.scrape_favicon(\"#{to_string(instance_uri)}\") error: #{inspect(e)}"
+ {:length, false} ->
+ Logger.debug(
+ "Instance.scrape_nodeinfo(\"#{to_string(instance_uri)}\") ignored too long body"
)
nil
+
+ _ ->
+ nil
+ end
+ end
+
+ defp scrape_favicon(%URI{} = instance_uri) do
+ with true <- Pleroma.Config.get([:instances_favicons, :enabled]),
+ {_, true} <- {:reachable, reachable?(instance_uri.host)},
+ {:ok, %Tesla.Env{body: html}} <-
+ Pleroma.HTTP.get(to_string(instance_uri), [{"accept", "text/html"}], []),
+ {_, [favicon_rel | _]} when is_binary(favicon_rel) <-
+ {:parse, html |> Floki.parse_document!() |> Floki.attribute("link[rel=icon]", "href")},
+ {_, favicon} when is_binary(favicon) <-
+ {:merge, URI.merge(instance_uri, favicon_rel) |> to_string()},
+ {:length, true} <- {:length, String.length(favicon) < 255} do
+ favicon
+ else
+ {:reachable, false} ->
+ Logger.debug(
+ "Instance.scrape_favicon(\"#{to_string(instance_uri)}\") ignored unreachable host"
+ )
+
+ nil
+
+ _ ->
+ nil
end
end
end)
|> Stream.run()
end
+
+ def get_by_url(url_or_host) do
+ url = host(url_or_host)
+ Repo.get_by(Instance, host: url)
+ end
+
+ def get_cached_by_url(url_or_host) do
+ url = host(url_or_host)
+
+ if url == Pleroma.Web.Endpoint.host() do
+ {:ok, local()}
+ else
+ @cachex.fetch!(:instances_cache, "instances:#{url}", fn _ ->
+ with %Instance{} = instance <- get_by_url(url) do
+ {:commit, {:ok, instance}}
+ else
+ _ -> {:ignore, nil}
+ end
+ end)
+ end
+ end
end
# - Increase the user note count
# - Increase the reply count
# - Increase replies count
+ # - Ask for scraping of nodeinfo
# - Set up ActivityExpiration
# - Set up notifications
# - Index incoming posts for search (if needed)
reply_depth = (meta[:depth] || 0) + 1
+ Pleroma.Workers.NodeInfoFetcherWorker.enqueue("process", %{
+ "source_url" => activity.data["actor"]
+ })
+
# FIXME: Force inReplyTo to replies
if Pleroma.Web.Federator.allowed_thread_distance?(reply_depth) and
object.data["replies"] != nil do
{:ok, activity, meta}
else
- e -> Repo.rollback(e)
+ e ->
+ Logger.error(inspect(e))
+ Repo.rollback(e)
end
end
render_many(targets, AccountView, "relationship.json", render_opts)
end
+ def render("instance.json", %{instance: %Pleroma.Instances.Instance{} = instance}) do
+ %{
+ name: instance.host,
+ favicon: instance.favicon |> MediaProxy.url(),
+ nodeinfo: instance.nodeinfo
+ }
+ end
+
+ def render("instance.json", _), do: nil
+
defp do_render("show.json", %{user: user} = opts) do
user = User.sanitize_html(user, User.html_filter_policy(opts[:for]))
display_name = user.name || user.nickname
%{}
end
- favicon =
- if Pleroma.Config.get([:instances_favicons, :enabled]) do
- user
- |> Map.get(:ap_id, "")
- |> URI.parse()
- |> URI.merge("/")
- |> Pleroma.Instances.Instance.get_or_update_favicon()
- |> MediaProxy.url()
+ instance =
+ with {:ok, instance} <- Pleroma.Instances.Instance.get_cached_by_url(user.ap_id) do
+ instance
else
+ _ ->
+ nil
+ end
+
+ favicon =
+ if is_nil(instance) do
nil
+ else
+ instance.favicon
+ |> MediaProxy.url()
end
%{
}
},
last_status_at: user.last_status_at,
-
+ akkoma: %{
+ instance: render("instance.json", %{instance: instance})
+ },
# Pleroma extensions
# Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub
fqn: User.full_nickname(user),
--- /dev/null
+defmodule Pleroma.Workers.NodeInfoFetcherWorker do
+ use Pleroma.Workers.WorkerHelper, queue: "nodeinfo_fetcher"
+
+ alias Oban.Job
+ alias Pleroma.Instances.Instance
+
+ @impl Oban.Worker
+ def perform(%Job{
+ args: %{"op" => "process", "source_url" => domain}
+ }) do
+ uri =
+ domain
+ |> URI.parse()
+ |> URI.merge("/")
+
+ Instance.update_metadata(uri)
+ end
+end
--- /dev/null
+defmodule Pleroma.Repo.Migrations.AddNodeinfo do
+ use Ecto.Migration
+
+ def up do
+ alter table(:instances) do
+ add_if_not_exists(:nodeinfo, :map, default: %{})
+ add_if_not_exists(:metadata_updated_at, :naive_datetime)
+ end
+ end
+
+ def down do
+ alter table(:instances) do
+ remove_if_exists(:nodeinfo, :map)
+ remove_if_exists(:metadata_updated_at, :naive_datetime)
+ end
+ end
+end
alias Pleroma.Tests.ObanHelpers
alias Pleroma.Web.CommonAPI
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: true
import ExUnit.CaptureLog
import Pleroma.Factory
- setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1)
+ setup_all do
+ clear_config([:instance, :federation_reachability_timeout_days], 1)
+ clear_config([:instances_nodeinfo, :enabled], true)
+ clear_config([:instances_favicons, :enabled], true)
+ end
describe "set_reachable/1" do
test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do
end
end
- describe "get_or_update_favicon/1" do
- test "Scrapes favicon URLs" do
- Tesla.Mock.mock(fn %{url: "https://favicon.example.org/"} ->
- %Tesla.Env{
- status: 200,
- body: ~s[<html><head><link rel="icon" href="/favicon.png"></head></html>]
- }
+ describe "update_metadata/1" do
+ test "Scrapes favicon URLs and nodeinfo" do
+ Tesla.Mock.mock(fn
+ %{url: "https://favicon.example.org/"} ->
+ %Tesla.Env{
+ status: 200,
+ body: ~s[<html><head><link rel="icon" href="/favicon.png"></head></html>]
+ }
+
+ %{url: "https://favicon.example.org/.well-known/nodeinfo"} ->
+ %Tesla.Env{
+ status: 200,
+ body:
+ Jason.encode!(%{
+ links: [
+ %{
+ rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
+ href: "https://favicon.example.org/nodeinfo/2.0"
+ }
+ ]
+ })
+ }
+
+ %{url: "https://favicon.example.org/nodeinfo/2.0"} ->
+ %Tesla.Env{
+ status: 200,
+ body: Jason.encode!(%{version: "2.0", software: %{name: "Akkoma"}})
+ }
end)
- assert "https://favicon.example.org/favicon.png" ==
- Instance.get_or_update_favicon(URI.parse("https://favicon.example.org/"))
+ assert {:ok, true} ==
+ Instance.update_metadata(URI.parse("https://favicon.example.org/"))
+
+ {:ok, instance} = Instance.get_cached_by_url("https://favicon.example.org/")
+ assert instance.favicon == "https://favicon.example.org/favicon.png"
+ assert instance.nodeinfo == %{"version" => "2.0", "software" => %{"name" => "Akkoma"}}
end
- test "Returns nil on too long favicon URLs" do
+ test "Does not retain favicons that are too long" do
long_favicon_url =
"https://Lorem.ipsum.dolor.sit.amet/consecteturadipiscingelit/Praesentpharetrapurusutaliquamtempus/Mauriseulaoreetarcu/atfacilisisorci/Nullamporttitor/nequesedfeugiatmollis/dolormagnaefficiturlorem/nonpretiumsapienorcieurisus/Nullamveleratsem/Maecenassedaccumsanexnam/favicon.png"
- Tesla.Mock.mock(fn %{url: "https://long-favicon.example.org/"} ->
- %Tesla.Env{
- status: 200,
- body:
- ~s[<html><head><link rel="icon" href="] <> long_favicon_url <> ~s["></head></html>]
- }
+ Tesla.Mock.mock(fn
+ %{url: "https://long-favicon.example.org/"} ->
+ %Tesla.Env{
+ status: 200,
+ body:
+ ~s[<html><head><link rel="icon" href="] <> long_favicon_url <> ~s["></head></html>]
+ }
+
+ %{url: "https://long-favicon.example.org/.well-known/nodeinfo"} ->
+ %Tesla.Env{
+ status: 200,
+ body:
+ Jason.encode!(%{
+ links: [
+ %{
+ rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
+ href: "https://long-favicon.example.org/nodeinfo/2.0"
+ }
+ ]
+ })
+ }
+
+ %{url: "https://long-favicon.example.org/nodeinfo/2.0"} ->
+ %Tesla.Env{
+ status: 200,
+ body: Jason.encode!(%{version: "2.0", software: %{name: "Akkoma"}})
+ }
end)
- assert capture_log(fn ->
- assert nil ==
- Instance.get_or_update_favicon(
- URI.parse("https://long-favicon.example.org/")
- )
- end) =~
- "Instance.get_or_update_favicon(\"long-favicon.example.org\") error: %Postgrex.Error{"
+ assert {:ok, true} ==
+ Instance.update_metadata(URI.parse("https://long-favicon.example.org/"))
+
+ {:ok, instance} = Instance.get_cached_by_url("https://long-favicon.example.org/")
+ assert instance.favicon == nil
end
test "Handles not getting a favicon URL properly" do
- Tesla.Mock.mock(fn %{url: "https://no-favicon.example.org/"} ->
- %Tesla.Env{
- status: 200,
- body: ~s[<html><head><h1>I wil look down and whisper "GNO.."</h1></head></html>]
- }
+ Tesla.Mock.mock(fn
+ %{url: "https://no-favicon.example.org/"} ->
+ %Tesla.Env{
+ status: 200,
+ body: ~s[<html><head><h1>I wil look down and whisper "GNO.."</h1></head></html>]
+ }
+
+ %{url: "https://no-favicon.example.org/.well-known/nodeinfo"} ->
+ %Tesla.Env{
+ status: 200,
+ body:
+ Jason.encode!(%{
+ links: [
+ %{
+ rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
+ href: "https://no-favicon.example.org/nodeinfo/2.0"
+ }
+ ]
+ })
+ }
+
+ %{url: "https://no-favicon.example.org/nodeinfo/2.0"} ->
+ %Tesla.Env{
+ status: 200,
+ body: Jason.encode!(%{version: "2.0", software: %{name: "Akkoma"}})
+ }
end)
refute capture_log(fn ->
- assert nil ==
- Instance.get_or_update_favicon(
- URI.parse("https://no-favicon.example.org/")
- )
- end) =~ "Instance.scrape_favicon(\"https://no-favicon.example.org/\") error: "
+ assert {:ok, true} =
+ Instance.update_metadata(URI.parse("https://no-favicon.example.org/"))
+ end) =~ "Instance.update_metadata(\"https://no-favicon.example.org/\") error: "
end
- test "Doesn't scrapes unreachable instances" do
+ test "Doesn't scrape unreachable instances" do
instance = insert(:instance, unreachable_since: Instances.reachability_datetime_threshold())
url = "https://" <> instance.host
- assert capture_log(fn -> assert nil == Instance.get_or_update_favicon(URI.parse(url)) end) =~
- "Instance.scrape_favicon(\"#{url}\") ignored unreachable host"
+ assert {:discard, :unreachable} == Instance.update_metadata(URI.parse(url))
+ end
+
+ test "doesn't continue scraping nodeinfo if we can't find a link" do
+ Tesla.Mock.mock(fn
+ %{url: "https://bad-nodeinfo.example.org/"} ->
+ %Tesla.Env{
+ status: 200,
+ body: ~s[<html><head><h1>I wil look down and whisper "GNO.."</h1></head></html>]
+ }
+
+ %{url: "https://bad-nodeinfo.example.org/.well-known/nodeinfo"} ->
+ %Tesla.Env{
+ status: 200,
+ body: "oepsie woepsie de nodeinfo is kapotie uwu"
+ }
+ end)
+
+ assert {:ok, true} ==
+ Instance.update_metadata(URI.parse("https://bad-nodeinfo.example.org/"))
+
+ {:ok, instance} = Instance.get_cached_by_url("https://bad-nodeinfo.example.org/")
+ assert instance.nodeinfo == nil
+ end
+
+ test "doesn't store bad json in the nodeinfo" do
+ Tesla.Mock.mock(fn
+ %{url: "https://bad-nodeinfo.example.org/"} ->
+ %Tesla.Env{
+ status: 200,
+ body: ~s[<html><head><h1>I wil look down and whisper "GNO.."</h1></head></html>]
+ }
+
+ %{url: "https://bad-nodeinfo.example.org/.well-known/nodeinfo"} ->
+ %Tesla.Env{
+ status: 200,
+ body:
+ Jason.encode!(%{
+ links: [
+ %{
+ rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
+ href: "https://bad-nodeinfo.example.org/nodeinfo/2.0"
+ }
+ ]
+ })
+ }
+
+ %{url: "https://bad-nodeinfo.example.org/nodeinfo/2.0"} ->
+ %Tesla.Env{
+ status: 200,
+ body: "oepsie woepsie de json might be bad uwu"
+ }
+ end)
+
+ assert {:ok, true} ==
+ Instance.update_metadata(URI.parse("https://bad-nodeinfo.example.org/"))
+
+ {:ok, instance} = Instance.get_cached_by_url("https://bad-nodeinfo.example.org/")
+ assert instance.nodeinfo == nil
+ end
+
+ test "doesn't store incredibly long json nodeinfo" do
+ too_long = String.duplicate("a", 50_000)
+
+ Tesla.Mock.mock(fn
+ %{url: "https://bad-nodeinfo.example.org/"} ->
+ %Tesla.Env{
+ status: 200,
+ body: ~s[<html><head><h1>I wil look down and whisper "GNO.."</h1></head></html>]
+ }
+
+ %{url: "https://bad-nodeinfo.example.org/.well-known/nodeinfo"} ->
+ %Tesla.Env{
+ status: 200,
+ body:
+ Jason.encode!(%{
+ links: [
+ %{
+ rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
+ href: "https://bad-nodeinfo.example.org/nodeinfo/2.0"
+ }
+ ]
+ })
+ }
+
+ %{url: "https://bad-nodeinfo.example.org/nodeinfo/2.0"} ->
+ %Tesla.Env{
+ status: 200,
+ body: Jason.encode!(%{version: "2.0", software: %{name: too_long}})
+ }
+ end)
+
+ assert {:ok, true} ==
+ Instance.update_metadata(URI.parse("https://bad-nodeinfo.example.org/"))
+
+ {:ok, instance} = Instance.get_cached_by_url("https://bad-nodeinfo.example.org/")
+ assert instance.nodeinfo == nil
end
end
import Mock
import Pleroma.Factory
+ describe "handle" do
+ test "it queues a fetch of instance information" do
+ author = insert(:user, local: false, ap_id: "https://wowee.example.com/users/1")
+ recipient = insert(:user, local: true)
+
+ {:ok, note_data, _meta} =
+ Builder.note(%Pleroma.Web.CommonAPI.ActivityDraft{
+ user: author,
+ to: [recipient.ap_id],
+ mentions: [recipient],
+ content_html: "hey",
+ extra: %{"id" => "https://wowee.example.com/notes/1"}
+ })
+
+ {:ok, create_activity_data, _meta} =
+ Builder.create(author, note_data["id"], [recipient.ap_id])
+
+ {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+ {:ok, _create_activity, _meta} =
+ SideEffects.handle(create_activity, local: false, object_data: note_data)
+
+ assert_enqueued(
+ worker: Pleroma.Workers.NodeInfoFetcherWorker,
+ args: %{"op" => "process", "source_url" => "https://wowee.example.com/users/1"}
+ )
+ end
+ end
+
describe "handle_after_transaction" do
test "it streams out notifications and streams" do
author = insert(:user, local: true)
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: false
alias Pleroma.User
alias Pleroma.UserRelationship
import Pleroma.Factory
import Tesla.Mock
+ import Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
user =
insert(:user, %{
+ ap_id: "https://example.com/users/chikichikibanban",
follower_count: 3,
note_count: 5,
background: background_image,
also_known_as: ["https://shitposter.zone/users/shp"]
})
+ insert(:instance, %{host: "example.com", nodeinfo: %{version: "2.1"}})
+
expected = %{
id: to_string(user.id),
username: "shp",
statuses_count: 5,
note: "<span>valid html</span>. a<br/>b<br/>c<br/>d<br/>f '&<>"",
url: user.ap_id,
+ akkoma: %{
+ instance: %{
+ name: "example.com",
+ nodeinfo: %{
+ "version" => "2.1"
+ },
+ favicon: nil
+ }
+ },
avatar: "http://localhost:4001/images/avi.png",
avatar_static: "http://localhost:4001/images/avi.png",
header: "http://localhost:4001/images/banner.png",
assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true})
end
+ describe "nodeinfo" do
+ setup do
+ [
+ user: insert(:user, ap_id: "https://somewhere.example.com/users/chikichikibanban"),
+ instance:
+ insert(:instance, %{
+ host: "somewhere.example.com",
+ favicon: "https://example.com/favicon.ico"
+ })
+ ]
+ end
+
+ test "is embedded in the account view", %{user: user} do
+ assert %{
+ akkoma: %{
+ instance: %{
+ name: "somewhere.example.com",
+ nodeinfo: %{
+ "version" => "2.0"
+ },
+ favicon: "https://example.com/favicon.ico"
+ }
+ }
+ } = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
+ end
+
+ test "uses local nodeinfo for local users" do
+ user = insert(:user)
+
+ assert %{
+ akkoma: %{
+ instance: %{
+ name: "localhost",
+ nodeinfo: %{
+ software: %{
+ name: "akkoma"
+ }
+ }
+ }
+ }
+ } = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
+ end
+ end
+
describe "favicon" do
setup do
- [user: insert(:user)]
+ [
+ user: insert(:user, ap_id: "https://example.com/users/chikichikibanban"),
+ instance:
+ insert(:instance, %{host: "example.com", favicon: "https://example.com/favicon.ico"})
+ ]
end
test "is parsed when :instance_favicons is enabled", %{user: user} do
assert %{
pleroma: %{
- favicon:
- "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png"
+ favicon: "https://example.com/favicon.ico"
}
} = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
end
- test "is nil when :instances_favicons is disabled", %{user: user} do
+ test "is nil when we have no instance", %{user: user} do
+ user = %{user | ap_id: "https://wowee.example.com/users/2"}
+
assert %{pleroma: %{favicon: nil}} =
AccountView.render("show.json", %{user: user, skip_visibility_check: true})
end
},
fqn: "shp@shitposter.club",
last_status_at: nil,
+ akkoma: %{
+ instance: %{
+ name: "localhost",
+ favicon: "http://localhost:4001/favicon.png",
+ nodeinfo: %{version: "2.0"}
+ }
+ },
pleroma: %{
ap_id: user.ap_id,
also_known_as: [],
background_image: nil,
- favicon: nil,
+ favicon: "http://localhost:4001/favicon.png",
is_confirmed: true,
tags: [],
is_admin: false,
}
}
- assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true})
+ with_mock(
+ Pleroma.Web.Nodeinfo.NodeinfoController,
+ raw_nodeinfo: fn -> %{version: "2.0"} end
+ ) do
+ assert expected ==
+ AccountView.render("show.json", %{user: user, skip_visibility_check: true})
+ end
end
test "Represent a Funkwhale channel" do
emoji: %{"joker_smile" => "https://evil.website/society.png"}
)
+ insert(:instance, %{host: "localhost", favicon: "https://evil.website/favicon.png"})
+
with media_preview_enabled <- [false, true] do
clear_config([:media_preview_proxy, :enabled], media_preview_enabled)
{key, url} when key in [:avatar, :avatar_static, :header, :header_static] ->
String.starts_with?(url, Pleroma.Web.Endpoint.url())
+ {:akkoma, %{instance: %{favicon: favicon_url}}} ->
+ String.starts_with?(favicon_url, Pleroma.Web.Endpoint.url())
+
{:emojis, emojis} ->
Enum.all?(emojis, fn %{url: url, static_url: static_url} ->
String.starts_with?(url, Pleroma.Web.Endpoint.url()) &&
|> assert()
end
end
+
+ test "returns nil in the instance field when no instance is held locally" do
+ user = insert(:user, ap_id: "https://example.com/users/1")
+ view = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
+ assert view[:akkoma][:instance] == nil
+ end
end
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StreamerTest do
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: false
import Pleroma.Factory
}
end
+ def instance_factory(attrs \\ %{}) do
+ %Pleroma.Instances.Instance{
+ host: attrs[:domain] || "example.com",
+ nodeinfo: %{version: "2.0", openRegistrations: true},
+ unreachable_since: nil
+ }
+ |> Map.merge(attrs)
+ end
+
def user_factory(attrs \\ %{}) do
pem = Enum.random(@rsa_keys)
}
end
- def instance_factory do
- %Pleroma.Instances.Instance{
- host: "domain.com",
- unreachable_since: nil
- }
- end
-
def oauth_token_factory(attrs \\ %{}) do
scopes = Map.get(attrs, :scopes, ["read"])
oauth_app = Map.get_lazy(attrs, :app, fn -> insert(:oauth_app, scopes: scopes) end)