diff --git a/lib/federation/activity_pub/types/actors.ex b/lib/federation/activity_pub/types/actors.ex index f62ca43f6..c3f00f95d 100644 --- a/lib/federation/activity_pub/types/actors.ex +++ b/lib/federation/activity_pub/types/actors.ex @@ -301,7 +301,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do ) do %Actor{id: relay_id} = Relay.get_actor() - unless follower.target_actor.manually_approves_followers or + unless follower.target_actor.manually_approves_followers == true or follower.target_actor.id == relay_id do require Logger Logger.debug("Target doesn't manually approves followers, we can accept right away") diff --git a/lib/federation/node_info.ex b/lib/federation/node_info.ex index b37bd3e96..8b227aa00 100644 --- a/lib/federation/node_info.ex +++ b/lib/federation/node_info.ex @@ -5,6 +5,7 @@ defmodule Mobilizon.Federation.NodeInfo do alias Mobilizon.Service.HTTP.WebfingerClient require Logger + import Mobilizon.Service.HTTP.Utils, only: [is_content_type?: 2] @application_uri "https://www.w3.org/ns/activitystreams#Application" @nodeinfo_rel_2_0 "http://nodeinfo.diaspora.software/ns/schema/2.0" @@ -20,7 +21,7 @@ defmodule Mobilizon.Federation.NodeInfo do {:ok, body} -> extract_application_actor(body) - {:error, :node_info_meta_http_error} -> + {:error, _err} -> nil end end @@ -31,7 +32,9 @@ defmodule Mobilizon.Federation.NodeInfo do with {:ok, endpoint} when is_binary(endpoint) <- fetch_nodeinfo_details(host), :ok <- Logger.debug("Going to get NodeInfo information from URL #{endpoint}"), - {:ok, %{body: body, status: code}} when code in 200..299 <- WebfingerClient.get(endpoint) do + {:ok, %{body: body, status: code, headers: headers}} when code in 200..299 <- + WebfingerClient.get(endpoint), + {:ok, body} <- validate_json_response(body, headers) do Logger.debug("Found nodeinfo information for domain #{host}") {:ok, body} else @@ -58,8 +61,8 @@ defmodule Mobilizon.Federation.NodeInfo do prefix = if @env !== :dev, do: "https", else: "http" case WebfingerClient.get("#{prefix}://#{host}/.well-known/nodeinfo") do - {:ok, %{body: body, status: code}} when code in 200..299 -> - {:ok, body} + {:ok, %{body: body, status: code, headers: headers}} when code in 200..299 -> + validate_json_response(body, headers) err -> Logger.debug("Failed to fetch NodeInfo data #{inspect(err)}") @@ -102,4 +105,19 @@ defmodule Mobilizon.Federation.NodeInfo do rel == relation and is_binary(href) end) end + + @spec validate_json_response(map() | String.t(), list()) :: + {:ok, String.t()} | {:error, :bad_content_type | :body_not_json} + defp validate_json_response(body, headers) do + cond do + !is_content_type?(headers, "application/json") -> + {:error, :bad_content_type} + + !is_map(body) -> + {:error, :body_not_json} + + true -> + {:ok, body} + end + end end diff --git a/lib/graphql/resolvers/admin.ex b/lib/graphql/resolvers/admin.ex index d052d8b09..fd0f81f5a 100644 --- a/lib/graphql/resolvers/admin.ex +++ b/lib/graphql/resolvers/admin.ex @@ -16,6 +16,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do alias Mobilizon.Reports.{Note, Report} alias Mobilizon.Service.Auth.Authenticator alias Mobilizon.Service.Statistics + alias Mobilizon.Service.Workers.RefreshInstances alias Mobilizon.Storage.Page alias Mobilizon.Users.User alias Mobilizon.Web.Email @@ -546,6 +547,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do case Relay.follow(domain) do {:ok, _activity, _follow} -> Instances.refresh() + RefreshInstances.refresh_instance_actor(domain) get_instance(parent, args, resolution) {:error, :follow_pending} -> diff --git a/lib/mobilizon/instances/instances.ex b/lib/mobilizon/instances/instances.ex index 9fa759907..126d2d0b7 100644 --- a/lib/mobilizon/instances/instances.ex +++ b/lib/mobilizon/instances/instances.ex @@ -22,15 +22,16 @@ defmodule Mobilizon.Instances do order_by_options = Keyword.new([{direction, order_by}]) - subquery = - Actor - |> where( - [a], - a.preferred_username == "relay" and a.type == :Application and not is_nil(a.domain) - ) - |> join(:left, [a], f1 in Follower, on: f1.target_actor_id == a.id) - |> join(:left, [a], f2 in Follower, on: f2.actor_id == a.id) - |> select([a, f1, f2], %{ + query = + Instance + |> join(:left, [i], ia in InstanceActor, on: i.domain == ia.domain) + |> join(:left, [_i, ia], a in Actor, on: ia.actor_id == a.id) + |> join(:left, [_i, _ia, a], f1 in Follower, on: f1.target_actor_id == a.id) + |> join(:left, [_i, _ia, a], f2 in Follower, on: f2.actor_id == a.id) + |> select([i, ia, a, f1, f2], %{ + instance: i, + instance_actor: ia, + actor: a, domain: a.domain, has_relay: fragment(@is_null_fragment, a.id), following: fragment(@is_null_fragment, f2.id), @@ -38,13 +39,6 @@ defmodule Mobilizon.Instances do follower: fragment(@is_null_fragment, f1.id), follower_approved: f1.approved }) - - query = - Instance - |> join(:left, [i], s in subquery(subquery), on: i.domain == s.domain) - |> join(:left, [i], ia in InstanceActor, on: i.domain == ia.domain) - |> join(:left, [_i, _s, ia], a in Actor, on: ia.actor_id == a.id) - |> select([i, s, ia, a], {i, s, ia, a}) |> order_by(^order_by_options) query = @@ -93,17 +87,17 @@ defmodule Mobilizon.Instances do SQL.query!(Repo, "REFRESH MATERIALIZED VIEW instances") end - defp convert_instance_meta( - {instance, - %{ - domain: _domain, - follower: follower, - follower_approved: follower_approved, - following: following, - following_approved: following_approved, - has_relay: has_relay - }, instance_meta, instance_actor} - ) do + defp convert_instance_meta(%{ + instance: instance, + instance_actor: instance_meta, + actor: instance_actor, + domain: _domain, + follower: follower, + follower_approved: follower_approved, + following: following, + following_approved: following_approved, + has_relay: has_relay + }) do instance |> Map.put(:follower_status, follow_status(following, following_approved)) |> Map.put(:followed_status, follow_status(follower, follower_approved)) diff --git a/lib/service/http/utils.ex b/lib/service/http/utils.ex new file mode 100644 index 000000000..8adbc5c24 --- /dev/null +++ b/lib/service/http/utils.ex @@ -0,0 +1,35 @@ +defmodule Mobilizon.Service.HTTP.Utils do + @moduledoc """ + Utils for HTTP operations + """ + + @spec get_header(Enum.t(), String.t()) :: String.t() | nil + def get_header(headers, key) do + key = String.downcase(key) + + case List.keyfind(headers, key, 0) do + {^key, value} -> String.downcase(value) + nil -> nil + end + end + + @spec is_content_type?(Enum.t(), String.t() | list(String.t())) :: boolean + def is_content_type?(headers, content_type) do + headers + |> get_header("Content-Type") + |> content_type_header_matches(content_type) + end + + @spec content_type_header_matches(String.t() | nil, Enum.t()) :: boolean + defp content_type_header_matches(header, content_types) + defp content_type_header_matches(nil, _content_types), do: false + + defp content_type_header_matches(header, content_type) + when is_binary(header) and is_binary(content_type), + do: content_type_header_matches(header, [content_type]) + + defp content_type_header_matches(header, content_types) + when is_binary(header) and is_list(content_types) do + Enum.any?(content_types, fn content_type -> String.starts_with?(header, content_type) end) + end +end diff --git a/lib/service/rich_media/parser.ex b/lib/service/rich_media/parser.ex index 2821f823d..6677e2718 100644 --- a/lib/service/rich_media/parser.ex +++ b/lib/service/rich_media/parser.ex @@ -22,6 +22,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do alias Mobilizon.Service.RichMedia.Parsers.Fallback alias Plug.Conn.Utils require Logger + import Mobilizon.Service.HTTP.Utils defp parsers do Mobilizon.Config.get([:rich_media, :parsers]) @@ -74,7 +75,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do opts: @options )}, {:is_html, _response_headers, true} <- - {:is_html, response_headers, is_html(response_headers)} do + {:is_html, response_headers, is_html?(response_headers)} do body |> convert_utf8(response_headers) |> maybe_parse() @@ -107,43 +108,21 @@ defmodule Mobilizon.Service.RichMedia.Parser do defp get_data_for_media(response_headers, url) do data = %{title: get_filename_from_headers(response_headers) || get_filename_from_url(url)} - if is_image(response_headers) do + if is_image?(response_headers) do Map.put(data, :image_remote_url, url) else data end end - @spec is_html(Enum.t()) :: boolean - def is_html(headers) do - headers - |> get_header("Content-Type") - |> content_type_header_matches(["text/html", "application/xhtml"]) + @spec is_html?(Enum.t()) :: boolean + defp is_html?(headers) do + is_content_type?(headers, ["text/html", "application/xhtml"]) end - @spec is_image(Enum.t()) :: boolean - defp is_image(headers) do - headers - |> get_header("Content-Type") - |> content_type_header_matches(["image/"]) - end - - @spec content_type_header_matches(String.t() | nil, Enum.t()) :: boolean - defp content_type_header_matches(header, content_types) - defp content_type_header_matches(nil, _content_types), do: false - - defp content_type_header_matches(header, content_types) when is_binary(header) do - Enum.any?(content_types, fn content_type -> String.starts_with?(header, content_type) end) - end - - @spec get_header(Enum.t(), String.t()) :: String.t() | nil - defp get_header(headers, key) do - key = String.downcase(key) - - case List.keyfind(headers, key, 0) do - {^key, value} -> String.downcase(value) - nil -> nil - end + @spec is_image?(Enum.t()) :: boolean + defp is_image?(headers) do + is_content_type?(headers, ["image/"]) end @spec get_filename_from_headers(Enum.t()) :: String.t() | nil diff --git a/lib/service/workers/refresh_instances.ex b/lib/service/workers/refresh_instances.ex index 5381b1916..a1b93cf36 100644 --- a/lib/service/workers/refresh_instances.ex +++ b/lib/service/workers/refresh_instances.ex @@ -20,21 +20,16 @@ defmodule Mobilizon.Service.Workers.RefreshInstances do Instances.refresh() Instances.all_domains() - |> Enum.each(&refresh_instance_actor/1) + |> Enum.each(fn %Instance{domain: domain} -> refresh_instance_actor(domain) end) end - @spec refresh_instance_actor(Instance.t()) :: - {:ok, Mobilizon.Actors.Actor.t()} - | {:error, - ActivityPubActor.make_actor_errors() - | Mobilizon.Federation.WebFinger.finger_errors()} - def refresh_instance_actor(%Instance{domain: nil}) do + @spec refresh_instance_actor(String.t() | nil) :: + {:ok, Mobilizon.Actors.Actor.t()} | {:error, Ecto.Changeset.t()} | {:error, atom} + def refresh_instance_actor(nil) do {:error, :not_remote_instance} end - @spec refresh_instance_actor(Instance.t()) :: - {:ok, InstanceActor.t()} | {:error, Ecto.Changeset.t()} | {:error, atom} - def refresh_instance_actor(%Instance{domain: domain}) do + def refresh_instance_actor(domain) do %Actor{url: url} = Relay.get_actor() %URI{host: host} = URI.new!(url) @@ -48,16 +43,17 @@ defmodule Mobilizon.Service.Workers.RefreshInstances do end with instance_metadata <- fetch_instance_metadata(domain), - :ok <- Logger.debug("Ready to save instance actor details"), + args <- %{ + domain: domain, + actor_id: actor_id, + instance_name: get_in(instance_metadata, ["metadata", "nodeName"]), + instance_description: get_in(instance_metadata, ["metadata", "nodeDescription"]), + software: get_in(instance_metadata, ["software", "name"]), + software_version: get_in(instance_metadata, ["software", "version"]) + }, + :ok <- Logger.debug("Ready to save instance actor details #{inspect(args)}"), {:ok, %InstanceActor{}} <- - Instances.create_instance_actor(%{ - domain: domain, - actor_id: actor_id, - instance_name: get_in(instance_metadata, ["metadata", "nodeName"]), - instance_description: get_in(instance_metadata, ["metadata", "nodeDescription"]), - software: get_in(instance_metadata, ["software", "name"]), - software_version: get_in(instance_metadata, ["software", "version"]) - }) do + Instances.create_instance_actor(args) do Logger.info("Saved instance actor details for domain #{host}") else err -> diff --git a/src/components/core/MaterialIcon.vue b/src/components/core/MaterialIcon.vue index 32ba57983..ea527c729 100644 --- a/src/components/core/MaterialIcon.vue +++ b/src/components/core/MaterialIcon.vue @@ -262,6 +262,8 @@ const icons: Record Promise> = { import(`../../../node_modules/vue-material-design-icons/PencilOutline.vue`), Apps: () => import(`../../../node_modules/vue-material-design-icons/Apps.vue`), + Server: () => + import(`../../../node_modules/vue-material-design-icons/Server.vue`), }; const props = withDefaults( diff --git a/src/views/Admin/InstancesView.vue b/src/views/Admin/InstancesView.vue index 7f0a6cb5f..f37562736 100644 --- a/src/views/Admin/InstancesView.vue +++ b/src/views/Admin/InstancesView.vue @@ -72,11 +72,11 @@ name: RouteName.INSTANCE, params: { domain: instance.domain }, }" - class="flex items-center mb-2 rounded bg-mbz-yellow-alt-300 hover:bg-mbz-yellow-alt-200 dark:bg-mbz-purple-600 dark:hover:bg-mbz-purple-700 p-4 flex-wrap justify-center gap-x-2 gap-y-3" + class="min-w-0 flex items-center mb-2 rounded bg-mbz-yellow-alt-300 hover:bg-mbz-yellow-alt-200 dark:bg-mbz-purple-600 dark:hover:bg-mbz-purple-700 p-4 flex-wrap md:flex-nowrap justify-center gap-x-2 gap-y-3" v-for="instance in instances.elements" :key="instance.domain" > -
+

