Merge branch 'instance-deletion' into 'develop'
AdminAPI: endpoint to delete all content from a remote instance See merge request pleroma/pleroma!3483
This commit is contained in:
		
						commit
						6e3df11693
					
				
					 10 changed files with 221 additions and 54 deletions
				
			
		| 
						 | 
				
			
			@ -59,6 +59,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
			
		|||
- MRF (`FollowBotPolicy`): New MRF Policy which makes a designated local Bot account attempt to follow all users in public Notes received by your instance. Users who require approving follower requests or have #nobot in their profile are excluded.
 | 
			
		||||
- Return OAuth token `id` (primary key) in POST `/oauth/token`.
 | 
			
		||||
- AdminAPI: return `created_at` date with users.
 | 
			
		||||
- AdminAPI: add DELETE `/api/v1/pleroma/admin/instances/:instance` to delete all content from a remote instance.
 | 
			
		||||
- `AnalyzeMetadata` upload filter for extracting image/video attachment dimensions and generating blurhashes for images. Blurhashes for videos are not generated at this time.
 | 
			
		||||
- Attachment dimensions and blurhashes are federated when available.
 | 
			
		||||
- Mastodon API: support `poll` notification.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -319,6 +319,22 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
 | 
			
		|||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## `DELETE /api/v1/pleroma/admin/instances/:instance`
 | 
			
		||||
 | 
			
		||||
### Delete all users and activities from a remote instance
 | 
			
		||||
 | 
			
		||||
Note: this will trigger a job to remove instance content in the background.
 | 
			
		||||
It may take some time.
 | 
			
		||||
 | 
			
		||||
- Params:
 | 
			
		||||
  - `instance`: remote instance host
 | 
			
		||||
- Response:
 | 
			
		||||
  - The `instance` name as a string
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
"lain.com"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## `GET /api/v1/pleroma/admin/statuses`
 | 
			
		||||
 | 
			
		||||
