forked from potsda.mn/mobilizon
Merge branch 'extract-nodeinfo-metadata' into 'main'
feat(nodeinfo): extract and save NodeInfo information from instances to... Closes #1392 See merge request framasoft/mobilizon!1513
This commit is contained in:
commit
58e4239aae
|
@ -7,22 +7,43 @@ defmodule Mobilizon.Federation.NodeInfo do
|
|||
require Logger
|
||||
|
||||
@application_uri "https://www.w3.org/ns/activitystreams#Application"
|
||||
@nodeinfo_rel_2_0 "http://nodeinfo.diaspora.software/ns/schema/2.0"
|
||||
@nodeinfo_rel_2_1 "http://nodeinfo.diaspora.software/ns/schema/2.1"
|
||||
|
||||
@env Application.compile_env(:mobilizon, :env)
|
||||
|
||||
@spec application_actor(String.t()) :: String.t() | nil
|
||||
def application_actor(host) do
|
||||
prefix = if @env !== :dev, do: "https", else: "http"
|
||||
Logger.debug("Fetching application actor from NodeInfo data for domain #{host}")
|
||||
|
||||
case WebfingerClient.get("#{prefix}://#{host}/.well-known/nodeinfo") do
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 ->
|
||||
case fetch_nodeinfo_endpoint(host) do
|
||||
{:ok, body} ->
|
||||
extract_application_actor(body)
|
||||
|
||||
err ->
|
||||
Logger.debug("Failed to fetch NodeInfo data #{inspect(err)}")
|
||||
{:error, :node_info_meta_http_error} ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec nodeinfo(String.t()) :: {:ok, map()} | {:error, atom()}
|
||||
def nodeinfo(host) do
|
||||
Logger.debug("Fetching NodeInfo details for domain #{host}")
|
||||
|
||||
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
|
||||
Logger.debug("Found nodeinfo information for domain #{host}")
|
||||
{:ok, body}
|
||||
else
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
err ->
|
||||
Logger.debug("Failed to fetch NodeInfo data from endpoint #{inspect(err)}")
|
||||
{:error, :node_info_endpoint_http_error}
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_application_actor(body) do
|
||||
body
|
||||
|> Map.get("links", [])
|
||||
|
@ -31,4 +52,54 @@ defmodule Mobilizon.Federation.NodeInfo do
|
|||
end)
|
||||
|> Map.get("href")
|
||||
end
|
||||
|
||||
@spec fetch_nodeinfo_endpoint(String.t()) :: {:ok, map()} | {:error, atom()}
|
||||
defp fetch_nodeinfo_endpoint(host) 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}
|
||||
|
||||
err ->
|
||||
Logger.debug("Failed to fetch NodeInfo data #{inspect(err)}")
|
||||
{:error, :node_info_meta_http_error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_nodeinfo_details(String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||
defp fetch_nodeinfo_details(host) do
|
||||
with {:ok, body} <- fetch_nodeinfo_endpoint(host) do
|
||||
extract_nodeinfo_endpoint(body)
|
||||
end
|
||||
end
|
||||
|
||||
@spec extract_nodeinfo_endpoint(map()) ::
|
||||
{:ok, String.t()}
|
||||
| {:error, :no_node_info_endpoint_found | :no_valid_node_info_endpoint_found}
|
||||
defp extract_nodeinfo_endpoint(body) do
|
||||
links = Map.get(body, "links", [])
|
||||
|
||||
relation =
|
||||
find_nodeinfo_relation(links, @nodeinfo_rel_2_1) ||
|
||||
find_nodeinfo_relation(links, @nodeinfo_rel_2_0)
|
||||
|
||||
if is_nil(relation) do
|
||||
{:error, :no_node_info_endpoint_found}
|
||||
else
|
||||
endpoint = Map.get(relation, "href")
|
||||
|
||||
if is_nil(endpoint) do
|
||||
{:error, :no_valid_node_info_endpoint_found}
|
||||
else
|
||||
{:ok, endpoint}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp find_nodeinfo_relation(links, relation) do
|
||||
Enum.find(links, fn %{"rel" => rel, "href" => href} ->
|
||||
rel == relation and is_binary(href)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -490,19 +490,35 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
when is_admin(role) do
|
||||
remote_relay = Actors.get_relay(domain)
|
||||
remote_relay = Instances.get_instance_actor(domain)
|
||||
local_relay = Relay.get_actor()
|
||||
|
||||
result = %{
|
||||
has_relay: !is_nil(remote_relay),
|
||||
relay_address:
|
||||
if(is_nil(remote_relay),
|
||||
do: nil,
|
||||
else: "#{remote_relay.preferred_username}@#{remote_relay.domain}"
|
||||
),
|
||||
follower_status: follow_status(remote_relay, local_relay),
|
||||
followed_status: follow_status(local_relay, remote_relay)
|
||||
}
|
||||
result =
|
||||
if is_nil(remote_relay) do
|
||||
%{
|
||||
has_relay: false,
|
||||
relay_address: nil,
|
||||
follower_status: nil,
|
||||
followed_status: nil,
|
||||
software: nil,
|
||||
software_version: nil
|
||||
}
|
||||
else
|
||||
%{
|
||||
has_relay: !is_nil(remote_relay.actor),
|
||||
relay_address:
|
||||
if(is_nil(remote_relay.actor),
|
||||
do: nil,
|
||||
else: Actor.preferred_username_and_domain(remote_relay.actor)
|
||||
),
|
||||
follower_status: follow_status(remote_relay.actor, local_relay),
|
||||
followed_status: follow_status(local_relay, remote_relay.actor),
|
||||
instance_name: remote_relay.instance_name,
|
||||
instance_description: remote_relay.instance_description,
|
||||
software: remote_relay.software,
|
||||
software_version: remote_relay.software_version
|
||||
}
|
||||
end
|
||||
|
||||
case Instances.instance(domain) do
|
||||
nil -> {:error, :not_found}
|
||||
|
|
|
@ -227,6 +227,16 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
|||
field(:relay_address, :string,
|
||||
description: "If this instance has a relay, it's federated username"
|
||||
)
|
||||
|
||||
field(:instance_name, :string, description: "This instance's name")
|
||||
|
||||
field(:instance_description, :string, description: "This instance's description")
|
||||
|
||||
field(:software, :string, description: "The software this instance declares running")
|
||||
|
||||
field(:software_version, :string,
|
||||
description: "The software version this instance declares running"
|
||||
)
|
||||
end
|
||||
|
||||
@desc """
|
||||
|
|
39
lib/mobilizon/instances/instance_actor.ex
Normal file
39
lib/mobilizon/instances/instance_actor.ex
Normal file
|
@ -0,0 +1,39 @@
|
|||
defmodule Mobilizon.Instances.InstanceActor do
|
||||
@moduledoc """
|
||||
An instance actor
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
domain: String.t(),
|
||||
actor: Actor.t(),
|
||||
instance_name: String.t(),
|
||||
instance_description: String.t(),
|
||||
software: String.t(),
|
||||
software_version: String.t()
|
||||
}
|
||||
|
||||
schema "instance_actors" do
|
||||
field(:domain, :string)
|
||||
field(:instance_name, :string)
|
||||
field(:instance_description, :string)
|
||||
field(:software, :string)
|
||||
field(:software_version, :string)
|
||||
belongs_to(:actor, Actor)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@required_attrs [:domain]
|
||||
@optional_attrs [:actor_id, :instance_name, :instance_description, :software, :software_version]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
def changeset(%__MODULE__{} = instance_actor, attrs) do
|
||||
instance_actor
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:domain)
|
||||
end
|
||||
end
|
|
@ -4,7 +4,7 @@ defmodule Mobilizon.Instances do
|
|||
"""
|
||||
alias Ecto.Adapters.SQL
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
alias Mobilizon.Instances.Instance
|
||||
alias Mobilizon.Instances.{Instance, InstanceActor}
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
import Ecto.Query
|
||||
|
||||
|
@ -12,13 +12,13 @@ defmodule Mobilizon.Instances do
|
|||
|
||||
@spec instances(Keyword.t()) :: Page.t(Instance.t())
|
||||
def instances(options) do
|
||||
page = Keyword.get(options, :page)
|
||||
limit = Keyword.get(options, :limit)
|
||||
order_by = Keyword.get(options, :order_by)
|
||||
direction = Keyword.get(options, :direction)
|
||||
page = Keyword.get(options, :page, 1)
|
||||
limit = Keyword.get(options, :limit, 10)
|
||||
order_by = Keyword.get(options, :order_by, :event_count)
|
||||
direction = Keyword.get(options, :direction, :desc)
|
||||
filter_domain = Keyword.get(options, :filter_domain)
|
||||
# suspend_status = Keyword.get(options, :filter_suspend_status)
|
||||
follow_status = Keyword.get(options, :filter_follow_status)
|
||||
# suspend_status = Keyword.get(options, :filter_suspend_status, :all)
|
||||
follow_status = Keyword.get(options, :filter_follow_status, :all)
|
||||
|
||||
order_by_options = Keyword.new([{direction, order_by}])
|
||||
|
||||
|
@ -42,7 +42,9 @@ defmodule Mobilizon.Instances do
|
|||
query =
|
||||
Instance
|
||||
|> join(:left, [i], s in subquery(subquery), on: i.domain == s.domain)
|
||||
|> select([i, s], {i, s})
|
||||
|> 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 =
|
||||
|
@ -100,16 +102,72 @@ defmodule Mobilizon.Instances do
|
|||
following: following,
|
||||
following_approved: following_approved,
|
||||
has_relay: has_relay
|
||||
}}
|
||||
}, instance_meta, instance_actor}
|
||||
) do
|
||||
instance
|
||||
|> Map.put(:follower_status, follow_status(following, following_approved))
|
||||
|> Map.put(:followed_status, follow_status(follower, follower_approved))
|
||||
|> Map.put(:has_relay, has_relay)
|
||||
|> Map.put(:instance_actor, instance_actor)
|
||||
|> add_metadata_details(instance_meta)
|
||||
end
|
||||
|
||||
@spec add_metadata_details(map(), InstanceActor.t() | nil) :: map()
|
||||
defp add_metadata_details(instance, nil), do: instance
|
||||
|
||||
defp add_metadata_details(instance, instance_meta) do
|
||||
instance
|
||||
|> Map.put(:instance_name, instance_meta.instance_name)
|
||||
|> Map.put(:instance_description, instance_meta.instance_description)
|
||||
|> Map.put(:software, instance_meta.software)
|
||||
|> Map.put(:software_version, instance_meta.software_version)
|
||||
end
|
||||
|
||||
defp follow_status(true, true), do: :approved
|
||||
defp follow_status(true, false), do: :pending
|
||||
defp follow_status(false, _), do: :none
|
||||
defp follow_status(nil, _), do: :none
|
||||
|
||||
@spec get_instance_actor(String.t()) :: InstanceActor.t() | nil
|
||||
def get_instance_actor(domain) do
|
||||
InstanceActor
|
||||
|> Repo.get_by(domain: domain)
|
||||
|> Repo.preload(:actor)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates an instance actor.
|
||||
"""
|
||||
@spec create_instance_actor(map) :: {:ok, InstanceActor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_instance_actor(attrs \\ %{}) do
|
||||
with {:ok, %InstanceActor{} = instance_actor} <-
|
||||
%InstanceActor{}
|
||||
|> InstanceActor.changeset(attrs)
|
||||
|> Repo.insert(on_conflict: :replace_all, conflict_target: :domain) do
|
||||
{:ok, Repo.preload(instance_actor, :actor)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates an instance actor.
|
||||
"""
|
||||
@spec update_instance_actor(InstanceActor.t(), map) ::
|
||||
{:ok, InstanceActor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update_instance_actor(%InstanceActor{} = instance_actor, attrs) do
|
||||
with {:ok, %InstanceActor{} = instance_actor} <-
|
||||
instance_actor
|
||||
|> Repo.preload(:actor)
|
||||
|> InstanceActor.changeset(attrs)
|
||||
|> Repo.update() do
|
||||
{:ok, Repo.preload(instance_actor, :actor)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a post
|
||||
"""
|
||||
@spec delete_instance_actor(InstanceActor.t()) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_instance_actor(%InstanceActor{} = instance_actor) do
|
||||
Repo.delete(instance_actor)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,14 +3,16 @@ defmodule Mobilizon.Service.Workers.RefreshInstances do
|
|||
Worker to refresh the instances materialized view and the relay actors
|
||||
"""
|
||||
|
||||
use Oban.Worker, unique: [period: :infinity, keys: [:event_uuid, :action]]
|
||||
use Oban.Worker
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.ActivityPub.Relay
|
||||
alias Mobilizon.Federation.NodeInfo
|
||||
alias Mobilizon.Instances
|
||||
alias Mobilizon.Instances.Instance
|
||||
alias Mobilizon.Instances.{Instance, InstanceActor}
|
||||
alias Oban.Job
|
||||
require Logger
|
||||
|
||||
@impl Oban.Worker
|
||||
@spec perform(Oban.Job.t()) :: :ok
|
||||
|
@ -30,6 +32,8 @@ defmodule Mobilizon.Service.Workers.RefreshInstances 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
|
||||
%Actor{url: url} = Relay.get_actor()
|
||||
%URI{host: host} = URI.new!(url)
|
||||
|
@ -37,7 +41,67 @@ defmodule Mobilizon.Service.Workers.RefreshInstances do
|
|||
if host == domain do
|
||||
{:error, :not_remote_instance}
|
||||
else
|
||||
ActivityPubActor.find_or_make_actor_from_nickname("relay@#{domain}")
|
||||
actor_id =
|
||||
case fetch_actor(domain) do
|
||||
{:ok, %Actor{id: actor_id}} -> actor_id
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
with instance_metadata <- fetch_instance_metadata(domain),
|
||||
:ok <- Logger.debug("Ready to save instance actor details"),
|
||||
{: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
|
||||
Logger.info("Saved instance actor details for domain #{host}")
|
||||
else
|
||||
err ->
|
||||
Logger.error(inspect(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp mobilizon(domain), do: "relay@#{domain}"
|
||||
defp peertube(domain), do: "peertube@#{domain}"
|
||||
defp mastodon(domain), do: "#{domain}@#{domain}"
|
||||
|
||||
defp fetch_actor(domain) do
|
||||
case NodeInfo.application_actor(domain) do
|
||||
nil -> guess_application_actor(domain)
|
||||
url -> ActivityPubActor.get_or_fetch_actor_by_url(url)
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_instance_metadata(domain) do
|
||||
case NodeInfo.nodeinfo(domain) do
|
||||
{:error, _} ->
|
||||
%{}
|
||||
|
||||
{:ok, metadata} ->
|
||||
metadata
|
||||
end
|
||||
end
|
||||
|
||||
defp guess_application_actor(domain) do
|
||||
Enum.find_value(
|
||||
[
|
||||
&mobilizon/1,
|
||||
&peertube/1,
|
||||
&mastodon/1
|
||||
],
|
||||
{:error, :no_application_actor_found},
|
||||
fn username_pattern ->
|
||||
case ActivityPubActor.find_or_make_actor_from_nickname(username_pattern.(domain)) do
|
||||
{:ok, %Actor{type: :Application} = actor} -> {:ok, actor}
|
||||
{:error, _err} -> false
|
||||
{:ok, _actor} -> false
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
17
priv/repo/migrations/20231220092536_add_actor_instances.exs
Normal file
17
priv/repo/migrations/20231220092536_add_actor_instances.exs
Normal file
|
@ -0,0 +1,17 @@
|
|||
defmodule Mobilizon.Storage.Repo.Migrations.AddActorInstances do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:instance_actors) do
|
||||
add(:domain, :string)
|
||||
add(:instance_name, :string)
|
||||
add(:instance_description, :string)
|
||||
add(:software, :string)
|
||||
add(:software_version, :string)
|
||||
add(:actor_id, references(:actors, on_delete: :delete_all))
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create(unique_index(:instance_actors, [:domain]))
|
||||
end
|
||||
end
|
BIN
public/img/gancio.png
Normal file
BIN
public/img/gancio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
21
public/img/wordpress-logo.svg
Normal file
21
public/img/wordpress-logo.svg
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="800px" height="800px" viewBox="0 0 96.24 96.24" xml:space="preserve"
|
||||
>
|
||||
<g>
|
||||
<path d="M48.122,0C21.587,0,0.001,21.585,0.001,48.118c0,26.535,21.587,48.122,48.12,48.122c26.532,0,48.117-21.587,48.117-48.122
|
||||
C96.239,21.586,74.654,0,48.122,0z M4.857,48.118c0-6.271,1.345-12.227,3.746-17.606l20.638,56.544
|
||||
C14.81,80.042,4.857,65.243,4.857,48.118z M48.122,91.385c-4.247,0-8.346-0.623-12.222-1.763L48.88,51.903l13.301,36.433
|
||||
c0.086,0.215,0.191,0.411,0.308,0.596C57.992,90.514,53.16,91.385,48.122,91.385z M54.083,27.834
|
||||
c2.604-0.137,4.953-0.412,4.953-0.412c2.33-0.276,2.057-3.701-0.277-3.564c0,0-7.007,0.549-11.532,0.549
|
||||
c-4.25,0-11.396-0.549-11.396-0.549c-2.332-0.137-2.604,3.427-0.273,3.564c0,0,2.208,0.275,4.537,0.412l6.74,18.469l-9.468,28.395
|
||||
L21.615,27.835c2.608-0.136,4.952-0.412,4.952-0.412c2.33-0.275,2.055-3.702-0.278-3.562c0,0-7.004,0.549-11.53,0.549
|
||||
c-0.813,0-1.77-0.021-2.784-0.052C19.709,12.611,33.008,4.856,48.122,4.856c11.265,0,21.519,4.306,29.215,11.357
|
||||
c-0.187-0.01-0.368-0.035-0.562-0.035c-4.248,0-7.264,3.702-7.264,7.679c0,3.564,2.055,6.582,4.248,10.146
|
||||
c1.647,2.882,3.567,6.585,3.567,11.932c0,3.704-1.422,8-3.293,13.986l-4.315,14.421L54.083,27.834z M69.871,85.516l13.215-38.208
|
||||
c2.471-6.171,3.29-11.106,3.29-15.497c0-1.591-0.104-3.07-0.292-4.449c3.38,6.163,5.303,13.236,5.301,20.758
|
||||
C91.384,64.08,82.732,78.016,69.871,85.516z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -293,7 +293,7 @@ button.menubar__button {
|
|||
@apply px-3 dark:text-black;
|
||||
}
|
||||
.pagination-link-current {
|
||||
@apply bg-primary dark:bg-primary cursor-not-allowed pointer-events-none border-primary text-white dark:text-zinc-900;
|
||||
@apply bg-primary dark:bg-primary cursor-not-allowed pointer-events-none border-primary text-white;
|
||||
}
|
||||
.pagination-ellipsis {
|
||||
@apply text-center m-1 text-gray-300;
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<li
|
||||
class="setting-menu-item"
|
||||
:class="{
|
||||
'cursor-pointer bg-mbz-yellow-alt-500 dark:bg-mbz-purple-500': isActive,
|
||||
'bg-mbz-yellow-alt-100 hover:bg-mbz-yellow-alt-200 dark:bg-mbz-purple-300 dark:hover:bg-mbz-purple-400 dark:text-white':
|
||||
'cursor-pointer bg-mbz-yellow-alt-500 dark:bg-mbz-purple-600': isActive,
|
||||
'bg-mbz-yellow-alt-100 hover:bg-mbz-yellow-alt-200 dark:bg-mbz-purple-500 dark:hover:bg-mbz-purple-600 dark:text-white':
|
||||
!isActive,
|
||||
}"
|
||||
>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<li
|
||||
class="bg-mbz-yellow-alt-300 text-violet-2 dark:bg-mbz-purple-500 dark:text-zinc-100 text-xl"
|
||||
class="bg-mbz-yellow-alt-300 text-violet-2 dark:bg-mbz-purple-700 dark:text-zinc-100 text-xl"
|
||||
>
|
||||
<router-link
|
||||
class="cursor-pointer my-2 mx-0 py-2 px-3 font-medium block no-underline"
|
||||
|
|
|
@ -81,6 +81,10 @@ export const INSTANCE_FRAGMENT = gql`
|
|||
fragment InstanceFragment on Instance {
|
||||
domain
|
||||
hasRelay
|
||||
instanceName
|
||||
instanceDescription
|
||||
software
|
||||
softwareVersion
|
||||
relayAddress
|
||||
followerStatus
|
||||
followedStatus
|
||||
|
|
|
@ -1638,5 +1638,8 @@
|
|||
"Return to the event page": "Return to the event page",
|
||||
"Cancel participation": "Cancel participation",
|
||||
"Add a recipient": "Add a recipient",
|
||||
"Announcements for {eventTitle}": "Announcements for {eventTitle}"
|
||||
"Announcements for {eventTitle}": "Announcements for {eventTitle}",
|
||||
"Visit {instance_domain}": "Visit {instance_domain}",
|
||||
"Software details: {software_details}": "Software details: {software_details}",
|
||||
"Only instances with an application actor can be followed": "Only instances with an application actor can be followed"
|
||||
}
|
|
@ -1632,5 +1632,8 @@
|
|||
"Return to the event page": "Retourner à la page de l'événement",
|
||||
"Cancel participation": "Annuler la participation",
|
||||
"Add a recipient": "Ajouter un·e destinataire",
|
||||
"Announcements for {eventTitle}": "Annonces pour {eventTitle}"
|
||||
"Announcements for {eventTitle}": "Annonces pour {eventTitle}",
|
||||
"Visit {instance_domain}": "Visiter {instance_domain}",
|
||||
"Software details: {software_details}": "Détails du logiciel : {software_details}",
|
||||
"Only instances with an application actor can be followed": "Seules les instances avec un acteur application peuvent être suivies"
|
||||
}
|
||||
|
|
|
@ -3,6 +3,10 @@ import { InstanceFollowStatus } from "./enums";
|
|||
export interface IInstance {
|
||||
domain: string;
|
||||
hasRelay: boolean;
|
||||
instanceName: string | null;
|
||||
instanceDescription: string | null;
|
||||
software: string | null;
|
||||
softwareVersion: string | null;
|
||||
relayAddress: string | null;
|
||||
followerStatus: InstanceFollowStatus;
|
||||
followedStatus: InstanceFollowStatus;
|
||||
|
|
|
@ -2,138 +2,187 @@
|
|||
<div v-if="instance">
|
||||
<breadcrumbs-nav
|
||||
:links="[
|
||||
{ name: RouteName.ADMIN, text: $t('Admin') },
|
||||
{ name: RouteName.INSTANCES, text: $t('Instances') },
|
||||
{ name: RouteName.ADMIN, text: t('Admin') },
|
||||
{ name: RouteName.INSTANCES, text: t('Instances') },
|
||||
{ text: instance.domain },
|
||||
]"
|
||||
/>
|
||||
<h1 class="text-2xl">{{ instance.domain }}</h1>
|
||||
<div
|
||||
class="grid md:grid-cols-2 xl:grid-cols-4 gap-2 content-center text-center mt-2"
|
||||
<section
|
||||
class="flex flex-wrap md:flex-nowrap items-center justify-between gap-4"
|
||||
>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PROFILES,
|
||||
query: { domain: instance.domain },
|
||||
}"
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold" v-if="instance.instanceName">
|
||||
{{ instance.instanceName }}
|
||||
</h2>
|
||||
<h2 class="text-4xl font-bold" v-else>{{ instance.domain }}</h2>
|
||||
<p
|
||||
v-if="instance.instanceDescription"
|
||||
class="text-slate-700 dark:text-slate-400 my-4"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.personCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Profiles") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_GROUPS,
|
||||
query: { domain: instance.domain },
|
||||
}"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.groupCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Groups") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.followingsCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Followings") }}</span>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.followersCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Followers") }}</span>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{ name: RouteName.REPORTS, query: { domain: instance.domain } }"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.reportsCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Reports") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 font-semibold block">{{
|
||||
formatBytes(instance.mediaSize)
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Uploaded media size") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 grid xl:grid-cols-2 gap-4">
|
||||
<div
|
||||
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md"
|
||||
v-if="instance.hasRelay"
|
||||
>
|
||||
<button
|
||||
@click="
|
||||
removeInstanceFollow({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followedStatus == InstanceFollowStatus.APPROVED"
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Stop following instance") }}
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
removeInstanceFollow({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-else-if="instance.followedStatus == InstanceFollowStatus.PENDING"
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Cancel follow request") }}
|
||||
</button>
|
||||
<button
|
||||
@click="followInstance"
|
||||
v-else
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Follow instance") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="md:h-48 py-16 text-center opacity-50">
|
||||
{{ $t("Only Mobilizon instances can be followed") }}
|
||||
</div>
|
||||
<div
|
||||
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2"
|
||||
>
|
||||
<button
|
||||
@click="
|
||||
acceptInstance({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
|
||||
class="bg-green-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Accept follow") }}
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
rejectInstance({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followerStatus != InstanceFollowStatus.NONE"
|
||||
class="bg-red-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Reject follow") }}
|
||||
</button>
|
||||
<p v-if="instance.followerStatus == InstanceFollowStatus.NONE">
|
||||
{{ $t("This instance doesn't follow yours.") }}
|
||||
{{ instance.instanceDescription }}
|
||||
</p>
|
||||
<i18n-t
|
||||
v-if="instance.software && instance.softwareVersion"
|
||||
keypath="Software details: {software_details}"
|
||||
class="my-4"
|
||||
>
|
||||
<template #software_details>
|
||||
<span class="capitalize">
|
||||
{{ instance.software }} - {{ instance.softwareVersion }}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
<o-button
|
||||
tag="a"
|
||||
:href="`https://${instance.domain}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
icon-right="open-in-new"
|
||||
class="mx-auto md:mx-0"
|
||||
>{{
|
||||
t("Visit {instance_domain}", { instance_domain: instance.domain })
|
||||
}}
|
||||
</o-button>
|
||||
</section>
|
||||
<section>
|
||||
<div
|
||||
class="grid md:grid-cols-2 xl:grid-cols-4 gap-2 content-center text-center mt-2"
|
||||
>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PROFILES,
|
||||
query: { domain: instance.domain },
|
||||
}"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.personCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ t("Profiles") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_GROUPS,
|
||||
query: { domain: instance.domain },
|
||||
}"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.groupCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ t("Groups") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.followingsCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ t("Followings") }}</span>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.followersCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ t("Followers") }}</span>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.REPORTS,
|
||||
query: { domain: instance.domain },
|
||||
}"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.reportsCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ t("Reports") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||
<span class="mb-4 font-semibold block">{{
|
||||
formatBytes(instance.mediaSize)
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ t("Uploaded media size") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="mt-3 grid xl:grid-cols-2 gap-4">
|
||||
<div
|
||||
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md"
|
||||
v-if="
|
||||
instance.hasRelay &&
|
||||
!['mastodon', 'peertube'].includes(
|
||||
instance.software?.toLowerCase() ?? ''
|
||||
)
|
||||
"
|
||||
>
|
||||
<button
|
||||
@click="
|
||||
removeInstanceFollow({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followedStatus == InstanceFollowStatus.APPROVED"
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ t("Stop following instance") }}
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
removeInstanceFollow({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-else-if="instance.followedStatus == InstanceFollowStatus.PENDING"
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ t("Cancel follow request") }}
|
||||
</button>
|
||||
<button
|
||||
@click="followInstance"
|
||||
v-else
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ t("Follow instance") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="md:h-48 py-16 text-center opacity-50">
|
||||
{{ t("Only instances with an application actor can be followed") }}
|
||||
</div>
|
||||
<div
|
||||
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2"
|
||||
>
|
||||
<button
|
||||
@click="
|
||||
acceptInstance({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
|
||||
class="bg-green-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ t("Accept follow") }}
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
rejectInstance({
|
||||
address: instance?.relayAddress,
|
||||
})
|
||||
"
|
||||
v-if="instance.followerStatus != InstanceFollowStatus.NONE"
|
||||
class="bg-red-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ t("Reject follow") }}
|
||||
</button>
|
||||
<p v-if="instance.followerStatus == InstanceFollowStatus.NONE">
|
||||
{{ t("This instance doesn't follow yours.") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
|
@ -152,6 +201,7 @@ import { InstanceFollowStatus } from "@/types/enums";
|
|||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import { computed, inject } from "vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{ domain: string }>();
|
||||
|
||||
|
@ -164,6 +214,8 @@ const instance = computed(() => instanceResult.value?.instance);
|
|||
|
||||
const notifier = inject<Notifier>("notifier");
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const { mutate: acceptInstance, onError: onAcceptInstanceError } = useMutation(
|
||||
ACCEPT_RELAY,
|
||||
() => ({
|
||||
|
|
|
@ -72,21 +72,65 @@
|
|||
name: RouteName.INSTANCE,
|
||||
params: { domain: instance.domain },
|
||||
}"
|
||||
class="flex items-center mb-2 rounded bg-mbz-yellow-alt-300 dark:bg-mbz-purple-400 p-4 flex-wrap justify-center gap-x-2 gap-y-3"
|
||||
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"
|
||||
v-for="instance in instances.elements"
|
||||
:key="instance.domain"
|
||||
>
|
||||
<div class="grow overflow-hidden flex items-center gap-1">
|
||||
<img
|
||||
class="w-12"
|
||||
v-if="instance.hasRelay"
|
||||
v-if="instance.software === 'Mobilizon'"
|
||||
src="/img/logo.svg"
|
||||
alt=""
|
||||
/>
|
||||
<CloudQuestion v-else :size="36" />
|
||||
<mastodon-logo
|
||||
class="w-8 mx-2"
|
||||
alt=""
|
||||
v-else-if="instance.software?.toLowerCase() === 'mastodon'"
|
||||
/>
|
||||
<img
|
||||
class="w-8 mx-2"
|
||||
v-else-if="instance.software?.toLowerCase() === 'gancio'"
|
||||
src="/img/gancio.png"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
class="w-8 mx-2"
|
||||
v-else-if="instance.software?.toLowerCase() === 'wordpress'"
|
||||
src="/img/wordpress-logo.svg"
|
||||
alt=""
|
||||
/>
|
||||
<CloudQuestion class="mx-1.5" v-else :size="36" />
|
||||
|
||||
<div class="">
|
||||
<h3 class="text-lg truncate">{{ instance.domain }}</h3>
|
||||
<h3
|
||||
class="text-lg truncate font-bold text-slate-800 dark:text-slate-100"
|
||||
v-if="instance.instanceName"
|
||||
>
|
||||
{{ instance.instanceName }}
|
||||
</h3>
|
||||
<h3
|
||||
class="text-lg truncate font-bold text-slate-800 dark:text-slate-100"
|
||||
v-else
|
||||
>
|
||||
{{ instance.domain }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="instance.instanceName"
|
||||
class="inline-flex gap-2 text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<span class="capitalize" v-if="instance.software">{{
|
||||
instance.software
|
||||
}}</span>
|
||||
-
|
||||
<span>{{ instance.domain }}</span>
|
||||
</p>
|
||||
<p
|
||||
v-else-if="instance.software"
|
||||
class="capitalize text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
{{ instance.software }}
|
||||
</p>
|
||||
<span
|
||||
class="text-sm"
|
||||
v-if="instance.followedStatus === InstanceFollowStatus.APPROVED"
|
||||
|
@ -186,6 +230,7 @@ import { useRouter } from "vue-router";
|
|||
import { useHead } from "@unhead/vue";
|
||||
import CloudQuestion from "../../../node_modules/vue-material-design-icons/CloudQuestion.vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import MastodonLogo from "@/components/Share/MastodonLogo.vue";
|
||||
|
||||
const INSTANCES_PAGE_LIMIT = 10;
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ defmodule Mobilizon.Federation.NodeInfoTest do
|
|||
@instance_domain "event-federation.eu"
|
||||
@nodeinfo_fixture_path "test/fixtures/nodeinfo/wp-event-federation.json"
|
||||
@nodeinfo_regular_fixture_path "test/fixtures/nodeinfo/regular.json"
|
||||
@nodeinfo_both_versions_fixture_path "test/fixtures/nodeinfo/both_versions.json"
|
||||
@nodeinfo_older_versions_fixture_path "test/fixtures/nodeinfo/older_versions.json"
|
||||
@nodeinfo_data_fixture_path "test/fixtures/nodeinfo/data.json"
|
||||
@nodeinfo_wp_data_fixture_path "test/fixtures/nodeinfo/wp-data.json"
|
||||
|
||||
describe "getting application actor" do
|
||||
test "from wordpress event federation" do
|
||||
|
@ -57,4 +61,128 @@ defmodule Mobilizon.Federation.NodeInfoTest do
|
|||
assert nil == NodeInfo.application_actor(@instance_domain)
|
||||
end
|
||||
end
|
||||
|
||||
describe "getting informations" do
|
||||
test "with both 2.0 and 2.1 endpoints" do
|
||||
nodeinfo_end_point_data =
|
||||
@nodeinfo_both_versions_fixture_path |> File.read!() |> Jason.decode!()
|
||||
|
||||
nodeinfo_data = @nodeinfo_data_fixture_path |> File.read!() |> Jason.decode!()
|
||||
|
||||
WebfingerClientMock
|
||||
|> expect(:call, fn
|
||||
%{
|
||||
method: :get,
|
||||
url: "https://mobilizon.fr/.well-known/nodeinfo"
|
||||
},
|
||||
_opts ->
|
||||
{:ok, %Tesla.Env{status: 200, body: nodeinfo_end_point_data}}
|
||||
end)
|
||||
|
||||
WebfingerClientMock
|
||||
|> expect(:call, fn
|
||||
%{
|
||||
method: :get,
|
||||
url: "https://mobilizon.fr/.well-known/nodeinfo/2.1"
|
||||
},
|
||||
_opts ->
|
||||
{:ok, %Tesla.Env{status: 200, body: nodeinfo_data}}
|
||||
end)
|
||||
|
||||
assert {:ok, data} = NodeInfo.nodeinfo("mobilizon.fr")
|
||||
assert data["version"] == "2.1"
|
||||
assert data["software"]["name"] == "Mobilizon"
|
||||
# Added in 2.1
|
||||
refute is_nil(data["software"]["repository"])
|
||||
end
|
||||
|
||||
test "with only 2.0 endpoint" do
|
||||
nodeinfo_end_point_data = @nodeinfo_regular_fixture_path |> File.read!() |> Jason.decode!()
|
||||
nodeinfo_wp_data = @nodeinfo_wp_data_fixture_path |> File.read!() |> Jason.decode!()
|
||||
|
||||
WebfingerClientMock
|
||||
|> expect(:call, fn
|
||||
%{
|
||||
method: :get,
|
||||
url: "https://event-federation.eu/.well-known/nodeinfo"
|
||||
},
|
||||
_opts ->
|
||||
{:ok, %Tesla.Env{status: 200, body: nodeinfo_end_point_data}}
|
||||
end)
|
||||
|
||||
WebfingerClientMock
|
||||
|> expect(:call, fn
|
||||
%{
|
||||
method: :get,
|
||||
url: "https://event-federation.eu/wp-json/activitypub/1.0/nodeinfo"
|
||||
},
|
||||
_opts ->
|
||||
{:ok, %Tesla.Env{status: 200, body: nodeinfo_wp_data}}
|
||||
end)
|
||||
|
||||
assert {:ok, data} = NodeInfo.nodeinfo("event-federation.eu")
|
||||
assert data["version"] == "2.0"
|
||||
assert data["software"]["name"] == "wordpress"
|
||||
# Added in 2.1
|
||||
assert is_nil(data["software"]["repository"])
|
||||
end
|
||||
|
||||
test "with no valid endpoint" do
|
||||
nodeinfo_end_point_data =
|
||||
@nodeinfo_older_versions_fixture_path |> File.read!() |> Jason.decode!()
|
||||
|
||||
WebfingerClientMock
|
||||
|> expect(:call, fn
|
||||
%{
|
||||
method: :get,
|
||||
url: "https://somewhere.tld/.well-known/nodeinfo"
|
||||
},
|
||||
_opts ->
|
||||
{:ok, %Tesla.Env{status: 200, body: nodeinfo_end_point_data}}
|
||||
end)
|
||||
|
||||
assert {:error, :no_node_info_endpoint_found} = NodeInfo.nodeinfo("somewhere.tld")
|
||||
end
|
||||
|
||||
test "with no answer from well-known endpoint" do
|
||||
WebfingerClientMock
|
||||
|> expect(:call, fn
|
||||
%{
|
||||
method: :get,
|
||||
url: "https://somewhere.tld/.well-known/nodeinfo"
|
||||
},
|
||||
_opts ->
|
||||
{:ok, %Tesla.Env{status: 404}}
|
||||
end)
|
||||
|
||||
assert {:error, :node_info_meta_http_error} = NodeInfo.nodeinfo("somewhere.tld")
|
||||
end
|
||||
|
||||
test "with no answer from version endpoint" do
|
||||
nodeinfo_end_point_data =
|
||||
@nodeinfo_both_versions_fixture_path |> File.read!() |> Jason.decode!()
|
||||
|
||||
WebfingerClientMock
|
||||
|> expect(:call, fn
|
||||
%{
|
||||
method: :get,
|
||||
url: "https://mobilizon.fr/.well-known/nodeinfo"
|
||||
},
|
||||
_opts ->
|
||||
{:ok, %Tesla.Env{status: 200, body: nodeinfo_end_point_data}}
|
||||
end)
|
||||
|
||||
WebfingerClientMock
|
||||
|> expect(:call, fn
|
||||
%{
|
||||
method: :get,
|
||||
url: "https://mobilizon.fr/.well-known/nodeinfo/2.1"
|
||||
},
|
||||
_opts ->
|
||||
{:ok, %Tesla.Env{status: 404}}
|
||||
end)
|
||||
|
||||
assert {:error, :node_info_endpoint_http_error} = NodeInfo.nodeinfo("mobilizon.fr")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
12
test/fixtures/nodeinfo/both_versions.json
vendored
Normal file
12
test/fixtures/nodeinfo/both_versions.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href": "https://mobilizon.fr/.well-known/nodeinfo/2.0"
|
||||
},
|
||||
{
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
||||
"href": "https://mobilizon.fr/.well-known/nodeinfo/2.1"
|
||||
}
|
||||
]
|
||||
}
|
29
test/fixtures/nodeinfo/data.json
vendored
Normal file
29
test/fixtures/nodeinfo/data.json
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"version": "2.1",
|
||||
"protocols": [
|
||||
"activitypub"
|
||||
],
|
||||
"metadata": {
|
||||
"nodeDescription": "Mobilizon.fr est l'instance Mobilizon de Framasoft.",
|
||||
"nodeName": "Mobilizon"
|
||||
},
|
||||
"usage": {
|
||||
"users": {
|
||||
"total": 9204
|
||||
},
|
||||
"localComments": 3253,
|
||||
"localPosts": 7545
|
||||
},
|
||||
"services": {
|
||||
"outbound": [
|
||||
"atom1.0"
|
||||
],
|
||||
"inbound": []
|
||||
},
|
||||
"software": {
|
||||
"name": "Mobilizon",
|
||||
"version": "4.0.2",
|
||||
"repository": "https://framagit.org/framasoft/mobilizon"
|
||||
},
|
||||
"openRegistrations": true
|
||||
}
|
8
test/fixtures/nodeinfo/older_versions.json
vendored
Normal file
8
test/fixtures/nodeinfo/older_versions.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/1.1",
|
||||
"href": "https://mobilizon.fr/.well-known/nodeinfo/1.1"
|
||||
}
|
||||
]
|
||||
}
|
4
test/fixtures/nodeinfo/regular.json
vendored
4
test/fixtures/nodeinfo/regular.json
vendored
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"links": [
|
||||
{
|
||||
"rel": "http:\/\/nodeinfo.diaspora.software\/ns\/schema\/2.0",
|
||||
"href": "https:\/\/event-federation.eu\/wp-json\/activitypub\/1.0\/nodeinfo"
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href": "https://event-federation.eu/wp-json/activitypub/1.0/nodeinfo"
|
||||
}
|
||||
]
|
||||
}
|
24
test/fixtures/nodeinfo/wp-data.json
vendored
Normal file
24
test/fixtures/nodeinfo/wp-data.json
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"version": "2.0",
|
||||
"software": {
|
||||
"name": "wordpress",
|
||||
"version": "6.4.2"
|
||||
},
|
||||
"usage": {
|
||||
"users": {
|
||||
"total": 1,
|
||||
"activeMonth": 1,
|
||||
"activeHalfyear": 1
|
||||
},
|
||||
"localPosts": 3,
|
||||
"localComments": 3
|
||||
},
|
||||
"openRegistrations": false,
|
||||
"protocols": [
|
||||
"activitypub"
|
||||
],
|
||||
"services": {
|
||||
"inbound": [],
|
||||
"outbound": []
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"links": [
|
||||
{
|
||||
"rel": "http:\/\/nodeinfo.diaspora.software\/ns\/schema\/2.0",
|
||||
"href": "https:\/\/event-federation.eu\/wp-json\/activitypub\/1.0\/nodeinfo"
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href": "https://event-federation.eu/wp-json/activitypub/1.0/nodeinfo"
|
||||
},
|
||||
{
|
||||
"rel": "https://www.w3.org/ns/activitystreams#Application",
|
||||
|
|
Loading…
Reference in a new issue