Insert text representation of hashtags into object["hashtags"]
authorHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Fri, 31 Jul 2020 14:46:35 +0000 (16:46 +0200)
committerHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Tue, 22 Dec 2020 04:15:34 +0000 (05:15 +0100)
Includes a new mix task: pleroma.database fill_old_hashtags

22 files changed:
CHANGELOG.md
docs/administration/CLI_tasks/database.md
lib/mix/tasks/pleroma/database.ex
lib/pleroma/activity/ir/topics.ex
lib/pleroma/constants.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/mrf/simple_policy.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/common_api/utils.ex
lib/pleroma/web/mastodon_api/views/status_view.ex
lib/pleroma/web/templates/feed/feed/_activity.atom.eex
lib/pleroma/web/templates/feed/feed/_activity.rss.eex
lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex
priv/repo/migrations/20200731165800_add_hashtags_index_to_objects.exs [new file with mode: 0644]
test/pleroma/activity/ir/topics_test.exs
test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
test/pleroma/web/activity_pub/transmogrifier_test.exs
test/pleroma/web/common_api/utils_test.exs
test/pleroma/web/common_api_test.exs
test/pleroma/web/mastodon_api/views/status_view_test.exs
test/support/factory.ex

index c6bf38ee02fdb2e3a1f75f16cb4577ea7e302c11..a5e5f5eccc5c7c553619a27052e265cab6844a05 100644 (file)
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ### Changed
 
+- **Breaking:** Changed storage of hashtags in plain-text to `object->hashtags`, run [`pleroma.database fill_old_hashtags` mix task](docs/administration/CLI_tasks/database.md) for old objects (works while pleroma is running).
 - Polls now always return a `voters_count`, even if they are single-choice.
 - Admin Emails: The ap id is used as the user link in emails now.
 - Improved registration workflow for email confirmation and account approval modes.
@@ -432,7 +433,6 @@ switched to a new configuration mechanism, however it was not officially removed
 - Static-FE: Fix remote posts not being sanitized
 
 ### Fixed
-=======
 - Rate limiter crashes when there is no explicitly specified ip in the config
 - 500 errors when no `Accept` header is present if Static-FE is enabled
 - Instance panel not being updated immediately due to wrong `Cache-Control` headers
