diff --git a/CHANGELOG.md b/CHANGELOG.md
index bab6c2580..f1711ae1f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md
index 8f855d251..82483fae7 100644
--- a/docs/development/API/admin_api.md
+++ b/docs/development/API/admin_api.md
@@ -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
diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex
index 4d0e8034d..2f338b3e2 100644
--- a/lib/pleroma/instances/instance.ex
+++ b/lib/pleroma/instances/instance.ex
@@ -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
diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
index 839ac1a8d..50aa294f0 100644
--- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -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
index 000000000..00857983f
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/instance_controller.ex
@@ -0,0 +1,63 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 1f84750b9..abb332ec2 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -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)
diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex
index 1e28384cb..4db077232 100644
--- a/lib/pleroma/workers/background_worker.ex
+++ b/lib/pleroma/workers/background_worker.ex
@@ -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
diff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs
index bacc0b19b..e49922724 100644
--- a/test/pleroma/instances/instance_test.exs
+++ b/test/pleroma/instances/instance_test.exs
@@ -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
diff --git a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
index 262f93ec4..f8cd103c6 100644
--- a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
@@ -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)
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
index 000000000..c78307fc8
--- /dev/null
+++ b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs
@@ -0,0 +1,80 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# 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