From cb889a1539a5f65010ad9b5329af5b5a92611165 Mon Sep 17 00:00:00 2001 From: FloatingGhost Date: Sun, 4 Dec 2022 00:33:23 +0000 Subject: [PATCH] Add user hashtag follow/unfollow functions --- lib/pleroma/user.ex | 28 +++++++ lib/pleroma/user/hashtag_follow.ex | 29 +++++++ ...0221203232118_add_user_follows_hashtag.exs | 12 +++ test/pleroma/user_test.exs | 80 +++++++++++++++++++ test/support/factory.ex | 7 ++ 5 files changed, 156 insertions(+) create mode 100644 lib/pleroma/user/hashtag_follow.ex create mode 100644 priv/repo/migrations/20221203232118_add_user_follows_hashtag.exs diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index b0ab9d0cd..c8e43a606 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -18,6 +18,8 @@ defmodule Pleroma.User do alias Pleroma.Emoji alias Pleroma.FollowingRelationship alias Pleroma.Formatter + alias Pleroma.Hashtag + alias Pleroma.User.HashtagFollow alias Pleroma.HTML alias Pleroma.Keys alias Pleroma.MFA @@ -168,6 +170,12 @@ defmodule Pleroma.User do has_many(:frontend_profiles, Pleroma.Akkoma.FrontendSettingsProfile) + many_to_many(:followed_hashtags, Hashtag, + on_replace: :delete, + on_delete: :delete_all, + join_through: HashtagFollow + ) + for {relationship_type, [ {outgoing_relation, outgoing_relation_target}, @@ -2550,4 +2558,24 @@ defmodule Pleroma.User do _ -> {:error, user} end end + + def hashtag_follows(%User{} = user) do + HashtagFollow + |> where(user_id: ^user.id) + |> Repo.all() + end + + def follow_hashtag(%User{} = user, %Hashtag{} = hashtag) do + Logger.debug("Follow hashtag #{hashtag.name} for user #{user.nickname}") + + HashtagFollow.new(user, hashtag) + end + + def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do + Logger.debug("Unfollow hashtag #{hashtag.name} for user #{user.nickname}") + + from(hf in HashtagFollow) + |> where([hf], hf.user_id == ^user.id and hf.hashtag_id == ^hashtag.id) + |> Repo.delete_all() + end end diff --git a/lib/pleroma/user/hashtag_follow.ex b/lib/pleroma/user/hashtag_follow.ex new file mode 100644 index 000000000..421493fe6 --- /dev/null +++ b/lib/pleroma/user/hashtag_follow.ex @@ -0,0 +1,29 @@ +defmodule Pleroma.User.HashtagFollow do + use Ecto.Schema + import Ecto.Changeset + + alias Pleroma.User + alias Pleroma.Hashtag + alias Pleroma.Repo + + schema "user_follows_hashtag" do + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + belongs_to(:hashtag, Hashtag) + end + + def changeset(%__MODULE__{} = user_hashtag_follow, attrs) do + user_hashtag_follow + |> cast(attrs, [:user_id, :hashtag_id]) + |> unique_constraint(:hashtag_id, + name: :user_hashtag_follows_user_id_hashtag_id_index, + message: "already following" + ) + |> validate_required([:user_id, :hashtag_id]) + end + + def new(%User{} = user, %Hashtag{} = hashtag) do + %__MODULE__{} + |> changeset(%{user_id: user.id, hashtag_id: hashtag.id}) + |> Repo.insert(on_conflict: :nothing) + end +end diff --git a/priv/repo/migrations/20221203232118_add_user_follows_hashtag.exs b/priv/repo/migrations/20221203232118_add_user_follows_hashtag.exs new file mode 100644 index 000000000..27fff2586 --- /dev/null +++ b/priv/repo/migrations/20221203232118_add_user_follows_hashtag.exs @@ -0,0 +1,12 @@ +defmodule Pleroma.Repo.Migrations.AddUserFollowsHashtag do + use Ecto.Migration + + def change do + create table(:user_follows_hashtag) do + add(:hashtag_id, references(:hashtags)) + add(:user_id, references(:users, type: :uuid, on_delete: :delete_all)) + end + + create(unique_index(:user_follows_hashtag, [:user_id, :hashtag_id])) + end +end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 44763daf7..bf8dc5300 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -2679,4 +2679,84 @@ defmodule Pleroma.UserTest do assert user.ap_id in user3_updated.also_known_as end end + + describe "follow_hashtag/2" do + test "should follow a hashtag" do + user = insert(:user) + hashtag = insert(:hashtag) + + assert {:ok, _} = user |> User.follow_hashtag(hashtag) + + user = + User.get_cached_by_ap_id(user.ap_id) + |> Repo.preload(:followed_hashtags) + + assert user.followed_hashtags |> length() == 1 + assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end) + end + + test "should not follow a hashtag twice" do + user = insert(:user) + hashtag = insert(:hashtag) + + assert {:ok, _} = user |> User.follow_hashtag(hashtag) + + assert {:ok, _} = user |> User.follow_hashtag(hashtag) + + user = + User.get_cached_by_ap_id(user.ap_id) + |> Repo.preload(:followed_hashtags) + + assert user.followed_hashtags |> length() == 1 + assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end) + end + + test "can follow multiple hashtags" do + user = insert(:user) + hashtag = insert(:hashtag) + other_hashtag = insert(:hashtag) + + assert {:ok, _} = user |> User.follow_hashtag(hashtag) + assert {:ok, _} = user |> User.follow_hashtag(other_hashtag) + + user = + User.get_cached_by_ap_id(user.ap_id) + |> Repo.preload(:followed_hashtags) + + assert user.followed_hashtags |> length() == 2 + assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end) + assert other_hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end) + end + end + + describe "unfollow_hashtag/2" do + test "should unfollow a hashtag" do + user = insert(:user) + hashtag = insert(:hashtag) + + assert {:ok, _} = user |> User.follow_hashtag(hashtag) + assert {1, nil} = user |> User.unfollow_hashtag(hashtag) + + user = + User.get_cached_by_ap_id(user.ap_id) + |> Repo.preload(:followed_hashtags) + + assert user.followed_hashtags |> length() == 0 + end + + test "should not error when trying to unfollow a hashtag twice" do + user = insert(:user) + hashtag = insert(:hashtag) + + assert {:ok, _} = user |> User.follow_hashtag(hashtag) + assert {1, nil} = user |> User.unfollow_hashtag(hashtag) + assert {0, nil} = user |> User.unfollow_hashtag(hashtag) + + user = + User.get_cached_by_ap_id(user.ap_id) + |> Repo.preload(:followed_hashtags) + + assert user.followed_hashtags |> length() == 0 + end + end end diff --git a/test/support/factory.ex b/test/support/factory.ex index 6ce4decbc..808f8f887 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -716,4 +716,11 @@ defmodule Pleroma.Factory do user: user } end + + def hashtag_factory(params \\ %{}) do + %Pleroma.Hashtag{ + name: "test #{sequence(:hashtag_name, & &1)}" + } + |> Map.merge(params) + end end