Remove duplicated entries in users' following lists
authorSergey Suprunenko <suprunenko.s@gmail.com>
Thu, 16 May 2019 20:04:08 +0000 (20:04 +0000)
committerfeld <feld@feld.me>
Thu, 16 May 2019 20:04:08 +0000 (20:04 +0000)
CHANGELOG.md
lib/mix/tasks/pleroma/database.ex
lib/pleroma/user.ex
test/tasks/database_test.exs [new file with mode: 0644]
test/tasks/user_test.exs
test/user_test.exs

index dc09d4f6b8e32707fe8ce7fcf5bb245834932ae9..4d314817b756b4c157ed983892028fe2e4e07dcb 100644 (file)
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Support for Mastodon's remote interaction
 - Mix Tasks: `mix pleroma.database bump_all_conversations`
 - Mix Tasks: `mix pleroma.database remove_embedded_objects`
+- Mix Tasks: `mix pleroma.database update_users_following_followers_counts`
 - Mix Tasks: `mix pleroma.user toggle_confirmed`
 - Federation: Support for reports
 - Configuration: `safe_dm_mentions` option
index 42753a1a48a87be70f5eb525c76afcf5a0c1ffa7..f650b447dde848fa381729fa6406ccc38e05ecde 100644 (file)
@@ -5,6 +5,8 @@
 defmodule Mix.Tasks.Pleroma.Database do
   alias Mix.Tasks.Pleroma.Common
   alias Pleroma.Conversation
+  alias Pleroma.Repo
+  alias Pleroma.User
   require Logger
   use Mix.Task
 
@@ -25,6 +27,9 @@ defmodule Mix.Tasks.Pleroma.Database do
 
       mix pleroma.database bump_all_conversations
 
+  ## Remove duplicated items from following and update followers count for all users
+
+      mix pleroma.database update_users_following_followers_counts
   """
   def run(["remove_embedded_objects" | args]) do
     {options, [], []} =
@@ -38,7 +43,7 @@ defmodule Mix.Tasks.Pleroma.Database do
     Common.start_pleroma()
     Logger.info("Removing embedded objects")
 
-    Pleroma.Repo.query!(
+    Repo.query!(
       "update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
       [],
       timeout: :infinity
@@ -47,7 +52,7 @@ defmodule Mix.Tasks.Pleroma.Database do
     if Keyword.get(options, :vacuum) do
       Logger.info("Runnning VACUUM FULL")
 
-      Pleroma.Repo.query!(
+      Repo.query!(
         "vacuum full;",
         [],
         timeout: :infinity
@@ -59,4 +64,12 @@ defmodule Mix.Tasks.Pleroma.Database do
     Common.start_pleroma()
     Conversation.bump_for_all_activities()
   end
+
+  def run(["update_users_following_followers_counts"]) do
+    Common.start_pleroma()
+
+    users = Repo.all(User)
+    Enum.each(users, &User.remove_duplicated_following/1)
+    Enum.each(users, &User.update_follower_count/1)
+  end
 end
index 1aa966dfc74fa441d6e1b192fa665b8d48cdb7a8..9ffb61300f040c261a51d41f728c5688359ad69b 100644 (file)
@@ -166,7 +166,7 @@ defmodule Pleroma.User do
 
   def update_changeset(struct, params \\ %{}) do
     struct
-    |> cast(params, [:bio, :name, :avatar])
+    |> cast(params, [:bio, :name, :avatar, :following])
     |> unique_constraint(:nickname)
     |> validate_format(:nickname, local_nickname_regex())
     |> validate_length(:bio, max: 5000)
@@ -709,6 +709,18 @@ defmodule Pleroma.User do
     end
   end
 
+  def remove_duplicated_following(%User{following: following} = user) do
+    uniq_following = Enum.uniq(following)
+
+    if length(following) == length(uniq_following) do
+      {:ok, user}
+    else
+      user
+      |> update_changeset(%{following: uniq_following})
+      |> update_and_set_cache()
+    end
+  end
+
   @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
   def get_users_from_set(ap_ids, local_only \\ true) do
     criteria = %{ap_id: ap_ids, deactivated: false}
diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs
new file mode 100644 (file)
index 0000000..579130b
--- /dev/null
@@ -0,0 +1,49 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.DatabaseTest do
+  alias Pleroma.Repo
+  alias Pleroma.User
+  use Pleroma.DataCase
+
+  import Pleroma.Factory
+
+  setup_all do
+    Mix.shell(Mix.Shell.Process)
+
+    on_exit(fn ->
+      Mix.shell(Mix.Shell.IO)
+    end)
+
+    :ok
+  end
+
+  describe "running update_users_following_followers_counts" do
+    test "following and followers count are updated" do
+      [user, user2] = insert_pair(:user)
+      {:ok, %User{following: following, info: info} = user} = User.follow(user, user2)
+
+      assert length(following) == 2
+      assert info.follower_count == 0
+
+      info_cng = Ecto.Changeset.change(info, %{follower_count: 3})
+
+      {:ok, user} =
+        user
+        |> Ecto.Changeset.change(%{following: following ++ following})
+        |> Ecto.Changeset.put_embed(:info, info_cng)
+        |> Repo.update()
+
+      assert length(user.following) == 4
+      assert user.info.follower_count == 3
+
+      assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"])
+
+      user = User.get_by_id(user.id)
+
+      assert length(user.following) == 2
+      assert user.info.follower_count == 0
+    end
+  end
+end
index 1f97740be3701cf52e9de47c5abee8037a3546c4..260ce0d954973868afe0424425a62aaff538096c 100644 (file)
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Mix.Tasks.Pleroma.UserTest do
+  alias Pleroma.Repo
   alias Pleroma.User
   use Pleroma.DataCase
 
index 16a014f2f93fe3b6fceb042d20bd6491f3c3deaa..721b65693dfa51920402728940f4201ef2c34f12 100644 (file)
@@ -626,6 +626,37 @@ defmodule Pleroma.UserTest do
     end
   end
 
+  describe "remove duplicates from following list" do
+    test "it removes duplicates" do
+      user = insert(:user)
+      follower = insert(:user)
+
+      {:ok, %User{following: following} = follower} = User.follow(follower, user)
+      assert length(following) == 2
+
+      {:ok, follower} =
+        follower
+        |> User.update_changeset(%{following: following ++ following})
+        |> Repo.update()
+
+      assert length(follower.following) == 4
+
+      {:ok, follower} = User.remove_duplicated_following(follower)
+      assert length(follower.following) == 2
+    end
+
+    test "it does nothing when following is uniq" do
+      user = insert(:user)
+      follower = insert(:user)
+
+      {:ok, follower} = User.follow(follower, user)
+      assert length(follower.following) == 2
+
+      {:ok, follower} = User.remove_duplicated_following(follower)
+      assert length(follower.following) == 2
+    end
+  end
+
   describe "follow_import" do
     test "it imports user followings from list" do
       [user1, user2, user3] = insert_list(3, :user)