--- /dev/null
+# 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
alias Pleroma.Activity
alias Pleroma.Config
+ alias Pleroma.Hashtag
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.ObjectTombstone
schema "objects" do
field(:data, :map)
+ many_to_many(:hashtags, Hashtag, join_through: "hashtags_objects", on_replace: :delete)
+
timestamps()
end
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
--- /dev/null
+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
--- /dev/null
+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
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
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
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]