index 6dca8316729079c80ca997fe24f9b84b479c4795..a2d2e8cdda516b57db0d2bdb322f7346a3f6876a 100644 (file)
@@ -91,6 +91,16 @@ Can be safely re-run
     mix pleroma.database fix_likes_collections
     ```
 
+## Fill hashtags for old objects
+
+```sh tab="OTP"
+./bin/pleroma_ctl database fill_old_hashtags
+```
+
+```sh tab="From Source"
+mix pleroma.database fill_old_hashtags
+```
+
 ## Vacuum the database
 
 ### Analyze
index 22151ce08e4d6fef4c42c23f04490d15837656d3..0c1343313118749930993926d4de8d3922fe6d19 100644 (file)
@@ -128,6 +128,49 @@ defmodule Mix.Tasks.Pleroma.Database do
     |> Stream.run()
   end
 
+  def run(["fill_old_hashtags"]) do
+    import Ecto.Query
+
+    start_pleroma()
+
+    from(
+      o in Object,
+      where: fragment("(?)->>'hashtags' is null", o.data),
+      where: fragment("(?)->>'tag' != '[]'", o.data),
+      select: %{id: o.id, tag: fragment("(?)->>'tag'", o.data)},
+      order_by: [:desc, o.id]
+    )
+    |> Pleroma.Repo.chunk_stream(200, :batches)
+    |> Stream.each(fn objects ->
+      Repo.transaction(fn ->
+        objects_first = objects |> List.first()
+        objects_last = objects |> List.last()
+
+        Logger.info(
+          "fill_old_hashtags: #{objects_first.id} (#{objects_first.inserted_at}) -- #{
+            objects_last.id
+          } (#{objects_last.inserted_at})"
+        )
+
+        objects
+        |> Enum.map(fn object ->
+          tags =
+            object.tag
+            |> Jason.decode!()
+            |> Enum.filter(&is_bitstring(&1))
+
+          Object
+          |> where([o], o.id == ^object.id)
+          |> update([o],
+            set: [data: fragment("safe_jsonb_set(?, '{hashtags}', ?, true)", o.data, ^tags)]
+          )
+          |> Repo.update_all([], timeout: :infinity)
+        end)
+      end)
+    end)
+    |> Stream.run()
+  end
+
   def run(["vacuum", args]) do
     start_pleroma()
 
index fe2e8cb5c900d7b84f0e92bc1ba42a74c85d685b..2cdecf1e4c67e667be717d0e5d28cae6ad810efa 100644 (file)
@@ -48,6 +48,10 @@ defmodule Pleroma.Activity.Ir.Topics do
     tags
   end
 
+  defp hashtags_to_topics(%{data: %{"hashtags" => tags}}) do
+    Enum.map(tags, fn tag -> "hashtag:" <> tag end)
+  end
+
   defp hashtags_to_topics(%{data: %{"tag" => tags}}) do
     tags
     |> Enum.filter(&is_bitstring(&1))
index cf8182d55a2d11486ca99716edb48b151f3b6dab..8f265715c3c2e6f0778aa4941c750ba78c304204 100644 (file)
@@ -18,7 +18,8 @@ defmodule Pleroma.Constants do
       "emoji",
       "context_id",
       "deleted_activity_id",
-      "pleroma_internal"
+      "pleroma_internal",
+      "hashtags"
     ]
   )
 
index 1c91bc07482b22ac3905512860537f5a009486d3..61c1043ed8163e00512b4db4f6b1cad3a0e4cda1 100644 (file)
@@ -666,7 +666,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   defp restrict_tag_reject(query, %{tag_reject: [_ | _] = tag_reject}) do
     from(
       [_activity, object] in query,
-      where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
+      where: fragment("not (?)->'hashtags' \\?| (?)", object.data, ^tag_reject)
     )
   end
 
@@ -679,7 +679,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   defp restrict_tag_all(query, %{tag_all: [_ | _] = tag_all}) do
     from(
       [_activity, object] in query,
-      where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
+      where: fragment("(?)->'hashtags' \\?& (?)", object.data, ^tag_all)
     )
   end
 
@@ -692,14 +692,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   defp restrict_tag(query, %{tag: tag}) when is_list(tag) do
     from(
       [_activity, object] in query,
-      where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
+      where: fragment("(?)->'hashtags' \\?| (?)", object.data, ^tag)
     )
   end
 
   defp restrict_tag(query, %{tag: tag}) when is_binary(tag) do
     from(
       [_activity, object] in query,
-      where: fragment("(?)->'tag' \\? (?)", object.data, ^tag)
+      where: fragment("(?)->'hashtags' \\? (?)", object.data, ^tag)
     )
   end
 
index 6cd91826db178ba72f9cc0859fe7eac5c7397eca..2fa7b3194d02235856ab7b1bfcd08aaede549b9d 100644 (file)
@@ -74,9 +74,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
 
     object =
       if MRF.subdomain_match?(media_nsfw, actor_host) do
-        tags = (child_object["tag"] || []) ++ ["nsfw"]
-        child_object = Map.put(child_object, "tag", tags)
-        child_object = Map.put(child_object, "sensitive", true)
+        child_object =
+          child_object
+          |> Map.put("hashtags", (child_object["hashtags"] || []) ++ ["nsfw"])
+          |> Map.put("sensitive", true)
+
         Map.put(object, "object", child_object)
       else
         object
index 565d324330b751ca35bc65dfbdb3589e993f6a4c..d3dc637dac34a3f2391517b2a915206f8b2349de 100644 (file)
@@ -312,16 +312,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   def fix_emoji(object), do: object
 
   def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
-    tags =
+    hashtags =
       tag
       |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
-      |> Enum.map(fn %{"name" => name} ->
-        name
-        |> String.slice(1..-1)
-        |> String.downcase()
+      |> Enum.map(fn
+        %{"name" => "#" <> hashtag} -> String.downcase(hashtag)
+        %{"name" => hashtag} -> String.downcase(hashtag)
       end)
 
-    Map.put(object, "tag", tag ++ tags)
+    Map.put(object, "hashtags", hashtags)
   end
 
   def fix_tag(%{"tag" => %{} = tag} = object) do
@@ -865,7 +864,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
 
   def add_hashtags(object) do
     tags =
-      (object["tag"] || [])
+      ((object["hashtags"] || []) ++ (object["tag"] || []))
       |> Enum.map(fn
         # Expand internal representation tags into AS2 tags.
         tag when is_binary(tag) ->
@@ -936,7 +935,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   end
 
   def set_sensitive(object) do
-    tags = object["tag"] || []
+    tags = object["hashtags"] || object["tag"] || []
     Map.put(object, "sensitive", "nsfw" in tags)
   end
 
index 1c74ea7875760355cbfc4baf7c0a58bf7bfb69a2..880b5d78f86965b617323960a31c2585696fbe2b 100644 (file)
@@ -310,7 +310,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
       "context" => draft.context,
       "attachment" => draft.attachments,
       "actor" => draft.user.ap_id,
-      "tag" => Keyword.values(draft.tags) |> Enum.uniq()
+      "tag" => Enum.filter(draft.tags, &is_map(&1)) |> Enum.uniq(),
+      "hashtags" =>
+        draft.tags
+        |> Enum.reduce([], fn
+          # Why so many formats
+          {:name, x}, acc -> if is_bitstring(x), do: [x | acc], else: acc
+          {"#" <> _, x}, acc -> if is_bitstring(x), do: [x | acc], else: acc
+          x, acc -> if is_bitstring(x), do: [x | acc], else: acc
+        end)
+        |> Enum.uniq()
     }
     |> add_in_reply_to(draft.in_reply_to)
     |> Map.merge(draft.extra)
index 2301e21cfaf2d56b760fe35fa733dd2be502eeba..6fc6272c28922b63c1cb4fc17b668aad62ef3e05 100644 (file)
@@ -347,7 +347,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       media_attachments: attachments,
       poll: render(PollView, "show.json", object: object, for: opts[:for]),
       mentions: mentions,
-      tags: build_tags(tags),
+      tags: build_tags(object.data["hashtags"] || tags),
       application: %{
         name: "Web",
         website: nil
index 3fd150c4e7570b6d54e449a87ffac7d1adc74d7a..12a9545f3458530131875b56bd8e6a4eacaf2b78 100644 (file)
@@ -22,8 +22,8 @@
     <link type="text/html" href='<%= @data["external_url"] %>' rel="alternate"/>
   <% end %>
 
-  <%= for tag <- @data["tag"] || [] do %>
-    <category term="<%= tag %>"></category>
+  <%= for hashtag <- @data["hashtags"] || [] do %>
+    <category term="<%= hashtag %>"></category>
   <% end %>
 
   <%= for attachment <- @data["attachment"] || [] do %>
index 42960de7d45f58926546a23145a8d1db60ad108f..00872b4b76db926e224001c3829fcc4ac7835c73 100644 (file)
@@ -21,8 +21,8 @@
     <link><%= @data["external_url"] %></link>
   <% end %>
 
-  <%= for tag <- @data["tag"] || [] do %>
-    <category term="<%= tag %>"></category>
+  <%= for hashtag <- @data["hashtags"] || [] do %>
+    <category term="<%= hashtag %>"></category>
   <% end %>
 
   <%= for attachment <- @data["attachment"] || [] do %>
index cf5874a91341cb8108631829aa150ec9fd70e15b..1377a6bbc72b2ecb41f6e5e2fc7e118ab9baec04 100644 (file)
@@ -41,8 +41,8 @@
       <% end %>
     <% end %>
 
-    <%= for tag <- @data["tag"] || [] do %>
-      <category term="<%= tag %>"></category>
+    <%= for hashtag <- @data["hashtags"] || [] do %>
+      <category term="<%= hashtag %>"></category>
     <% end %>
 
     <%= for {emoji, file} <- @data["emoji"] || %{} do %>
diff --git a/priv/repo/migrations/20200731165800_add_hashtags_index_to_objects.exs b/priv/repo/migrations/20200731165800_add_hashtags_index_to_objects.exs
new file mode 100644 (file)
index 0000000..b786828
--- /dev/null
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.AddHashtagsIndexToObjects do
+  use Ecto.Migration
+
+  def change do
+    drop_if_exists(index(:objects, ["(data->'tag')"], using: :gin, name: :objects_tags))
+
+    create_if_not_exists(
+      index(:objects, ["(data->'hashtags')"], using: :gin, name: :objects_hashtags)
+    )
+  end
+end
index 5e5c2f8dac7d48138931187a147c0685084a9782..eb098ee958ed607c95669fcee5ffc31b75a1767c 100644 (file)
@@ -78,7 +78,7 @@ defmodule Pleroma.Activity.Ir.TopicsTest do
     end
 
     test "converts tags to hash tags", %{activity: %{object: %{data: data} = object} = activity} do
-      tagged_data = Map.put(data, "tag", ["foo", "bar"])
+      tagged_data = Map.put(data, "hashtags", ["foo", "bar"])
       activity = %{activity | object: %{object | data: tagged_data}}
 
       topics = Topics.get_activity_topics(activity)
index d7dde62c40c8643bf2fbafc3b4fc7c08f6b17ec9..9777fcde1b0d8a2e98c319b7f643bd2170911d03 100644 (file)
@@ -78,7 +78,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
       assert SimplePolicy.filter(media_message) ==
                {:ok,
                 media_message
-                |> put_in(["object", "tag"], ["foo", "nsfw"])
+                |> put_in(["object", "hashtags"], ["foo", "nsfw"])
                 |> put_in(["object", "sensitive"], true)}
 
       assert SimplePolicy.filter(local_message) == {:ok, local_message}
@@ -92,7 +92,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
       assert SimplePolicy.filter(media_message) ==
                {:ok,
                 media_message
-                |> put_in(["object", "tag"], ["foo", "nsfw"])
+                |> put_in(["object", "hashtags"], ["foo", "nsfw"])
                 |> put_in(["object", "sensitive"], true)}
 
       assert SimplePolicy.filter(local_message) == {:ok, local_message}
@@ -105,7 +105,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
       "type" => "Create",
       "object" => %{
         "attachment" => [%{}],
-        "tag" => ["foo"],
+        "hashtags" => ["foo"],
         "sensitive" => false
       }
     }
index b4a006aec7f75048b26d17b64a184f90fb2b5837..528636f04b797d0c4491651eb05029a9a6e40b6a 100644 (file)
@@ -39,7 +39,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
       object = Object.normalize(data["object"])
 
-      assert "test" in object.data["tag"]
+      assert ["test"] == object.data["hashtags"]
     end
 
     test "it cleans up incoming notices which are not really DMs" do
@@ -220,7 +220,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
       object = Object.normalize(data["object"])
 
-      assert Enum.at(object.data["tag"], 2) == "moo"
+      assert object.data["hashtags"] == ["moo"]
     end
 
     test "it works for incoming notices with contentMap" do
index 66ea7664aff340a9135158e9cc9be64e853ded31..d0bd00b58dda8c162e9f4b9da1a2c3f8f8d1b9af 100644 (file)
@@ -204,30 +204,37 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
 
       {:ok, activity} = CommonAPI.post(user, %{status: "#2hu :firefox:"})
 
-      {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
-
-      assert length(modified["object"]["tag"]) == 2
-
-      assert is_nil(modified["object"]["emoji"])
-      assert is_nil(modified["object"]["like_count"])
-      assert is_nil(modified["object"]["announcements"])
-      assert is_nil(modified["object"]["announcement_count"])
-      assert is_nil(modified["object"]["context_id"])
+      {:ok, %{"object" => modified_object}} = Transmogrifier.prepare_outgoing(activity.data)
+
+      assert [
+               %{"name" => "#2hu", "type" => "Hashtag"},
+               %{"name" => ":firefox:", "type" => "Emoji"}
+             ] = modified_object["tag"]
+
+      refute Map.has_key?(modified_object, "hashtags")
+      refute Map.has_key?(modified_object, "emoji")
+      refute Map.has_key?(modified_object, "like_count")
+      refute Map.has_key?(modified_object, "announcements")
+      refute Map.has_key?(modified_object, "announcement_count")
+      refute Map.has_key?(modified_object, "context_id")
     end
 
     test "it strips internal fields of article" do
       activity = insert(:article_activity)
 
-      {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+      {:ok, %{"object" => modified_object}} = Transmogrifier.prepare_outgoing(activity.data)
 
-      assert length(modified["object"]["tag"]) == 2
+      assert [
+               %{"name" => "#2hu", "type" => "Hashtag"},
+               %{"name" => ":2hu:", "type" => "Emoji"}
+             ] = modified_object["tag"]
 
-      assert is_nil(modified["object"]["emoji"])
-      assert is_nil(modified["object"]["like_count"])
-      assert is_nil(modified["object"]["announcements"])
-      assert is_nil(modified["object"]["announcement_count"])
-      assert is_nil(modified["object"]["context_id"])
-      assert is_nil(modified["object"]["likes"])
+      refute Map.has_key?(modified_object, "hashtags")
+      refute Map.has_key?(modified_object, "emoji")
+      refute Map.has_key?(modified_object, "like_count")
+      refute Map.has_key?(modified_object, "announcements")
+      refute Map.has_key?(modified_object, "announcement_count")
+      refute Map.has_key?(modified_object, "context_id")
     end
 
     test "the directMessage flag is present" do
index 4d6c9ea2610786def850afffaecec5bef223ecf2..2110421927ccc60620f7082e76ef964b2fc73f26 100644 (file)
@@ -591,7 +591,8 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
                "context" => "2hu",
                "sensitive" => false,
                "summary" => "test summary",
-               "tag" => ["jimm"],
+               "hashtags" => ["jimm"],
+               "tag" => [],
                "to" => [user2.ap_id],
                "type" => "Note",
                "custom_tag" => "test"
index 585b2c174c9564c4aa2ccb408c7847f39374b671..3b7ac20336ddc6909ba264e50b32ab364770d8a0 100644 (file)
@@ -493,7 +493,8 @@ defmodule Pleroma.Web.CommonAPITest do
 
     object = Object.normalize(activity)
 
-    assert object.data["tag"] == ["2hu"]
+    assert object.data["tag"] == []
+    assert object.data["hashtags"] == ["2hu"]
   end
 
   test "it adds emoji in the object" do
index f2a7469edb436ab1459d26572b4ec6278cdfb041..ecce26261dade86b775153ec5de0dd7d7323bead 100644 (file)
@@ -262,8 +262,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
       mentions: [],
       tags: [
         %{
-          name: "#{object_data["tag"]}",
-          url: "/tag/#{object_data["tag"]}"
+          name: "2hu",
+          url: "/tag/2hu"
         }
       ],
       application: %{
index 8eb07dc3c19e4f54d24f5372a8a46a58147a29d3..a709d0daeda313e70fed92d4034d986c05d12623 100644 (file)
@@ -93,7 +93,7 @@ defmodule Pleroma.Factory do
       "like_count" => 0,
       "context" => "2hu",
       "summary" => "2hu",
-      "tag" => ["2hu"],
+      "hashtags" => ["2hu"],
       "emoji" => %{
         "2hu" => "corndog.png"
       }