Merge remote-tracking branch 'pleroma/develop' into instance-deletion
authorAlex Gleason <alex@alexgleason.me>
Sat, 17 Jul 2021 20:03:43 +0000 (15:03 -0500)
committerAlex Gleason <alex@alexgleason.me>
Sat, 17 Jul 2021 20:03:43 +0000 (15:03 -0500)
docs/development/API/admin_api.md
lib/pleroma/instances/instance.ex
lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
lib/pleroma/web/admin_api/controllers/instance_controller.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
lib/pleroma/workers/background_worker.ex
test/pleroma/instances/instance_test.exs
test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
test/pleroma/web/admin_api/controllers/instance_controller_test.exs [new file with mode: 0644]

index 8f855d251a5ac2b9c2cb945c0534b8699d3f4605..82483fae71deaf9f82c8ff6afb3ef2f913183083 100644 (file)
@@ -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
index 4d0e8034d61400f623169a0d47c3bc21f236e1a6..2f338b3e2bae30518508b31f49f022dcccb0d436 100644 (file)
@@ -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 @@ defmodule Pleroma.Instances.Instance 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
index 839ac1a8d9185381f56cc84ff14122da63cfb899..50aa294f0ff0aed48ac6089323a47a4a6e2353b7 100644 (file)
@@ -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
diff --git a/lib/pleroma/web/admin_api/controllers/instance_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_controller.ex
new file mode 100644 (file)
index 0000000..0085798
--- /dev/null
@@ -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
index efca7078a178344c091e17ac61f748bee68e65ba..15124362edb9fefd0eccd59b9ef0dd9b592c9185 100644 (file)
@@ -213,7 +213,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)
index 1e28384cb188d7fe626e1cf9d9b7dc7cab7601c2..4db077232a850eac077dd98793496f217e2c8f36 100644 (file)
@@ -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 @@ defmodule Pleroma.Workers.BackgroundWorker do
 
     Pleroma.FollowingRelationship.move_following(origin, target)
   end
+
+  def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do
+    Instance.perform(:delete_instance, host)
+  end
 end
index bacc0b19be1517756ee84f0acda65628a128756d..e49922724e12fc9890c2221e9dfc2f7f7ac043d2 100644 (file)
@@ -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 @@ defmodule Pleroma.Instances.InstanceTest 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
index 8cd9f939b20312ca64553e47a1b39c78b9ba63c3..a9a0c800b873931b47f3eb02bc0cb370d44cc230 100644 (file)
@@ -800,40 +800,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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)
diff --git a/test/pleroma/web/admin_api/controllers/instance_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs
new file mode 100644 (file)
index 0000000..c78307f
--- /dev/null
@@ -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