Change imports to generate an Oban job per each task

This commit is contained in:
Mark Felder 2024-08-22 12:49:32 -04:00
parent 649e51b581
commit a9aa810d3d
6 changed files with 170 additions and 109 deletions

View file

@ -1 +1 @@
Imports of blocks, mutes, and following would retry until Oban runs out of attempts due to incorrect return value being considered an error. Imports of blocks, mutes, and follows would retry repeatedly due to incorrect error handling and all work executed in a single job

View file

@ -5,6 +5,7 @@
defmodule Pleroma.User.Import do defmodule Pleroma.User.Import do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.BackgroundWorker alias Pleroma.Workers.BackgroundWorker
@ -12,80 +13,103 @@ defmodule Pleroma.User.Import do
require Logger require Logger
@spec perform(atom(), User.t(), list()) :: :ok | list() | {:error, any()} @spec perform(atom(), User.t(), list()) :: :ok | list() | {:error, any()}
def perform(:mutes_import, %User{} = user, [_ | _] = identifiers) do def perform(:mute_import, %User{} = user, actor) do
Enum.map( with {:ok, %User{} = muted_user} <- User.get_or_fetch(actor),
identifiers, {_, false} <- {:existing_mute, User.mutes_user?(user, muted_user)},
fn identifier -> {:ok, _} <- User.mute(user, muted_user),
with {:ok, %User{} = muted_user} <- User.get_or_fetch(identifier), # User.mute/2 returns a FollowingRelationship not a %User{} like we get
{:ok, _} <- User.mute(user, muted_user) do # from CommonAPI.block/2 or CommonAPI.follow/2, so we fetch again to
{:ok, muted_user} # return the target actor for consistency
else {:ok, muted_user} <- User.get_or_fetch(actor) do
error -> handle_error(:mutes_import, identifier, error) {:ok, muted_user}
end else
end {:existing_mute, true} -> :ok
) error -> handle_error(:mutes_import, actor, error)
end
end end
def perform(:blocks_import, %User{} = blocker, [_ | _] = identifiers) do def perform(:block_import, %User{} = user, actor) do
Enum.map( with {:ok, %User{} = blocked} <- User.get_or_fetch(actor),
identifiers, {_, false} <- {:existing_block, User.blocks_user?(user, blocked)},
fn identifier -> {:ok, _block} <- CommonAPI.block(blocked, user) do
with {:ok, %User{} = blocked} <- User.get_or_fetch(identifier), {:ok, blocked}
{:ok, _block} <- CommonAPI.block(blocked, blocker) do else
{:ok, blocked} {:existing_block, true} -> :ok
else error -> handle_error(:blocks_import, actor, error)
error -> handle_error(:blocks_import, identifier, error) end
end
end
)
end end
def perform(:follow_import, %User{} = follower, [_ | _] = identifiers) do def perform(:follow_import, %User{} = user, actor) do
Enum.map( with {:ok, %User{} = followed} <- User.get_or_fetch(actor),
identifiers, {_, false} <- {:existing_follow, User.following?(user, followed)},
fn identifier -> {:ok, user, followed} <- User.maybe_direct_follow(user, followed),
with {:ok, %User{} = followed} <- User.get_or_fetch(identifier), {:ok, _, _, _} <- CommonAPI.follow(followed, user) do
{:ok, follower, followed} <- User.maybe_direct_follow(follower, followed), {:ok, followed}
{:ok, _, _, _} <- CommonAPI.follow(followed, follower) do else
{:ok, followed} {:existing_follow, true} -> :ok
else error -> handle_error(:follow_import, actor, error)
error -> handle_error(:follow_import, identifier, error) end
end
end
)
end end
def perform(_, _, _), do: :ok
defp handle_error(op, user_id, error) do defp handle_error(op, user_id, error) do
Logger.debug("#{op} failed for #{user_id} with: #{inspect(error)}") Logger.debug("#{op} failed for #{user_id} with: #{inspect(error)}")
error error
end end
def blocks_import(%User{} = blocker, [_ | _] = identifiers) do def blocks_import(%User{} = user, [_ | _] = actors) do
BackgroundWorker.new(%{ jobs =
"op" => "blocks_import", Repo.checkout(fn ->
"user_id" => blocker.id, Enum.reduce(actors, [], fn actor, acc ->
"identifiers" => identifiers {:ok, job} =
}) BackgroundWorker.new(%{
|> Oban.insert() "op" => "block_import",
"user_id" => user.id,
"actor" => actor
})
|> Oban.insert()
acc ++ [job]
end)
end)
{:ok, jobs}
end end
def follow_import(%User{} = follower, [_ | _] = identifiers) do def follows_import(%User{} = user, [_ | _] = actors) do
BackgroundWorker.new(%{ jobs =
"op" => "follow_import", Repo.checkout(fn ->
"user_id" => follower.id, Enum.reduce(actors, [], fn actor, acc ->
"identifiers" => identifiers {:ok, job} =
}) BackgroundWorker.new(%{
|> Oban.insert() "op" => "follow_import",
"user_id" => user.id,
"actor" => actor
})
|> Oban.insert()
acc ++ [job]
end)
end)
{:ok, jobs}
end end
def mutes_import(%User{} = user, [_ | _] = identifiers) do def mutes_import(%User{} = user, [_ | _] = actors) do
BackgroundWorker.new(%{ jobs =
"op" => "mutes_import", Repo.checkout(fn ->
"user_id" => user.id, Enum.reduce(actors, [], fn actor, acc ->
"identifiers" => identifiers {:ok, job} =
}) BackgroundWorker.new(%{
|> Oban.insert() "op" => "mute_import",
"user_id" => user.id,
"actor" => actor
})
|> Oban.insert()
acc ++ [job]
end)
end)
{:ok, jobs}
end end
end end