### Retrives all latest statuses
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,8 @@ defmodule Pleroma.Instances.Instance do
 | 
			
		|||
  alias Pleroma.Instances
 | 
			
		||||
  alias Pleroma.Instances.Instance
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Workers.BackgroundWorker
 | 
			
		||||
 | 
			
		||||
  use Ecto.Schema
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -195,4 +197,24 @@ defp scrape_favicon(%URI{} = instance_uri) do
 | 
			
		|||
        nil
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Deletes all users from an instance in a background task, thus also deleting
 | 
			
		||||
  all of those users' activities and notifications.
 | 
			
		||||
  """
 | 
			
		||||
  def delete_users_and_activities(host) when is_binary(host) do
 | 
			
		||||
    BackgroundWorker.enqueue("delete_instance", %{"host" => host})
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def perform(:delete_instance, host) when is_binary(host) do
 | 
			
		||||
    User.Query.build(%{nickname: "@#{host}"})
 | 
			
		||||
    |> Repo.chunk_stream(100, :batches)
 | 
			
		||||
    |> Stream.each(fn users ->
 | 
			
		||||
      users
 | 
			
		||||
      |> Enum.each(fn user ->
 | 
			
		||||
        User.perform(:delete, user)
 | 
			
		||||
      end)
 | 
			
		||||
    end)
 | 
			
		||||
    |> Stream.run()
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,7 +49,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
 | 
			
		|||
  plug(
 | 
			
		||||
    OAuthScopesPlug,
 | 
			
		||||
    %{scopes: ["admin:read:statuses"]}
 | 
			
		||||
    when action in [:list_user_statuses, :list_instance_statuses]
 | 
			
		||||
    when action in [:list_user_statuses]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  plug(
 | 
			
		||||
| 
						 | 
				
			
			@ -81,24 +81,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
 | 
			
		|||
 | 
			
		||||
  action_fallback(AdminAPI.FallbackController)
 | 
			
		||||
 | 
			
		||||
  def list_instance_statuses(conn, %{"instance" => instance} = params) do
 | 
			
		||||
    with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
 | 
			
		||||
    {page, page_size} = page_params(params)
 | 
			
		||||
 | 
			
		||||
    result =
 | 
			
		||||
      ActivityPub.fetch_statuses(nil, %{
 | 
			
		||||
        instance: instance,
 | 
			
		||||
        limit: page_size,
 | 
			
		||||
        offset: (page - 1) * page_size,
 | 
			
		||||
        exclude_reblogs: not with_reblogs,
 | 
			
		||||
        total: true
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    conn
 | 
			
		||||
    |> put_view(AdminAPI.StatusView)
 | 
			
		||||
    |> render("index.json", %{total: result[:total], activities: result[:items], as: :activity})
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do
 | 
			
		||||
    with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
 | 
			
		||||
    godmode = params["godmode"] == "true" || params["godmode"] == true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										63
									
								
								lib/pleroma/web/admin_api/controllers/instance_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								lib/pleroma/web/admin_api/controllers/instance_controller.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Web.AdminAPI.InstanceController do
 | 
			
		||||
  use Pleroma.Web, :controller
 | 
			
		||||
 | 
			
		||||
  import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 3]
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.Instances.Instance
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.ActivityPub
 | 
			
		||||
  alias Pleroma.Web.AdminAPI
 | 
			
		||||
  alias Pleroma.Web.Plugs.OAuthScopesPlug
 | 
			
		||||
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  @default_page_size 50
 | 
			
		||||
 | 
			
		||||
  plug(
 | 
			
		||||
    OAuthScopesPlug,
 | 
			
		||||
    %{scopes: ["admin:read:statuses"]}
 | 
			
		||||
    when action in [:list_statuses]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  plug(
 | 
			
		||||
    OAuthScopesPlug,
 | 
			
		||||
    %{scopes: ["admin:write:accounts", "admin:write:statuses"]}
 | 
			
		||||
    when action in [:delete]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  action_fallback(AdminAPI.FallbackController)
 | 
			
		||||
 | 
			
		||||
  def list_statuses(conn, %{"instance" => instance} = params) do
 | 
			
		||||
    with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
 | 
			
		||||
    {page, page_size} = page_params(params)
 | 
			
		||||
 | 
			
		||||
    result =
 | 
			
		||||
      ActivityPub.fetch_statuses(nil, %{
 | 
			
		||||
        instance: instance,
 | 
			
		||||
        limit: page_size,
 | 
			
		||||
        offset: (page - 1) * page_size,
 | 
			
		||||
        exclude_reblogs: not with_reblogs,
 | 
			
		||||
        total: true
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    conn
 | 
			
		||||
    |> put_view(AdminAPI.StatusView)
 | 
			
		||||
    |> render("index.json", %{total: result[:total], activities: result[:items], as: :activity})
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def delete(conn, %{"instance" => instance}) do
 | 
			
		||||
    with {:ok, _job} <- Instance.delete_users_and_activities(instance) do
 | 
			
		||||
      json(conn, instance)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp page_params(params) do
 | 
			
		||||
    {
 | 
			
		||||
      fetch_integer_param(params, "page", 1),
 | 
			
		||||
      fetch_integer_param(params, "page_size", @default_page_size)
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -211,7 +211,8 @@ defmodule Pleroma.Web.Router do
 | 
			
		|||
    get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
 | 
			
		||||
    get("/users/:nickname/chats", AdminAPIController, :list_user_chats)
 | 
			
		||||
 | 
			
		||||
    get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
 | 
			
		||||
    get("/instances/:instance/statuses", InstanceController, :list_statuses)
 | 
			
		||||
    delete("/instances/:instance", InstanceController, :delete)
 | 
			
		||||
 | 
			
		||||
    get("/instance_document/:name", InstanceDocumentController, :show)
 | 
			
		||||
    patch("/instance_document/:name", InstanceDocumentController, :update)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Workers.BackgroundWorker do
 | 
			
		||||
  alias Pleroma.Instances.Instance
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
 | 
			
		||||
  use Pleroma.Workers.WorkerHelper, queue: "background"
 | 
			
		||||
| 
						 | 
				
			
			@ -38,4 +39,8 @@ def perform(%Job{
 | 
			
		|||
 | 
			
		||||
    Pleroma.FollowingRelationship.move_following(origin, target)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do
 | 
			
		||||
    Instance.perform(:delete_instance, host)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,8 @@ defmodule Pleroma.Instances.InstanceTest do
 | 
			
		|||
  alias Pleroma.Instances
 | 
			
		||||
  alias Pleroma.Instances.Instance
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
  alias Pleroma.Tests.ObanHelpers
 | 
			
		||||
  alias Pleroma.Web.CommonAPI
 | 
			
		||||
 | 
			
		||||
  use Pleroma.DataCase
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -158,4 +160,33 @@ test "Doesn't scrapes unreachable instances" do
 | 
			
		|||
               "Instance.scrape_favicon(\"#{url}\") ignored unreachable host"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  test "delete_users_and_activities/1 deletes remote instance users and activities" do
 | 
			
		||||
    [mario, luigi, _peach, wario] =
 | 
			
		||||
      users = [
 | 
			
		||||
        insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario"),
 | 
			
		||||
        insert(:user, nickname: "luigi@mushroom.kingdom", name: "Luigi"),
 | 
			
		||||
        insert(:user, nickname: "peach@mushroom.kingdom", name: "Peach"),
 | 
			
		||||
        insert(:user, nickname: "wario@greedville.biz", name: "Wario")
 | 
			
		||||
      ]
 | 
			
		||||
 | 
			
		||||
    {:ok, post1} = CommonAPI.post(mario, %{status: "letsa go!"})
 | 
			
		||||
    {:ok, post2} = CommonAPI.post(luigi, %{status: "itsa me... luigi"})
 | 
			
		||||
    {:ok, post3} = CommonAPI.post(wario, %{status: "WHA-HA-HA!"})
 | 
			
		||||
 | 
			
		||||
    {:ok, job} = Instance.delete_users_and_activities("mushroom.kingdom")
 | 
			
		||||
    :ok = ObanHelpers.perform(job)
 | 
			
		||||
 | 
			
		||||
    [mario, luigi, peach, wario] = Repo.reload(users)
 | 
			
		||||
 | 
			
		||||
    refute mario.is_active
 | 
			
		||||
    refute luigi.is_active
 | 
			
		||||
    refute peach.is_active
 | 
			
		||||
    refute peach.name == "Peach"
 | 
			
		||||
 | 
			
		||||
    assert wario.is_active
 | 
			
		||||
    assert wario.name == "Wario"
 | 
			
		||||
 | 
			
		||||
    assert [nil, nil, %{}] = Repo.reload([post1, post2, post3])
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -798,40 +798,6 @@ test "sets password_reset_pending to true", %{conn: conn} do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "instances" do
 | 
			
		||||
    test "GET /instances/:instance/statuses", %{conn: conn} do
 | 
			
		||||
      user = insert(:user, local: false, ap_id: "https://archae.me/users/archaeme")
 | 
			
		||||
      user2 = insert(:user, local: false, ap_id: "https://test.com/users/test")
 | 
			
		||||
      insert_pair(:note_activity, user: user)
 | 
			
		||||
      activity = insert(:note_activity, user: user2)
 | 
			
		||||
 | 
			
		||||
      %{"total" => 2, "activities" => activities} =
 | 
			
		||||
        conn |> get("/api/pleroma/admin/instances/archae.me/statuses") |> json_response(200)
 | 
			
		||||
 | 
			
		||||
      assert length(activities) == 2
 | 
			
		||||
 | 
			
		||||
      %{"total" => 1, "activities" => [_]} =
 | 
			
		||||
        conn |> get("/api/pleroma/admin/instances/test.com/statuses") |> json_response(200)
 | 
			
		||||
 | 
			
		||||
      %{"total" => 0, "activities" => []} =
 | 
			
		||||
        conn |> get("/api/pleroma/admin/instances/nonexistent.com/statuses") |> json_response(200)
 | 
			
		||||
 | 
			
		||||
      CommonAPI.repeat(activity.id, user)
 | 
			
		||||
 | 
			
		||||
      %{"total" => 2, "activities" => activities} =
 | 
			
		||||
        conn |> get("/api/pleroma/admin/instances/archae.me/statuses") |> json_response(200)
 | 
			
		||||
 | 
			
		||||
      assert length(activities) == 2
 | 
			
		||||
 | 
			
		||||
      %{"total" => 3, "activities" => activities} =
 | 
			
		||||
        conn
 | 
			
		||||
        |> get("/api/pleroma/admin/instances/archae.me/statuses?with_reblogs=true")
 | 
			
		||||
        |> json_response(200)
 | 
			
		||||
 | 
			
		||||
      assert length(activities) == 3
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "PATCH /confirm_email" do
 | 
			
		||||
    test "it confirms emails of two users", %{conn: conn, admin: admin} do
 | 
			
		||||
      [first_user, second_user] = insert_pair(:user, is_confirmed: false)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,80 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do
 | 
			
		||||
  use Pleroma.Web.ConnCase
 | 
			
		||||
  use Oban.Testing, repo: Pleroma.Repo
 | 
			
		||||
 | 
			
		||||
  import Pleroma.Factory
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
  alias Pleroma.Tests.ObanHelpers
 | 
			
		||||
  alias Pleroma.Web.CommonAPI
 | 
			
		||||
 | 
			
		||||
  setup_all do
 | 
			
		||||
    Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
 | 
			
		||||
 | 
			
		||||
    :ok
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  setup do
 | 
			
		||||
    admin = insert(:user, is_admin: true)
 | 
			
		||||
    token = insert(:oauth_admin_token, user: admin)
 | 
			
		||||
 | 
			
		||||
    conn =
 | 
			
		||||
      build_conn()
 | 
			
		||||
      |> assign(:user, admin)
 | 
			
		||||
      |> assign(:token, token)
 | 
			
		||||
 | 
			
		||||
    {:ok, %{admin: admin, token: token, conn: conn}}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  test "GET /instances/:instance/statuses", %{conn: conn} do
 | 
			
		||||
    user = insert(:user, local: false, ap_id: "https://archae.me/users/archaeme")
 | 
			
		||||
    user2 = insert(:user, local: false, ap_id: "https://test.com/users/test")
 | 
			
		||||
    insert_pair(:note_activity, user: user)
 | 
			
		||||
    activity = insert(:note_activity, user: user2)
 | 
			
		||||
 | 
			
		||||
    %{"total" => 2, "activities" => activities} =
 | 
			
		||||
      conn |> get("/api/pleroma/admin/instances/archae.me/statuses") |> json_response(200)
 | 
			
		||||
 | 
			
		||||
    assert length(activities) == 2
 | 
			
		||||
 | 
			
		||||
    %{"total" => 1, "activities" => [_]} =
 | 
			
		||||
      conn |> get("/api/pleroma/admin/instances/test.com/statuses") |> json_response(200)
 | 
			
		||||
 | 
			
		||||
    %{"total" => 0, "activities" => []} =
 | 
			
		||||
      conn |> get("/api/pleroma/admin/instances/nonexistent.com/statuses") |> json_response(200)
 | 
			
		||||
 | 
			
		||||
    CommonAPI.repeat(activity.id, user)
 | 
			
		||||
 | 
			
		||||
    %{"total" => 2, "activities" => activities} =
 | 
			
		||||
      conn |> get("/api/pleroma/admin/instances/archae.me/statuses") |> json_response(200)
 | 
			
		||||
 | 
			
		||||
    assert length(activities) == 2
 | 
			
		||||
 | 
			
		||||
    %{"total" => 3, "activities" => activities} =
 | 
			
		||||
      conn
 | 
			
		||||
      |> get("/api/pleroma/admin/instances/archae.me/statuses?with_reblogs=true")
 | 
			
		||||
      |> json_response(200)
 | 
			
		||||
 | 
			
		||||
    assert length(activities) == 3
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  test "DELETE /instances/:instance", %{conn: conn} do
 | 
			
		||||
    user = insert(:user, nickname: "lain@lain.com")
 | 
			
		||||
    post = insert(:note_activity, user: user)
 | 
			
		||||
 | 
			
		||||
    response =
 | 
			
		||||
      conn
 | 
			
		||||
      |> delete("/api/pleroma/admin/instances/lain.com")
 | 
			
		||||
      |> json_response(200)
 | 
			
		||||
 | 
			
		||||
    [:ok] = ObanHelpers.perform_all()
 | 
			
		||||
 | 
			
		||||
    assert response == "lain.com"
 | 
			
		||||
    refute Repo.reload(user).is_active
 | 
			
		||||
    refute Repo.reload(post)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
		Loading…
	
		Reference in a new issue