Added Hashtag entity and objects-hashtags association with auto-sync with `data.tag...
authorIvan Tashkinov <ivantashkinov@gmail.com>
Tue, 22 Dec 2020 19:04:33 +0000 (22:04 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Tue, 22 Dec 2020 19:04:33 +0000 (22:04 +0300)
lib/pleroma/hashtag.ex [new file with mode: 0644]
lib/pleroma/object.ex
priv/repo/migrations/20201221202251_create_hashtags.exs [new file with mode: 0644]
priv/repo/migrations/20201221203824_create_hashtags_objects.exs [new file with mode: 0644]
test/pleroma/object_test.exs
test/pleroma/web/activity_pub/activity_pub_test.exs

diff --git a/lib/pleroma/hashtag.ex b/lib/pleroma/hashtag.ex
new file mode 100644 (file)
index 0000000..b059275
--- /dev/null
@@ -0,0 +1,58 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Hashtag do
+  use Ecto.Schema
+
+  import Ecto.Changeset
+
+  alias Pleroma.Hashtag
+  alias Pleroma.Repo
+
+  @derive {Jason.Encoder, only: [:data]}
+
+  schema "hashtags" do
+    field(:name, :string)
+    field(:data, :map, default: %{})
+
+    many_to_many(:objects, Pleroma.Object, join_through: "hashtags_objects", on_replace: :delete)
+
+    timestamps()
+  end
+
+  def get_by_name(name) do
+    Repo.get_by(Hashtag, name: name)
+  end
+
+  def get_or_create_by_name(name) when is_bitstring(name) do
+    with %Hashtag{} = hashtag <- get_by_name(name) do
+      {:ok, hashtag}
+    else
+      _ ->
+        %Hashtag{}
+        |> changeset(%{name: name})
+        |> Repo.insert()
+    end
+  end
+
+  def get_or_create_by_names(names) when is_list(names) do
+    Enum.reduce_while(names, {:ok, []}, fn name, {:ok, list} ->
+      case get_or_create_by_name(name) do
+        {:ok, %Hashtag{} = hashtag} ->
+          {:cont, {:ok, list ++ [hashtag]}}
+
+        error ->
+          {:halt, error}
+      end
+    end)
+  end
+
+  def changeset(%Hashtag{} = struct, params) do
+    struct
+    |> cast(params, [:name, :data])
+    |> update_change(:name, &String.downcase/1)
+    |> validate_required([:name])
+    |> unique_constraint(:name)
+  end
+end
index 2088c765615ed7fc1e352371adcf315504f6156c..357a3b504777f653219d3a130c073e1abb52fdde 100644 (file)
@@ -10,6 +10,7 @@ defmodule Pleroma.Object do
 
   alias Pleroma.Activity
   alias Pleroma.Config
+  alias Pleroma.Hashtag
   alias Pleroma.Object
   alias Pleroma.Object.Fetcher
   alias Pleroma.ObjectTombstone
@@ -26,6 +27,8 @@ defmodule Pleroma.Object do
   schema "objects" do
     field(:data, :map)
 
+    many_to_many(:hashtags, Hashtag, join_through: "hashtags_objects", on_replace: :delete)
+
     timestamps()
   end
 
@@ -53,17 +56,31 @@ defmodule Pleroma.Object do
   end
 
   def change(struct, params \\ %{}) do
-    changeset =
-      struct
-      |> cast(params, [:data])
-      |> validate_required([:data])
-      |> unique_constraint(:ap_id, name: :objects_unique_apid_index)
-
-    if hashtags_changed?(struct, get_change(changeset, :data)) do
-      # TODO: modify assoc once it's introduced
-      changeset
+    struct
+    |> cast(params, [:data])
+    |> validate_required([:data])
+    |> unique_constraint(:ap_id, name: :objects_unique_apid_index)
+    |> maybe_handle_hashtags_change(struct)
+  end
+
+  defp maybe_handle_hashtags_change(changeset, struct) do
+    with data_hashtags_change = get_change(changeset, :data),
+         true <- hashtags_changed?(struct, data_hashtags_change),
+         {:ok, hashtag_records} <-
+           data_hashtags_change
+           |> object_data_hashtags()
+           |> Hashtag.get_or_create_by_names() do
+      put_assoc(changeset, :hashtags, hashtag_records)
     else
-      changeset
+      false ->
+        changeset
+
+      {:error, hashtag_changeset} ->
+        failed_hashtag = get_field(hashtag_changeset, :name)
+
+        validate_change(changeset, :data, fn _, _ ->
+          [data: "error referencing hashtag: #{failed_hashtag}"]
+        end)
     end
   end
 
diff --git a/priv/repo/migrations/20201221202251_create_hashtags.exs b/priv/repo/migrations/20201221202251_create_hashtags.exs
new file mode 100644 (file)
index 0000000..afc5220
--- /dev/null
@@ -0,0 +1,14 @@
+defmodule Pleroma.Repo.Migrations.CreateHashtags do
+  use Ecto.Migration
+
+  def change do
+    create_if_not_exists table(:hashtags) do
+      add(:name, :citext, null: false)
+      add(:data, :map, default: %{})
+
+      timestamps()
+    end
+
+    create_if_not_exists(unique_index(:hashtags, [:name]))
+  end
+end
diff --git a/priv/repo/migrations/20201221203824_create_hashtags_objects.exs b/priv/repo/migrations/20201221203824_create_hashtags_objects.exs
new file mode 100644 (file)
index 0000000..b2649b4
--- /dev/null
@@ -0,0 +1,13 @@
+defmodule Pleroma.Repo.Migrations.CreateHashtagsObjects do
+  use Ecto.Migration
+
+  def change do
+    create_if_not_exists table(:hashtags_objects) do
+      add(:hashtag_id, references(:hashtags), null: false)
+      add(:object_id, references(:objects), null: false)
+    end
+
+    create_if_not_exists(unique_index(:hashtags_objects, [:hashtag_id, :object_id]))
+    create_if_not_exists(index(:hashtags_objects, [:object_id]))
+  end
+end
index 5d4e6fb84691cb904aec72f4cafb10777718f92b..819ecd210aa46f572d0ab311cba746fd8f5ce03b 100644 (file)
@@ -5,10 +5,13 @@
 defmodule Pleroma.ObjectTest do
   use Pleroma.DataCase
   use Oban.Testing, repo: Pleroma.Repo
+
   import ExUnit.CaptureLog
   import Pleroma.Factory
   import Tesla.Mock
+
   alias Pleroma.Activity
+  alias Pleroma.Hashtag
   alias Pleroma.Object
   alias Pleroma.Repo
   alias Pleroma.Tests.ObanHelpers
@@ -406,4 +409,28 @@ defmodule Pleroma.ObjectTest do
       assert updated_object.data["like_count"] == 1
     end
   end
+
+  describe ":hashtags association" do
+    test "Hashtag records are created with Object record and updated on its change" do
+      user = insert(:user)
+
+      {:ok, %{object: object}} =
+        CommonAPI.post(user, %{status: "some text #hashtag1 #hashtag2 ..."})
+
+      assert [%Hashtag{name: "hashtag1"}, %Hashtag{name: "hashtag2"}] =
+               Enum.sort_by(object.hashtags, & &1.name)
+
+      {:ok, object} = Object.update_data(object, %{"tag" => []})
+
+      assert [] = object.hashtags
+
+      object = Object.get_by_id(object.id) |> Repo.preload(:hashtags)
+      assert [] = object.hashtags
+
+      {:ok, object} = Object.update_data(object, %{"tag" => ["abc", "def"]})
+
+      assert [%Hashtag{name: "abc"}, %Hashtag{name: "def"}] =
+               Enum.sort_by(object.hashtags, & &1.name)
+    end
+  end
 end
index 9eb7ae86b138a0c40b949174f196a557b784fc2c..bfec32042db39c432ad10309a37a8c34e994d1d4 100644 (file)
@@ -217,6 +217,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
         tag_all: ["test", "reject"]
       })
 
+    [fetch_one, fetch_two, fetch_three, fetch_four] =
+      Enum.map([fetch_one, fetch_two, fetch_three, fetch_four], fn statuses ->
+        Enum.map(statuses, fn s -> Repo.preload(s, object: :hashtags) end)
+      end)
+
     assert fetch_one == [status_one, status_three]
     assert fetch_two == [status_one, status_two, status_three]
     assert fetch_three == [status_one, status_two]