View file

@ -38,8 +38,8 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do
|> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@"))) |> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@")))
|> Enum.reject(&(&1 == "")) |> Enum.reject(&(&1 == ""))
User.Import.follow_import(follower, identifiers) User.Import.follows_import(follower, identifiers)
json(conn, "job started") json(conn, "jobs started")
end end
def blocks( def blocks(
@ -55,7 +55,7 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do
defp do_block(%{assigns: %{user: blocker}} = conn, list) do defp do_block(%{assigns: %{user: blocker}} = conn, list) do
User.Import.blocks_import(blocker, prepare_user_identifiers(list)) User.Import.blocks_import(blocker, prepare_user_identifiers(list))
json(conn, "job started") json(conn, "jobs started")
end end
def mutes( def mutes(
@ -71,7 +71,7 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do
defp do_mute(%{assigns: %{user: user}} = conn, list) do defp do_mute(%{assigns: %{user: user}} = conn, list) do
User.Import.mutes_import(user, prepare_user_identifiers(list)) User.Import.mutes_import(user, prepare_user_identifiers(list))
json(conn, "job started") json(conn, "jobs started")
end end
defp prepare_user_identifiers(list) do defp prepare_user_identifiers(list) do

View file

@ -19,10 +19,10 @@ defmodule Pleroma.Workers.BackgroundWorker do
User.perform(:force_password_reset, user) User.perform(:force_password_reset, user)
end end
def perform(%Job{args: %{"op" => op, "user_id" => user_id, "identifiers" => identifiers}}) def perform(%Job{args: %{"op" => op, "user_id" => user_id, "actor" => actor}})
when op in ["blocks_import", "follow_import", "mutes_import"] do when op in ["block_import", "follow_import", "mute_import"] do
user = User.get_cached_by_id(user_id) user = User.get_cached_by_id(user_id)
{:ok, User.Import.perform(String.to_existing_atom(op), user, identifiers)} User.Import.perform(String.to_existing_atom(op), user, actor)
end end
def perform(%Job{ def perform(%Job{

View file

@ -25,11 +25,12 @@ defmodule Pleroma.User.ImportTest do
user3.nickname user3.nickname
] ]
{:ok, job} = User.Import.follow_import(user1, identifiers) {:ok, jobs} = User.Import.follows_import(user1, identifiers)
for job <- jobs do
assert {:ok, %User{}} = ObanHelpers.perform(job)
end
assert {:ok, result} = ObanHelpers.perform(job)
assert is_list(result)
assert result == [{:ok, refresh_record(user2)}, {:ok, refresh_record(user3)}]
assert User.following?(user1, user2) assert User.following?(user1, user2)
assert User.following?(user1, user3) assert User.following?(user1, user3)
end end
@ -44,11 +45,12 @@ defmodule Pleroma.User.ImportTest do
user3.nickname user3.nickname
] ]
{:ok, job} = User.Import.blocks_import(user1, identifiers) {:ok, jobs} = User.Import.blocks_import(user1, identifiers)
for job <- jobs do
assert {:ok, %User{}} = ObanHelpers.perform(job)
end
assert {:ok, result} = ObanHelpers.perform(job)
assert is_list(result)
assert result == [{:ok, user2}, {:ok, user3}]
assert User.blocks?(user1, user2) assert User.blocks?(user1, user2)
assert User.blocks?(user1, user3) assert User.blocks?(user1, user3)
end end
@ -63,11 +65,12 @@ defmodule Pleroma.User.ImportTest do
user3.nickname user3.nickname
] ]
{:ok, job} = User.Import.mutes_import(user1, identifiers) {:ok, jobs} = User.Import.mutes_import(user1, identifiers)
for job <- jobs do
assert {:ok, %User{}} = ObanHelpers.perform(job)
end
assert {:ok, result} = ObanHelpers.perform(job)
assert is_list(result)
assert result == [{:ok, user2}, {:ok, user3}]
assert User.mutes?(user1, user2) assert User.mutes?(user1, user2)
assert User.mutes?(user1, user3) assert User.mutes?(user1, user3)
end end

View file

@ -22,7 +22,7 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do
test "it returns HTTP 200", %{conn: conn} do test "it returns HTTP 200", %{conn: conn} do
user2 = insert(:user) user2 = insert(:user)
assert "job started" == assert "jobs started" ==
conn conn
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"}) |> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"})
@ -38,7 +38,7 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do
"Account address,Show boosts\n#{user2.ap_id},true" "Account address,Show boosts\n#{user2.ap_id},true"
end} end}
]) do ]) do
assert "job started" == assert "jobs started" ==
conn conn
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/pleroma/follow_import", %{ |> post("/api/pleroma/follow_import", %{
@ -46,9 +46,9 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do
}) })
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
assert [{:ok, job_result}] = ObanHelpers.perform_all() assert [{:ok, updated_user}] = ObanHelpers.perform_all()
assert job_result == [refresh_record(user2)] assert updated_user.id == user2.id
assert [%Pleroma.User{follower_count: 1}] = job_result assert updated_user.follower_count == 1
end end
end end
@ -63,7 +63,7 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do
}) })
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
assert response == "job started" assert response == "jobs started"
end end
test "requires 'follow' or 'write:follows' permissions" do test "requires 'follow' or 'write:follows' permissions" do
@ -102,14 +102,20 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do
] ]
|> Enum.join("\n") |> Enum.join("\n")
assert "job started" == assert "jobs started" ==
conn conn
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/pleroma/follow_import", %{"list" => identifiers}) |> post("/api/pleroma/follow_import", %{"list" => identifiers})
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
assert [{:ok, job_result}] = ObanHelpers.perform_all() results = ObanHelpers.perform_all()
assert job_result == Enum.map(users, &refresh_record/1)
returned_users =
for {_, returned_user} <- results do
returned_user
end
assert returned_users == Enum.map(users, &refresh_record/1)
end end
end end
@ -120,7 +126,7 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do
test "it returns HTTP 200", %{conn: conn} do test "it returns HTTP 200", %{conn: conn} do
user2 = insert(:user) user2 = insert(:user)
assert "job started" == assert "jobs started" ==
conn conn
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"}) |> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"})
@ -133,7 +139,7 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do
with_mocks([ with_mocks([
{File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} {File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end}
]) do ]) do
assert "job started" == assert "jobs started" ==
conn conn
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/pleroma/blocks_import", %{ |> post("/api/pleroma/blocks_import", %{
@ -141,8 +147,14 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do
}) })
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
assert [{:ok, job_result}] = ObanHelpers.perform_all() results = ObanHelpers.perform_all()
assert job_result == users
returned_users =
for {_, returned_user} <- results do
returned_user
end
assert returned_users == users
end end
end end
@ -159,14 +171,25 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do
] ]
|> Enum.join(" ") |> Enum.join(" ")
assert "job started" == assert "jobs started" ==
conn conn
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/pleroma/blocks_import", %{"list" => identifiers}) |> post("/api/pleroma/blocks_import", %{"list" => identifiers})
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
assert [{:ok, job_result}] = ObanHelpers.perform_all() results = ObanHelpers.perform_all()
assert job_result == users
returned_user_ids =
for {_, user} <- results do
user.id
end
original_user_ids =
for user <- users do
user.id
end
assert match?(^original_user_ids, returned_user_ids)
end end
end end
@ -177,24 +200,25 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do
test "it returns HTTP 200", %{user: user, conn: conn} do test "it returns HTTP 200", %{user: user, conn: conn} do
user2 = insert(:user) user2 = insert(:user)
assert "job started" == assert "jobs started" ==
conn conn
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/pleroma/mutes_import", %{"list" => "#{user2.ap_id}"}) |> post("/api/pleroma/mutes_import", %{"list" => "#{user2.ap_id}"})
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
assert [{:ok, job_result}] = ObanHelpers.perform_all() [{:ok, result_user}] = ObanHelpers.perform_all()
assert job_result == [user2]
assert result_user == refresh_record(user2)
assert Pleroma.User.mutes?(user, user2) assert Pleroma.User.mutes?(user, user2)
end end
test "it imports mutes users from file", %{user: user, conn: conn} do test "it imports mutes users from file", %{user: user, conn: conn} do
users = [user2, user3] = insert_list(2, :user) [user2, user3] = insert_list(2, :user)
with_mocks([ with_mocks([
{File, [], read!: fn "mutes_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end} {File, [], read!: fn "mutes_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end}
]) do ]) do
assert "job started" == assert "jobs started" ==
conn conn
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/pleroma/mutes_import", %{ |> post("/api/pleroma/mutes_import", %{
@ -202,14 +226,19 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do
}) })
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
assert [{:ok, job_result}] = ObanHelpers.perform_all() results = ObanHelpers.perform_all()
assert job_result == users
assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) returned_users =
for {_, returned_user} <- results do
returned_user
end
assert Enum.all?(returned_users, &Pleroma.User.mutes?(user, &1))
end end
end end
test "it imports mutes with different nickname variations", %{user: user, conn: conn} do test "it imports mutes with different nickname variations", %{user: user, conn: conn} do
users = [user2, user3, user4, user5, user6] = insert_list(5, :user) [user2, user3, user4, user5, user6] = insert_list(5, :user)
identifiers = identifiers =
[ [
@ -221,15 +250,20 @@ defmodule Pleroma.Web.PleromaAPI.UserImportControllerTest do
] ]
|> Enum.join(" ") |> Enum.join(" ")
assert "job started" == assert "jobs started" ==
conn conn
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/pleroma/mutes_import", %{"list" => identifiers}) |> post("/api/pleroma/mutes_import", %{"list" => identifiers})
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
assert [{:ok, job_result}] = ObanHelpers.perform_all() results = ObanHelpers.perform_all()
assert job_result == users
assert Enum.all?(users, &Pleroma.User.mutes?(user, &1)) returned_users =
for {_, returned_user} <- results do
returned_user
end
assert Enum.all?(returned_users, &Pleroma.User.mutes?(user, &1))
end end
end end
end end