{{ instance.instanceName }} @@ -115,52 +115,62 @@ > {{ instance.domain }}

-

- {{ - instance.software - }} - - - {{ instance.domain }} -

-

- {{ instance.software }} -

- - - {{ t("Followed") }} - - - {{ t("Followed, pending response") }} - - - {{ t("Follows us") }} - - - {{ t("Follows us, pending approval") }} +
+
+

+ + {{ instance.domain }} +

+

+ + {{ instance.software }} +

+
+
+

+ + {{ t("Followed") }} +

+

+ + {{ t("Followed, pending response") }} +

+

+ + {{ t("Follows us") }} +

+

+ + {{ t("Follows us, pending approval") }} +

+
+
diff --git a/test/federation/node_info_test.exs b/test/federation/node_info_test.exs index d4f34d6c7..203040bc9 100644 --- a/test/federation/node_info_test.exs +++ b/test/federation/node_info_test.exs @@ -24,7 +24,12 @@ defmodule Mobilizon.Federation.NodeInfoTest do url: "https://event-federation.eu/.well-known/nodeinfo" }, _opts -> - {:ok, %Tesla.Env{status: 200, body: nodeinfo_data}} + {:ok, + %Tesla.Env{ + status: 200, + body: nodeinfo_data, + headers: [{"content-type", "application/json"}] + }} end) assert "https://event-federation.eu/actor-relay" == @@ -76,7 +81,12 @@ defmodule Mobilizon.Federation.NodeInfoTest do url: "https://mobilizon.fr/.well-known/nodeinfo" }, _opts -> - {:ok, %Tesla.Env{status: 200, body: nodeinfo_end_point_data}} + {:ok, + %Tesla.Env{ + status: 200, + body: nodeinfo_end_point_data, + headers: [{"content-type", "application/json"}] + }} end) WebfingerClientMock @@ -86,7 +96,12 @@ defmodule Mobilizon.Federation.NodeInfoTest do url: "https://mobilizon.fr/.well-known/nodeinfo/2.1" }, _opts -> - {:ok, %Tesla.Env{status: 200, body: nodeinfo_data}} + {:ok, + %Tesla.Env{ + status: 200, + body: nodeinfo_data, + headers: [{"content-type", "application/json"}] + }} end) assert {:ok, data} = NodeInfo.nodeinfo("mobilizon.fr") @@ -107,7 +122,12 @@ defmodule Mobilizon.Federation.NodeInfoTest do url: "https://event-federation.eu/.well-known/nodeinfo" }, _opts -> - {:ok, %Tesla.Env{status: 200, body: nodeinfo_end_point_data}} + {:ok, + %Tesla.Env{ + status: 200, + body: nodeinfo_end_point_data, + headers: [{"content-type", "application/json"}] + }} end) WebfingerClientMock @@ -117,7 +137,12 @@ defmodule Mobilizon.Federation.NodeInfoTest do url: "https://event-federation.eu/wp-json/activitypub/1.0/nodeinfo" }, _opts -> - {:ok, %Tesla.Env{status: 200, body: nodeinfo_wp_data}} + {:ok, + %Tesla.Env{ + status: 200, + body: nodeinfo_wp_data, + headers: [{"content-type", "application/json"}] + }} end) assert {:ok, data} = NodeInfo.nodeinfo("event-federation.eu") @@ -138,7 +163,12 @@ defmodule Mobilizon.Federation.NodeInfoTest do url: "https://somewhere.tld/.well-known/nodeinfo" }, _opts -> - {:ok, %Tesla.Env{status: 200, body: nodeinfo_end_point_data}} + {:ok, + %Tesla.Env{ + status: 200, + body: nodeinfo_end_point_data, + headers: [{"content-type", "application/json"}] + }} end) assert {:error, :no_node_info_endpoint_found} = NodeInfo.nodeinfo("somewhere.tld") @@ -169,7 +199,12 @@ defmodule Mobilizon.Federation.NodeInfoTest do url: "https://mobilizon.fr/.well-known/nodeinfo" }, _opts -> - {:ok, %Tesla.Env{status: 200, body: nodeinfo_end_point_data}} + {:ok, + %Tesla.Env{ + status: 200, + body: nodeinfo_end_point_data, + headers: [{"content-type", "application/json"}] + }} end) WebfingerClientMock diff --git a/test/service/workers/refresh_instances_test.exs b/test/service/workers/refresh_instances_test.exs index 0a5181238..0948b4740 100644 --- a/test/service/workers/refresh_instances_test.exs +++ b/test/service/workers/refresh_instances_test.exs @@ -5,7 +5,6 @@ defmodule Mobilizon.Service.Workers.RefreshInstancesTest do alias Mobilizon.Actors.Actor alias Mobilizon.Federation.ActivityPub.Relay - alias Mobilizon.Instances.Instance alias Mobilizon.Service.Workers.RefreshInstances use Mobilizon.DataCase @@ -14,7 +13,7 @@ defmodule Mobilizon.Service.Workers.RefreshInstancesTest do test "unless if local actor" do # relay = Mobilizon.Web.Relay.get_actor() assert {:error, :not_remote_instance} == - RefreshInstances.refresh_instance_actor(%Instance{domain: nil}) + RefreshInstances.refresh_instance_actor(nil) end test "unless if local relay actor" do @@ -22,7 +21,7 @@ defmodule Mobilizon.Service.Workers.RefreshInstancesTest do %URI{host: domain} = URI.new!(url) assert {:error, :not_remote_instance} == - RefreshInstances.refresh_instance_actor(%Instance{domain: domain}) + RefreshInstances.refresh_instance_actor(domain) end end end