instances: Store some metadata based on NodeInfo

This commit is contained in:
Haelwenn (lanodan) Monnier 2023-03-06 21:16:34 +01:00
parent 714bf0cb23
commit 9a2523a09a
7 changed files with 177 additions and 1 deletions

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Instances.Instance do
alias Pleroma.Instances alias Pleroma.Instances
alias Pleroma.Instances.Instance alias Pleroma.Instances.Instance
alias Pleroma.Maps
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Workers.BackgroundWorker alias Pleroma.Workers.BackgroundWorker
@ -24,6 +25,14 @@ defmodule Pleroma.Instances.Instance do
field(:favicon, :string) field(:favicon, :string)
field(:favicon_updated_at, :naive_datetime) field(:favicon_updated_at, :naive_datetime)
embeds_one :metadata, Pleroma.Instances.Metadata, primary_key: false do
field(:software_name, :string)
field(:software_version, :string)
field(:software_repository, :string)
end
field(:metadata_updated_at, :utc_datetime)
timestamps() timestamps()
end end
@ -31,11 +40,17 @@ defmodule Pleroma.Instances.Instance do
def changeset(struct, params \\ %{}) do def changeset(struct, params \\ %{}) do
struct struct
|> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at]) |> cast(params, __schema__(:fields) -- [:metadata])
|> cast_embed(:metadata, with: &metadata_changeset/2)
|> validate_required([:host]) |> validate_required([:host])
|> unique_constraint(:host) |> unique_constraint(:host)
end end
def metadata_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:software_name, :software_version, :software_repository])
end
def filter_reachable([]), do: %{} def filter_reachable([]), do: %{}
def filter_reachable(urls_or_hosts) when is_list(urls_or_hosts) do def filter_reachable(urls_or_hosts) when is_list(urls_or_hosts) do
@ -198,6 +213,89 @@ defmodule Pleroma.Instances.Instance do
end end
end end
def get_or_update_metadata(%URI{host: host} = instance_uri) do
existing_record = Repo.get_by(Instance, %{host: host})
now = NaiveDateTime.utc_now()
if existing_record && existing_record.metadata_updated_at &&
NaiveDateTime.diff(now, existing_record.metadata_updated_at) < 86_400 do
existing_record.metadata
else
metadata = scrape_metadata(instance_uri)
if existing_record do
existing_record
|> changeset(%{metadata: metadata, metadata_updated_at: now})
|> Repo.update()
else
%Instance{}
|> changeset(%{host: host, metadata: metadata, metadata_updated_at: now})
|> Repo.insert()
end
metadata
end
end
defp get_nodeinfo_uri(well_known) do
links = Map.get(well_known, "links", [])
nodeinfo21 =
Enum.find(links, &(&1["rel"] == "http://nodeinfo.diaspora.software/ns/schema/2.1"))["href"]
nodeinfo20 =
Enum.find(links, &(&1["rel"] == "http://nodeinfo.diaspora.software/ns/schema/2.0"))["href"]
cond do
is_binary(nodeinfo21) -> {:ok, nodeinfo21}
is_binary(nodeinfo20) -> {:ok, nodeinfo20}
true -> {:error, :no_links}
end
end
defp scrape_metadata(%URI{} = instance_uri) do
try do
with {_, true} <- {:reachable, reachable?(instance_uri.host)},
{:ok, %Tesla.Env{body: well_known_body}} <-
instance_uri
|> URI.merge("/.well-known/nodeinfo")
|> to_string()
|> Pleroma.HTTP.get([{"accept", "application/json"}]),
{:ok, well_known_json} <- Jason.decode(well_known_body),
{:ok, nodeinfo_uri} <- get_nodeinfo_uri(well_known_json),
{:ok, %Tesla.Env{body: nodeinfo_body}} <-
Pleroma.HTTP.get(nodeinfo_uri, [{"accept", "application/json"}]),
{:ok, nodeinfo} <- Jason.decode(nodeinfo_body) do
# Can extract more metadata from NodeInfo but need to be careful about it's size,
# can't just dump the entire thing
software = Map.get(nodeinfo, "software", %{})
%{
software_name: software["name"],
software_version: software["version"]
}
|> Maps.put_if_present(:software_repository, software["repository"])
else
{:reachable, false} ->
Logger.debug(
"Instance.scrape_metadata(\"#{to_string(instance_uri)}\") ignored unreachable host"
)
nil
_ ->
nil
end
rescue
e ->
Logger.warn(
"Instance.scrape_metadata(\"#{to_string(instance_uri)}\") error: #{inspect(e)}"
)
nil
end
end
@doc """ @doc """
Deletes all users from an instance in a background task, thus also deleting Deletes all users from an instance in a background task, thus also deleting
all of those users' activities and notifications. all of those users' activities and notifications.

