forked from potsda.mn/mobilizon
feat(nodeinfo): extract and save NodeInfo information from instances to display it on instances list
We also try to detect the application actor if it's not given by NodeInfo metadata (FEP-2677) (guessing for Mobilizon, PeerTube & Mastodon). Closes #1392 Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
2fba6379f1
commit
99b2339424
|
@ -7,22 +7,43 @@ defmodule Mobilizon.Federation.NodeInfo do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@application_uri "https://www.w3.org/ns/activitystreams#Application"
|
@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)
|
@env Application.compile_env(:mobilizon, :env)
|
||||||
|
|
||||||
@spec application_actor(String.t()) :: String.t() | nil
|
@spec application_actor(String.t()) :: String.t() | nil
|
||||||
def application_actor(host) do
|
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
|
case fetch_nodeinfo_endpoint(host) do
|
||||||
{:ok, %{body: body, status: code}} when code in 200..299 ->
|
{:ok, body} ->
|
||||||
extract_application_actor(body)
|
extract_application_actor(body)
|
||||||
|
|
||||||
err ->
|
{:error, :node_info_meta_http_error} ->
|
||||||
Logger.debug("Failed to fetch NodeInfo data #{inspect(err)}")
|
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
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
|
defp extract_application_actor(body) do
|
||||||
body
|
body
|
||||||
|> Map.get("links", [])
|
|> Map.get("links", [])
|
||||||
|
@ -31,4 +52,54 @@ defmodule Mobilizon.Federation.NodeInfo do
|
||||||
end)
|
end)
|
||||||
|> Map.get("href")
|
|> Map.get("href")
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -490,19 +490,35 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
||||||
context: %{current_user: %User{role: role}}
|
context: %{current_user: %User{role: role}}
|
||||||
})
|
})
|
||||||
when is_admin(role) do
|
when is_admin(role) do
|
||||||
remote_relay = Actors.get_relay(domain)
|
remote_relay = Instances.get_instance_actor(domain)
|
||||||
local_relay = Relay.get_actor()
|
local_relay = Relay.get_actor()
|
||||||
|
|
||||||
result = %{
|
result =
|
||||||
has_relay: !is_nil(remote_relay),
|
if is_nil(remote_relay) do
|
||||||
relay_address:
|
%{
|
||||||
if(is_nil(remote_relay),
|
has_relay: false,
|
||||||
do: nil,
|
relay_address: nil,
|
||||||
else: "#{remote_relay.preferred_username}@#{remote_relay.domain}"
|
follower_status: nil,
|
||||||
),
|
followed_status: nil,
|
||||||
follower_status: follow_status(remote_relay, local_relay),
|
software: nil,
|
||||||
followed_status: follow_status(local_relay, remote_relay)
|
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
|
case Instances.instance(domain) do
|
||||||
nil -> {:error, :not_found}
|
nil -> {:error, :not_found}
|
||||||
|
|
|
@ -227,6 +227,16 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||||
field(:relay_address, :string,
|
field(:relay_address, :string,
|
||||||
description: "If this instance has a relay, it's federated username"
|
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
|
end
|
||||||
|
|
||||||
@desc """
|
@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 Ecto.Adapters.SQL
|
||||||
alias Mobilizon.Actors.{Actor, Follower}
|
alias Mobilizon.Actors.{Actor, Follower}
|
||||||
alias Mobilizon.Instances.Instance
|
alias Mobilizon.Instances.{Instance, InstanceActor}
|
||||||
alias Mobilizon.Storage.{Page, Repo}
|
alias Mobilizon.Storage.{Page, Repo}
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
|
@ -12,13 +12,13 @@ defmodule Mobilizon.Instances do
|
||||||
|
|
||||||
@spec instances(Keyword.t()) :: Page.t(Instance.t())
|
@spec instances(Keyword.t()) :: Page.t(Instance.t())
|
||||||
def instances(options) do
|
def instances(options) do
|
||||||
page = Keyword.get(options, :page)
|
page = Keyword.get(options, :page, 1)
|
||||||
limit = Keyword.get(options, :limit)
|
limit = Keyword.get(options, :limit, 10)
|
||||||
order_by = Keyword.get(options, :order_by)
|
order_by = Keyword.get(options, :order_by, :event_count)
|
||||||
direction = Keyword.get(options, :direction)
|
direction = Keyword.get(options, :direction, :desc)
|
||||||
filter_domain = Keyword.get(options, :filter_domain)
|
filter_domain = Keyword.get(options, :filter_domain)
|
||||||
# suspend_status = Keyword.get(options, :filter_suspend_status)
|
# suspend_status = Keyword.get(options, :filter_suspend_status, :all)
|
||||||
follow_status = Keyword.get(options, :filter_follow_status)
|
follow_status = Keyword.get(options, :filter_follow_status, :all)
|
||||||
|
|
||||||
order_by_options = Keyword.new([{direction, order_by}])
|
order_by_options = Keyword.new([{direction, order_by}])
|
||||||
|
|
||||||
|
@ -42,7 +42,9 @@ defmodule Mobilizon.Instances do
|
||||||
query =
|
query =
|
||||||
Instance
|
Instance
|
||||||
|> join(:left, [i], s in subquery(subquery), on: i.domain == s.domain)
|
|> 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)
|
|> order_by(^order_by_options)
|
||||||
|
|
||||||
query =
|
query =
|
||||||
|
@ -100,16 +102,72 @@ defmodule Mobilizon.Instances do
|
||||||
following: following,
|
following: following,
|
||||||
following_approved: following_approved,
|
following_approved: following_approved,
|
||||||
has_relay: has_relay
|
has_relay: has_relay
|
||||||
}}
|
}, instance_meta, instance_actor}
|
||||||
) do
|
) do
|
||||||
instance
|
instance
|
||||||
|> Map.put(:follower_status, follow_status(following, following_approved))
|
|> Map.put(:follower_status, follow_status(following, following_approved))
|
||||||
|> Map.put(:followed_status, follow_status(follower, follower_approved))
|
|> Map.put(:followed_status, follow_status(follower, follower_approved))
|
||||||
|> Map.put(:has_relay, has_relay)
|
|> 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
|
end
|
||||||
|
|
||||||
defp follow_status(true, true), do: :approved
|
defp follow_status(true, true), do: :approved
|
||||||
defp follow_status(true, false), do: :pending
|
defp follow_status(true, false), do: :pending
|
||||||
defp follow_status(false, _), do: :none
|
defp follow_status(false, _), do: :none
|
||||||
defp follow_status(nil, _), 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
|
end
|
||||||
|
|
|
@ -3,14 +3,16 @@ defmodule Mobilizon.Service.Workers.RefreshInstances do
|
||||||
Worker to refresh the instances materialized view and the relay actors
|
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.Actors.Actor
|
||||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||||
alias Mobilizon.Federation.ActivityPub.Relay
|
alias Mobilizon.Federation.ActivityPub.Relay
|
||||||
|
alias Mobilizon.Federation.NodeInfo
|
||||||
alias Mobilizon.Instances
|
alias Mobilizon.Instances
|
||||||
alias Mobilizon.Instances.Instance
|
alias Mobilizon.Instances.{Instance, InstanceActor}
|
||||||
alias Oban.Job
|
alias Oban.Job
|
||||||
|
require Logger
|
||||||
|
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
@spec perform(Oban.Job.t()) :: :ok
|
@spec perform(Oban.Job.t()) :: :ok
|
||||||
|
@ -30,6 +32,8 @@ defmodule Mobilizon.Service.Workers.RefreshInstances do
|
||||||
{:error, :not_remote_instance}
|
{:error, :not_remote_instance}
|
||||||
end
|
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(%Instance{domain: domain}) do
|
||||||
%Actor{url: url} = Relay.get_actor()
|
%Actor{url: url} = Relay.get_actor()
|
||||||
%URI{host: host} = URI.new!(url)
|
%URI{host: host} = URI.new!(url)
|
||||||
|
@ -37,7 +41,67 @@ defmodule Mobilizon.Service.Workers.RefreshInstances do
|
||||||
if host == domain do
|
if host == domain do
|
||||||
{:error, :not_remote_instance}
|
{:error, :not_remote_instance}
|
||||||
else
|
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
|
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;
|
@apply px-3 dark:text-black;
|
||||||
}
|
}
|
||||||
.pagination-link-current {
|
.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 {
|
.pagination-ellipsis {
|
||||||
@apply text-center m-1 text-gray-300;
|
@apply text-center m-1 text-gray-300;
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<li
|
<li
|
||||||
class="setting-menu-item"
|
class="setting-menu-item"
|
||||||
:class="{
|
:class="{
|
||||||
'cursor-pointer bg-mbz-yellow-alt-500 dark:bg-mbz-purple-500': isActive,
|
'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-300 dark:hover:bg-mbz-purple-400 dark:text-white':
|
'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,
|
!isActive,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<li
|
<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
|
<router-link
|
||||||
class="cursor-pointer my-2 mx-0 py-2 px-3 font-medium block no-underline"
|
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 {
|
fragment InstanceFragment on Instance {
|
||||||
domain
|
domain
|
||||||
hasRelay
|
hasRelay
|
||||||
|
instanceName
|
||||||
|
instanceDescription
|
||||||
|
software
|
||||||
|
softwareVersion
|
||||||
relayAddress
|
relayAddress
|
||||||
followerStatus
|
followerStatus
|
||||||
followedStatus
|
followedStatus
|
||||||
|
|
|
@ -1638,5 +1638,8 @@
|
||||||
"Return to the event page": "Return to the event page",
|
"Return to the event page": "Return to the event page",
|
||||||
"Cancel participation": "Cancel participation",
|
"Cancel participation": "Cancel participation",
|
||||||
"Add a recipient": "Add a recipient",
|
"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",
|
"Return to the event page": "Retourner à la page de l'événement",
|
||||||
"Cancel participation": "Annuler la participation",
|
"Cancel participation": "Annuler la participation",
|
||||||
"Add a recipient": "Ajouter un·e destinataire",
|
"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 {
|
export interface IInstance {
|
||||||
domain: string;
|
domain: string;
|
||||||
hasRelay: boolean;
|
hasRelay: boolean;
|
||||||
|
instanceName: string | null;
|
||||||
|
instanceDescription: string | null;
|
||||||
|
software: string | null;
|
||||||
|
softwareVersion: string | null;
|
||||||
relayAddress: string | null;
|
relayAddress: string | null;
|
||||||
followerStatus: InstanceFollowStatus;
|
followerStatus: InstanceFollowStatus;
|
||||||
followedStatus: InstanceFollowStatus;
|
followedStatus: InstanceFollowStatus;
|
||||||
|
|
|
@ -2,12 +2,50 @@
|
||||||
<div v-if="instance">
|
<div v-if="instance">
|
||||||
<breadcrumbs-nav
|
<breadcrumbs-nav
|
||||||
:links="[
|
:links="[
|
||||||
{ name: RouteName.ADMIN, text: $t('Admin') },
|
{ name: RouteName.ADMIN, text: t('Admin') },
|
||||||
{ name: RouteName.INSTANCES, text: $t('Instances') },
|
{ name: RouteName.INSTANCES, text: t('Instances') },
|
||||||
{ text: instance.domain },
|
{ text: instance.domain },
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
<h1 class="text-2xl">{{ instance.domain }}</h1>
|
<section
|
||||||
|
class="flex flex-wrap md:flex-nowrap items-center justify-between gap-4"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{{ 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>
|
||||||
|
<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
|
<div
|
||||||
class="grid md:grid-cols-2 xl:grid-cols-4 gap-2 content-center text-center mt-2"
|
class="grid md:grid-cols-2 xl:grid-cols-4 gap-2 content-center text-center mt-2"
|
||||||
>
|
>
|
||||||
|
@ -21,7 +59,7 @@
|
||||||
<span class="mb-4 text-xl font-semibold block">{{
|
<span class="mb-4 text-xl font-semibold block">{{
|
||||||
instance.personCount
|
instance.personCount
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="text-sm block">{{ $t("Profiles") }}</span>
|
<span class="text-sm block">{{ t("Profiles") }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
<div class="bg-gray-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||||
|
@ -34,42 +72,52 @@
|
||||||
<span class="mb-4 text-xl font-semibold block">{{
|
<span class="mb-4 text-xl font-semibold block">{{
|
||||||
instance.groupCount
|
instance.groupCount
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="text-sm block">{{ $t("Groups") }}</span>
|
<span class="text-sm block">{{ t("Groups") }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||||
<span class="mb-4 text-xl font-semibold block">{{
|
<span class="mb-4 text-xl font-semibold block">{{
|
||||||
instance.followingsCount
|
instance.followingsCount
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="text-sm block">{{ $t("Followings") }}</span>
|
<span class="text-sm block">{{ t("Followings") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||||
<span class="mb-4 text-xl font-semibold block">{{
|
<span class="mb-4 text-xl font-semibold block">{{
|
||||||
instance.followersCount
|
instance.followersCount
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="text-sm block">{{ $t("Followers") }}</span>
|
<span class="text-sm block">{{ t("Followers") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: RouteName.REPORTS, query: { domain: instance.domain } }"
|
:to="{
|
||||||
|
name: RouteName.REPORTS,
|
||||||
|
query: { domain: instance.domain },
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<span class="mb-4 text-xl font-semibold block">{{
|
<span class="mb-4 text-xl font-semibold block">{{
|
||||||
instance.reportsCount
|
instance.reportsCount
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="text-sm block">{{ $t("Reports") }}</span>
|
<span class="text-sm block">{{ t("Reports") }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
|
||||||
<span class="mb-4 font-semibold block">{{
|
<span class="mb-4 font-semibold block">{{
|
||||||
formatBytes(instance.mediaSize)
|
formatBytes(instance.mediaSize)
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="text-sm block">{{ $t("Uploaded media size") }}</span>
|
<span class="text-sm block">{{ t("Uploaded media size") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
<div class="mt-3 grid xl:grid-cols-2 gap-4">
|
<div class="mt-3 grid xl:grid-cols-2 gap-4">
|
||||||
<div
|
<div
|
||||||
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md"
|
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md"
|
||||||
v-if="instance.hasRelay"
|
v-if="
|
||||||
|
instance.hasRelay &&
|
||||||
|
!['mastodon', 'peertube'].includes(
|
||||||
|
instance.software?.toLowerCase() ?? ''
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@click="
|
@click="
|
||||||
|
@ -80,7 +128,7 @@
|
||||||
v-if="instance.followedStatus == InstanceFollowStatus.APPROVED"
|
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"
|
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") }}
|
{{ t("Stop following instance") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="
|
@click="
|
||||||
|
@ -91,18 +139,18 @@
|
||||||
v-else-if="instance.followedStatus == InstanceFollowStatus.PENDING"
|
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"
|
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") }}
|
{{ t("Cancel follow request") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="followInstance"
|
@click="followInstance"
|
||||||
v-else
|
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"
|
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") }}
|
{{ t("Follow instance") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="md:h-48 py-16 text-center opacity-50">
|
<div v-else class="md:h-48 py-16 text-center opacity-50">
|
||||||
{{ $t("Only Mobilizon instances can be followed") }}
|
{{ t("Only instances with an application actor can be followed") }}
|
||||||
</div>
|
</div>
|
||||||
<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"
|
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"
|
||||||
|
@ -116,7 +164,7 @@
|
||||||
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
|
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"
|
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") }}
|
{{ t("Accept follow") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="
|
@click="
|
||||||
|
@ -127,13 +175,14 @@
|
||||||
v-if="instance.followerStatus != InstanceFollowStatus.NONE"
|
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"
|
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") }}
|
{{ t("Reject follow") }}
|
||||||
</button>
|
</button>
|
||||||
<p v-if="instance.followerStatus == InstanceFollowStatus.NONE">
|
<p v-if="instance.followerStatus == InstanceFollowStatus.NONE">
|
||||||
{{ $t("This instance doesn't follow yours.") }}
|
{{ t("This instance doesn't follow yours.") }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -152,6 +201,7 @@ import { InstanceFollowStatus } from "@/types/enums";
|
||||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||||
import { computed, inject } from "vue";
|
import { computed, inject } from "vue";
|
||||||
import { Notifier } from "@/plugins/notifier";
|
import { Notifier } from "@/plugins/notifier";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const props = defineProps<{ domain: string }>();
|
const props = defineProps<{ domain: string }>();
|
||||||
|
|
||||||
|
@ -164,6 +214,8 @@ const instance = computed(() => instanceResult.value?.instance);
|
||||||
|
|
||||||
const notifier = inject<Notifier>("notifier");
|
const notifier = inject<Notifier>("notifier");
|
||||||
|
|
||||||
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
const { mutate: acceptInstance, onError: onAcceptInstanceError } = useMutation(
|
const { mutate: acceptInstance, onError: onAcceptInstanceError } = useMutation(
|
||||||
ACCEPT_RELAY,
|
ACCEPT_RELAY,
|
||||||
() => ({
|
() => ({
|
||||||
|
|
|
@ -72,21 +72,65 @@
|
||||||
name: RouteName.INSTANCE,
|
name: RouteName.INSTANCE,
|
||||||
params: { domain: instance.domain },
|
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"
|
v-for="instance in instances.elements"
|
||||||
:key="instance.domain"
|
:key="instance.domain"
|
||||||
>
|
>
|
||||||
<div class="grow overflow-hidden flex items-center gap-1">
|
<div class="grow overflow-hidden flex items-center gap-1">
|
||||||
<img
|
<img
|
||||||
class="w-12"
|
class="w-12"
|
||||||
v-if="instance.hasRelay"
|
v-if="instance.software === 'Mobilizon'"
|
||||||
src="/img/logo.svg"
|
src="/img/logo.svg"
|
||||||
alt=""
|
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="">
|
<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
|
<span
|
||||||
class="text-sm"
|
class="text-sm"
|
||||||
v-if="instance.followedStatus === InstanceFollowStatus.APPROVED"
|
v-if="instance.followedStatus === InstanceFollowStatus.APPROVED"
|
||||||
|
@ -186,6 +230,7 @@ import { useRouter } from "vue-router";
|
||||||
import { useHead } from "@unhead/vue";
|
import { useHead } from "@unhead/vue";
|
||||||
import CloudQuestion from "../../../node_modules/vue-material-design-icons/CloudQuestion.vue";
|
import CloudQuestion from "../../../node_modules/vue-material-design-icons/CloudQuestion.vue";
|
||||||
import { Notifier } from "@/plugins/notifier";
|
import { Notifier } from "@/plugins/notifier";
|
||||||
|
import MastodonLogo from "@/components/Share/MastodonLogo.vue";
|
||||||
|
|
||||||
const INSTANCES_PAGE_LIMIT = 10;
|
const INSTANCES_PAGE_LIMIT = 10;
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,10 @@ defmodule Mobilizon.Federation.NodeInfoTest do
|
||||||
@instance_domain "event-federation.eu"
|
@instance_domain "event-federation.eu"
|
||||||
@nodeinfo_fixture_path "test/fixtures/nodeinfo/wp-event-federation.json"
|
@nodeinfo_fixture_path "test/fixtures/nodeinfo/wp-event-federation.json"
|
||||||
@nodeinfo_regular_fixture_path "test/fixtures/nodeinfo/regular.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
|
describe "getting application actor" do
|
||||||
test "from wordpress event federation" do
|
test "from wordpress event federation" do
|
||||||
|
@ -57,4 +61,128 @@ defmodule Mobilizon.Federation.NodeInfoTest do
|
||||||
assert nil == NodeInfo.application_actor(@instance_domain)
|
assert nil == NodeInfo.application_actor(@instance_domain)
|
||||||
end
|
end
|
||||||
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
|
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": [
|
"links": [
|
||||||
{
|
{
|
||||||
"rel": "http:\/\/nodeinfo.diaspora.software\/ns\/schema\/2.0",
|
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||||
"href": "https:\/\/event-federation.eu\/wp-json\/activitypub\/1.0\/nodeinfo"
|
"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": [
|
"links": [
|
||||||
{
|
{
|
||||||
"rel": "http:\/\/nodeinfo.diaspora.software\/ns\/schema\/2.0",
|
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||||
"href": "https:\/\/event-federation.eu\/wp-json\/activitypub\/1.0\/nodeinfo"
|
"href": "https://event-federation.eu/wp-json/activitypub/1.0/nodeinfo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rel": "https://www.w3.org/ns/activitystreams#Application",
|
"rel": "https://www.w3.org/ns/activitystreams#Application",
|
||||||
|
|
Loading…
Reference in a new issue