From c2e415143b1dfe5d89eff06fbce6840c445aa5fa Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sun, 22 Mar 2020 21:51:44 +0300 Subject: [PATCH] WIP: preloading of user relations for timeline/statuses rendering (performance improvement). --- lib/pleroma/user.ex | 6 +- lib/pleroma/user_relationship.ex | 44 ++++++++++++ .../web/mastodon_api/views/account_view.ex | 69 ++++++++++++++++--- .../web/mastodon_api/views/status_view.ex | 60 ++++++++++++++-- 4 files changed, 160 insertions(+), 19 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 12c2ad815..daaa6d86b 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1642,8 +1642,12 @@ def all_superusers do |> Repo.all() end + def muting_reblogs?(%User{} = user, %User{} = target) do + UserRelationship.reblog_mute_exists?(user, target) + end + def showing_reblogs?(%User{} = user, %User{} = target) do - not UserRelationship.reblog_mute_exists?(user, target) + not muting_reblogs?(user, target) end @doc """ diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index 393947942..167a3919c 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -8,6 +8,7 @@ defmodule Pleroma.UserRelationship do import Ecto.Changeset import Ecto.Query + alias FlakeId.Ecto.CompatType alias Pleroma.Repo alias Pleroma.User alias Pleroma.UserRelationship @@ -34,6 +35,10 @@ def unquote(:"#{relationship_type}_exists?")(source, target), do: exists?(unquote(relationship_type), source, target) end + def user_relationship_types, do: Keyword.keys(user_relationship_mappings()) + + def user_relationship_mappings, do: UserRelationshipTypeEnum.__enum_map__() + def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do user_relationship |> cast(params, [:relationship_type, :source_id, :target_id]) @@ -72,6 +77,45 @@ def delete(relationship_type, %User{} = source, %User{} = target) do end end + def dictionary( + source_users, + target_users, + source_to_target_rel_types \\ nil, + target_to_source_rel_types \\ nil + ) + when is_list(source_users) and is_list(target_users) do + get_bin_ids = fn user -> + with {:ok, bin_id} <- CompatType.dump(user.id), do: bin_id + end + + source_user_ids = Enum.map(source_users, &get_bin_ids.(&1)) + target_user_ids = Enum.map(target_users, &get_bin_ids.(&1)) + + get_rel_type_codes = fn rel_type -> user_relationship_mappings()[rel_type] end + + source_to_target_rel_types = + Enum.map(source_to_target_rel_types || user_relationship_types(), &get_rel_type_codes.(&1)) + + target_to_source_rel_types = + Enum.map(target_to_source_rel_types || user_relationship_types(), &get_rel_type_codes.(&1)) + + __MODULE__ + |> where( + fragment( + "(source_id = ANY(?) AND target_id = ANY(?) AND relationship_type = ANY(?)) OR \ + (source_id = ANY(?) AND target_id = ANY(?) AND relationship_type = ANY(?))", + ^source_user_ids, + ^target_user_ids, + ^source_to_target_rel_types, + ^target_user_ids, + ^source_user_ids, + ^target_to_source_rel_types + ) + ) + |> select([ur], [ur.relationship_type, ur.source_id, ur.target_id]) + |> Repo.all() + end + defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do changeset |> validate_change(:target_id, fn _, target_id -> diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 4ebce73b4..15a579278 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -10,6 +10,19 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MediaProxy + def test_rel(user_relationships, rel_type, source, target, func) do + cond do + is_nil(source) or is_nil(target) -> + false + + user_relationships -> + [rel_type, source.id, target.id] in user_relationships + + true -> + func.(source, target) + end + end + def render("index.json", %{users: users} = opts) do users |> render_many(AccountView, "show.json", opts) @@ -35,21 +48,50 @@ def render("relationship.json", %{user: nil, target: _target}) do %{} end - def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do - follow_state = User.get_follow_state(user, target) + def render( + "relationship.json", + %{user: %User{} = reading_user, target: %User{} = target} = opts + ) do + user_relationships = Map.get(opts, :user_relationships) + follow_state = User.get_follow_state(reading_user, target) + + # TODO: add a note on adjusting StatusView.user_relationships_opt/1 re: preloading of user relations %{ id: to_string(target.id), following: follow_state == "accept", - followed_by: User.following?(target, user), - blocking: User.blocks_user?(user, target), - blocked_by: User.blocks_user?(target, user), - muting: User.mutes?(user, target), - muting_notifications: User.muted_notifications?(user, target), - subscribing: User.subscribed_to?(user, target), + followed_by: User.following?(target, reading_user), + blocking: + test_rel(user_relationships, :block, reading_user, target, &User.blocks_user?(&1, &2)), + blocked_by: + test_rel(user_relationships, :block, target, reading_user, &User.blocks_user?(&1, &2)), + muting: test_rel(user_relationships, :mute, reading_user, target, &User.mutes?(&1, &2)), + muting_notifications: + test_rel( + user_relationships, + :notification_mute, + reading_user, + target, + &User.muted_notifications?(&1, &2) + ), + subscribing: + test_rel( + user_relationships, + :inverse_subscription, + target, + reading_user, + &User.subscribed_to?(&2, &1) + ), requested: follow_state == "pending", - domain_blocking: User.blocks_domain?(user, target), - showing_reblogs: User.showing_reblogs?(user, target), + domain_blocking: User.blocks_domain?(reading_user, target), + showing_reblogs: + not test_rel( + user_relationships, + :reblog_mute, + reading_user, + target, + &User.muting_reblogs?(&1, &2) + ), endorsed: false } end @@ -93,7 +135,12 @@ defp do_render("show.json", %{user: user} = opts) do } end) - relationship = render("relationship.json", %{user: opts[:for], target: user}) + relationship = + render("relationship.json", %{ + user: opts[:for], + target: user, + user_relationships: opts[:user_relationships] + }) %{ id: to_string(user.id), diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index f7469cdff..e0c368ec9 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User + alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView @@ -70,11 +71,34 @@ defp reblogged?(activity, user) do present?(user && user.ap_id in (object.data["announcements"] || [])) end - def render("index.json", opts) do - replied_to_activities = get_replied_to_activities(opts.activities) - opts = Map.put(opts, :replied_to_activities, replied_to_activities) + defp user_relationships_opt(opts) do + reading_user = opts[:for] - safe_render_many(opts.activities, StatusView, "show.json", opts) + if reading_user do + activities = opts[:activities] + actors = Enum.map(activities, fn a -> get_user(a.data["actor"]) end) + + UserRelationship.dictionary( + [reading_user], + actors, + [:block, :mute, :notification_mute, :reblog_mute], + [:block, :inverse_subscription] + ) + else + [] + end + end + + def render("index.json", opts) do + activities = opts.activities + replied_to_activities = get_replied_to_activities(activities) + + opts = + opts + |> Map.put(:replied_to_activities, replied_to_activities) + |> Map.put(:user_relationships, user_relationships_opt(opts)) + + safe_render_many(activities, StatusView, "show.json", opts) end def render( @@ -107,7 +131,12 @@ def render( id: to_string(activity.id), uri: activity_object.data["id"], url: activity_object.data["id"], - account: AccountView.render("show.json", %{user: user, for: opts[:for]}), + account: + AccountView.render("show.json", %{ + user: user, + for: opts[:for], + user_relationships: opts[:user_relationships] + }), in_reply_to_id: nil, in_reply_to_account_id: nil, reblog: reblogged, @@ -253,11 +282,28 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} _ -> [] end + user_relationships_opt = opts[:user_relationships] + + muted = + thread_muted? || + Pleroma.Web.MastodonAPI.AccountView.test_rel( + user_relationships_opt, + :mute, + opts[:for], + user, + fn for_user, user -> User.mutes?(for_user, user) end + ) + %{ id: to_string(activity.id), uri: object.data["id"], url: url, - account: AccountView.render("show.json", %{user: user, for: opts[:for]}), + account: + AccountView.render("show.json", %{ + user: user, + for: opts[:for], + user_relationships: user_relationships_opt + }), in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), reblog: nil, @@ -270,7 +316,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} reblogged: reblogged?(activity, opts[:for]), favourited: present?(favorited), bookmarked: present?(bookmarked), - muted: thread_muted? || User.mutes?(opts[:for], user), + muted: muted, pinned: pinned?(activity, user), sensitive: sensitive, spoiler_text: summary,