View file

@ -0,0 +1,14 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo.Migrations.InstancesAddMetadata do
use Ecto.Migration
def change do
alter table(:instances) do
add(:metadata, :map)
add(:metadata_updated_at, :utc_datetime)
end
end
end

View file

@ -0,0 +1 @@
{"version":"2.0","software":{"name":"mastodon","version":"4.1.0"},"protocols":["activitypub"],"services":{"outbound":[],"inbound":[]},"usage":{"users":{"total":971090,"activeMonth":167218,"activeHalfyear":384808},"localPosts":52071541},"openRegistrations":true,"metadata":{}}

View file

@ -0,0 +1 @@
{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"https://mastodon.example.org/nodeinfo/2.0"}]}

View file

@ -0,0 +1 @@
{"version":"2.1","software":{"name":"wildebeest","version":"0.0.1","repository":"https://github.com/cloudflare/wildebeest"},"protocols":["activitypub"],"usage":{"users":{"total":1,"activeMonth":1,"activeHalfyear":1}},"openRegistrations":false,"metadata":{"upstream":{"name":"mastodon","version":"3.5.1"}}}

View file

@ -0,0 +1 @@
{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"https://wildebeest.example.org/nodeinfo/2.0"},{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.1","href":"https://wildebeest.example.org/nodeinfo/2.1"}]}

View file

@ -161,6 +161,66 @@ defmodule Pleroma.Instances.InstanceTest do
end end
end end
describe "get_or_update_metadata/1" do
test "Scrapes Wildebeest NodeInfo" do
Tesla.Mock.mock(fn
%{url: "https://wildebeest.example.org/.well-known/nodeinfo"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/wildebeest-well-known-nodeinfo.json")
}
%{url: "https://wildebeest.example.org/nodeinfo/2.1"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/wildebeest-nodeinfo21.json")
}
end)
expected = %{
software_name: "wildebeest",
software_repository: "https://github.com/cloudflare/wildebeest",
software_version: "0.0.1"
}
assert expected ==
Instance.get_or_update_metadata(URI.parse("https://wildebeest.example.org/"))
expected = %Pleroma.Instances.Instance.Pleroma.Instances.Metadata{
software_name: "wildebeest",
software_repository: "https://github.com/cloudflare/wildebeest",
software_version: "0.0.1"
}
assert expected ==
Repo.get_by(Pleroma.Instances.Instance, %{host: "wildebeest.example.org"}).metadata
end
test "Scrapes Mastodon NodeInfo" do
Tesla.Mock.mock(fn
%{url: "https://mastodon.example.org/.well-known/nodeinfo"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/mastodon-well-known-nodeinfo.json")
}
%{url: "https://mastodon.example.org/nodeinfo/2.0"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/mastodon-nodeinfo20.json")
}
end)
expected = %{
software_name: "mastodon",
software_version: "4.1.0"
}
assert expected ==
Instance.get_or_update_metadata(URI.parse("https://mastodon.example.org/"))
end
end
test "delete_users_and_activities/1 deletes remote instance users and activities" do test "delete_users_and_activities/1 deletes remote instance users and activities" do
[mario, luigi, _peach, wario] = [mario, luigi, _peach, wario] =
users = [ users = [