Introduce instances admin page
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
|
||||
alias Mobilizon.{Actors, Admin, Config, Events}
|
||||
alias Mobilizon.{Actors, Admin, Config, Events, Instances}
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
alias Mobilizon.Admin.{ActionLog, Setting}
|
||||
alias Mobilizon.Cldr.Language
|
||||
@@ -329,6 +329,79 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
||||
{:error, :unauthenticated}
|
||||
end
|
||||
|
||||
def get_instances(
|
||||
_parent,
|
||||
args,
|
||||
%{
|
||||
context: %{current_user: %User{role: role}}
|
||||
}
|
||||
)
|
||||
when is_admin(role) do
|
||||
{:ok,
|
||||
Instances.instances(
|
||||
args
|
||||
|> Keyword.new()
|
||||
|> Keyword.take([
|
||||
:page,
|
||||
:limit,
|
||||
:order_by,
|
||||
:direction,
|
||||
:filter_domain,
|
||||
:filter_follow_status,
|
||||
:filter_suspend_status
|
||||
])
|
||||
)}
|
||||
end
|
||||
|
||||
def get_instances(_parent, _args, %{context: %{current_user: %User{}}}) do
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
def get_instances(_parent, _args, _resolution) do
|
||||
{:error, :unauthenticated}
|
||||
end
|
||||
|
||||
def get_instance(_parent, %{domain: domain}, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
when is_admin(role) do
|
||||
has_relay = Actors.has_relay?(domain)
|
||||
remote_relay = Actors.get_actor_by_name("relay@#{domain}")
|
||||
local_relay = Relay.get_actor()
|
||||
|
||||
result = %{
|
||||
has_relay: has_relay,
|
||||
follower_status: follow_status(remote_relay, local_relay),
|
||||
followed_status: follow_status(local_relay, remote_relay)
|
||||
}
|
||||
|
||||
{:ok, Map.merge(Instances.instance(domain), result)}
|
||||
end
|
||||
|
||||
def get_instance(_parent, _args, %{context: %{current_user: %User{}}}) do
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
def get_instance(_parent, _args, _resolution) do
|
||||
{:error, :unauthenticated}
|
||||
end
|
||||
|
||||
def create_instance(
|
||||
parent,
|
||||
%{domain: domain} = args,
|
||||
%{context: %{current_user: %User{role: role}}} = resolution
|
||||
)
|
||||
when is_admin(role) do
|
||||
case Relay.follow(domain) do
|
||||
{:ok, _activity, _follow} ->
|
||||
Instances.refresh()
|
||||
get_instance(parent, args, resolution)
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_relay(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Follower.t()} | {:error, any()}
|
||||
def create_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}})
|
||||
@@ -425,4 +498,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@spec follow_status(Actor.t() | nil, Actor.t() | nil) :: :approved | :pending | :none
|
||||
defp follow_status(follower, followed) when follower != nil and followed != nil do
|
||||
case Actors.check_follow(follower, followed) do
|
||||
%Follower{approved: true} -> :approved
|
||||
%Follower{approved: false} -> :pending
|
||||
_ -> :none
|
||||
end
|
||||
end
|
||||
|
||||
defp follow_status(_, _), do: :none
|
||||
end
|
||||
|
||||
@@ -153,6 +153,80 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||
value(:custom, as: "CUSTOM", description: "Custom privacy policy text")
|
||||
end
|
||||
|
||||
enum :instance_follow_status do
|
||||
value(:approved, description: "The instance follow was approved")
|
||||
value(:pending, description: "The instance follow is still pending")
|
||||
value(:none, description: "There's no instance follow etablished")
|
||||
end
|
||||
|
||||
enum :instances_sort_fields do
|
||||
value(:event_count)
|
||||
value(:person_count)
|
||||
value(:group_count)
|
||||
value(:followers_count)
|
||||
value(:followings_count)
|
||||
value(:reports_count)
|
||||
value(:media_size)
|
||||
end
|
||||
|
||||
enum :instance_filter_follow_status do
|
||||
value(:all)
|
||||
value(:following)
|
||||
value(:followed)
|
||||
end
|
||||
|
||||
enum :instance_filter_suspend_status do
|
||||
value(:all)
|
||||
value(:suspended)
|
||||
end
|
||||
|
||||
@desc """
|
||||
An instance representation
|
||||
"""
|
||||
object :instance do
|
||||
field(:domain, :id, description: "The domain name of the instance")
|
||||
field(:has_relay, :boolean, description: "Whether this instance has a Mobilizon relay actor")
|
||||
field(:follower_status, :instance_follow_status, description: "Do we follow this instance")
|
||||
field(:followed_status, :instance_follow_status, description: "Does this instance follow us?")
|
||||
|
||||
field(:event_count, :integer, description: "The number of events on this instance we know of")
|
||||
|
||||
field(:person_count, :integer,
|
||||
description: "The number of profiles on this instance we know of"
|
||||
)
|
||||
|
||||
field(:group_count, :integer, description: "The number of grouo on this instance we know of")
|
||||
|
||||
field(:followers_count, :integer,
|
||||
description: "The number of their profiles who follow our groups"
|
||||
)
|
||||
|
||||
field(:followings_count, :integer,
|
||||
description: "The number of our profiles who follow their groups"
|
||||
)
|
||||
|
||||
field(:reports_count, :integer,
|
||||
description: "The number of reports made against profiles from this instance"
|
||||
)
|
||||
|
||||
field(:media_size, :integer,
|
||||
description: "The size of all the media files sent by actors from this instance"
|
||||
)
|
||||
|
||||
field(:has_relay, :boolean,
|
||||
description:
|
||||
"Whether this instance has a relay, meaning that it's a Mobilizon instance that we can follow"
|
||||
)
|
||||
end
|
||||
|
||||
@desc """
|
||||
A paginated list of instances
|
||||
"""
|
||||
object :paginated_instance_list do
|
||||
field(:elements, list_of(:instance), description: "A list of instances")
|
||||
field(:total, :integer, description: "The total number of instances in the list")
|
||||
end
|
||||
|
||||
object :admin_queries do
|
||||
@desc "Get the list of action logs"
|
||||
field :action_logs, type: :paginated_action_log_list do
|
||||
@@ -226,9 +300,59 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||
arg(:direction, :string, default_value: :desc, description: "The sorting direction")
|
||||
resolve(&Admin.list_relay_followings/3)
|
||||
end
|
||||
|
||||
@desc """
|
||||
List instances
|
||||
"""
|
||||
field :instances, :paginated_instance_list do
|
||||
arg(:page, :integer,
|
||||
default_value: 1,
|
||||
description: "The page in the paginated relay followings list"
|
||||
)
|
||||
|
||||
arg(:limit, :integer,
|
||||
default_value: 10,
|
||||
description: "The limit of relay followings per page"
|
||||
)
|
||||
|
||||
arg(:order_by, :instances_sort_fields,
|
||||
default_value: :event_count,
|
||||
description: "The field to order by the list"
|
||||
)
|
||||
|
||||
arg(:filter_domain, :string, default_value: nil, description: "Filter by domain")
|
||||
|
||||
arg(:filter_follow_status, :instance_filter_follow_status,
|
||||
default_value: :all,
|
||||
description: "Whether or not to filter instances by the follow status"
|
||||
)
|
||||
|
||||
arg(:filter_suspend_status, :instance_filter_suspend_status,
|
||||
default_value: :all,
|
||||
description: "Whether or not to filter instances by the suspended status"
|
||||
)
|
||||
|
||||
arg(:direction, :string, default_value: :desc, description: "The sorting direction")
|
||||
resolve(&Admin.get_instances/3)
|
||||
end
|
||||
|
||||
@desc """
|
||||
Get an instance's details
|
||||
"""
|
||||
field :instance, :instance do
|
||||
arg(:domain, non_null(:id), description: "The instance domain")
|
||||
resolve(&Admin.get_instance/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :admin_mutations do
|
||||
@desc "Add an instance subscription"
|
||||
field :add_instance, type: :instance do
|
||||
arg(:domain, non_null(:string), description: "The instance domain to add")
|
||||
|
||||
resolve(&Admin.create_instance/3)
|
||||
end
|
||||
|
||||
@desc "Add a relay subscription"
|
||||
field :add_relay, type: :follower do
|
||||
arg(:address, non_null(:string), description: "The relay hostname to add")
|
||||
|
||||
@@ -1256,6 +1256,16 @@ defmodule Mobilizon.Actors do
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec has_relay?(String.t()) :: boolean()
|
||||
def has_relay?(domain) do
|
||||
Actor
|
||||
|> where(
|
||||
[a],
|
||||
a.preferred_username == "relay" and a.domain == ^domain and a.type == :Application
|
||||
)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
@spec delete_files_if_media_changed(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp delete_files_if_media_changed(%Ecto.Changeset{changes: changes, data: data} = changeset) do
|
||||
Enum.each([:avatar, :banner], fn key ->
|
||||
|
||||
19
lib/mobilizon/instances/instance.ex
Normal file
19
lib/mobilizon/instances/instance.ex
Normal file
@@ -0,0 +1,19 @@
|
||||
defmodule Mobilizon.Instances.Instance do
|
||||
@moduledoc """
|
||||
An instance representation
|
||||
|
||||
Using a MATERIALIZED VIEW underneath
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
@primary_key {:domain, :string, []}
|
||||
schema "instances" do
|
||||
field(:event_count, :integer)
|
||||
field(:person_count, :integer)
|
||||
field(:group_count, :integer)
|
||||
field(:followers_count, :integer)
|
||||
field(:followings_count, :integer)
|
||||
field(:reports_count, :integer)
|
||||
field(:media_size, :integer)
|
||||
end
|
||||
end
|
||||
115
lib/mobilizon/instances/instances.ex
Normal file
115
lib/mobilizon/instances/instances.ex
Normal file
@@ -0,0 +1,115 @@
|
||||
defmodule Mobilizon.Instances do
|
||||
@moduledoc """
|
||||
The instances context
|
||||
"""
|
||||
alias Ecto.Adapters.SQL
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
alias Mobilizon.Instances.Instance
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
import Ecto.Query
|
||||
|
||||
@is_null_fragment "CASE WHEN ? IS NULL THEN FALSE ELSE TRUE END"
|
||||
|
||||
@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)
|
||||
filter_domain = Keyword.get(options, :filter_domain)
|
||||
# suspend_status = Keyword.get(options, :filter_suspend_status)
|
||||
follow_status = Keyword.get(options, :filter_follow_status)
|
||||
|
||||
order_by_options = Keyword.new([{direction, order_by}])
|
||||
|
||||
subquery =
|
||||
Actor
|
||||
|> where(
|
||||
[a],
|
||||
a.preferred_username == "relay" and a.type == :Application and not is_nil(a.domain)
|
||||
)
|
||||
|> join(:left, [a], f1 in Follower, on: f1.target_actor_id == a.id)
|
||||
|> join(:left, [a], f2 in Follower, on: f2.actor_id == a.id)
|
||||
|> select([a, f1, f2], %{
|
||||
domain: a.domain,
|
||||
has_relay: fragment(@is_null_fragment, a.id),
|
||||
following: fragment(@is_null_fragment, f2.id),
|
||||
following_approved: f2.approved,
|
||||
follower: fragment(@is_null_fragment, f1.id),
|
||||
follower_approved: f1.approved
|
||||
})
|
||||
|
||||
query =
|
||||
Instance
|
||||
|> join(:left, [i], s in subquery(subquery), on: i.domain == s.domain)
|
||||
|> select([i, s], {i, s})
|
||||
|> order_by(^order_by_options)
|
||||
|
||||
query =
|
||||
if is_nil(filter_domain) or filter_domain == "" do
|
||||
query
|
||||
else
|
||||
where(query, [i], like(i.domain, ^"%#{filter_domain}%"))
|
||||
end
|
||||
|
||||
query =
|
||||
case follow_status do
|
||||
:following -> where(query, [i, s], s.following == true)
|
||||
:followed -> where(query, [i, s], s.follower == true)
|
||||
:all -> query
|
||||
end
|
||||
|
||||
%Page{elements: elements} = paged_instances = Page.build_page(query, page, limit, :domain)
|
||||
|
||||
%Page{
|
||||
paged_instances
|
||||
| elements: Enum.map(elements, &convert_instance_meta/1)
|
||||
}
|
||||
end
|
||||
|
||||
@spec instance(String.t()) :: Instance.t()
|
||||
def instance(domain) do
|
||||
Instance
|
||||
|> where(domain: ^domain)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@spec all_domains :: list(Instance.t())
|
||||
def all_domains do
|
||||
Instance
|
||||
|> distinct(true)
|
||||
|> select([:domain])
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec refresh :: %{
|
||||
:rows => nil | [[term()] | binary()],
|
||||
:num_rows => non_neg_integer(),
|
||||
optional(atom()) => any()
|
||||
}
|
||||
def refresh do
|
||||
SQL.query!(Repo, "REFRESH MATERIALIZED VIEW instances")
|
||||
end
|
||||
|
||||
defp convert_instance_meta(
|
||||
{instance,
|
||||
%{
|
||||
domain: _domain,
|
||||
follower: follower,
|
||||
follower_approved: follower_approved,
|
||||
following: following,
|
||||
following_approved: following_approved,
|
||||
has_relay: has_relay
|
||||
}}
|
||||
) 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)
|
||||
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
|
||||
end
|
||||
31
lib/service/workers/refresh_instances.ex
Normal file
31
lib/service/workers/refresh_instances.ex
Normal file
@@ -0,0 +1,31 @@
|
||||
defmodule Mobilizon.Service.Workers.RefreshInstances do
|
||||
@moduledoc """
|
||||
Worker to refresh the instances materialized view and the relay actors
|
||||
"""
|
||||
|
||||
use Oban.Worker, unique: [period: :infinity, keys: [:event_uuid, :action]]
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Instances
|
||||
alias Mobilizon.Instances.Instance
|
||||
alias Oban.Job
|
||||
|
||||
@impl Oban.Worker
|
||||
@spec perform(Oban.Job.t()) :: :ok
|
||||
def perform(%Job{}) do
|
||||
Instances.refresh()
|
||||
|
||||
Instances.all_domains()
|
||||
|> Enum.each(&refresh_instance_actor/1)
|
||||
end
|
||||
|
||||
@spec refresh_instance_actor(Instance.t()) ::
|
||||
{:ok, Mobilizon.Actors.Actor.t()}
|
||||
| {:error,
|
||||
Mobilizon.Federation.ActivityPub.Actor.make_actor_errors()
|
||||
| Mobilizon.Federation.WebFinger.finger_errors()}
|
||||
|
||||
defp refresh_instance_actor(%Instance{domain: domain}) do
|
||||
ActivityPubActor.find_or_make_actor_from_nickname("relay@#{domain}")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user