Merge branch 'develop' into issue/1342
[akkoma] / lib / pleroma / user.ex
index 615f1b72575e7b51996d7f3096b1333fe4aa7cd8..e2afc6de8215b3555f41a8f44b56984e388ecac5 100644 (file)
@@ -44,10 +44,17 @@ defmodule Pleroma.User do
   @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
   @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
 
+  # AP ID user relationships (blocks, mutes etc.)
   # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
   @user_relationships_config [
-    block: [blocker_blocks: :blocked_users, blockee_blocks: :blocker_users],
-    mute: [muter_mutes: :muted_users, mutee_mutes: :muter_users],
+    block: [
+      blocker_blocks: :blocked_users,
+      blockee_blocks: :blocker_users
+    ],
+    mute: [
+      muter_mutes: :muted_users,
+      mutee_mutes: :muter_users
+    ],
     reblog_mute: [
       reblog_muter_mutes: :reblog_muted_users,
       reblog_mutee_mutes: :reblog_muter_users
@@ -55,6 +62,11 @@ defmodule Pleroma.User do
     notification_mute: [
       notification_muter_mutes: :notification_muted_users,
       notification_mutee_mutes: :notification_muter_users
+    ],
+    # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
+    inverse_subscription: [
+      subscribee_subscriptions: :subscriber_users,
+      subscriber_subscriptions: :subscribee_users
     ]
   ]
 
@@ -82,15 +94,13 @@ defmodule Pleroma.User do
     field(:source_data, :map, default: %{})
     field(:note_count, :integer, default: 0)
     field(:follower_count, :integer, default: 0)
-    # Should be filled in only for remote users
-    field(:following_count, :integer, default: nil)
+    field(:following_count, :integer, default: 0)
     field(:locked, :boolean, default: false)
     field(:confirmation_pending, :boolean, default: false)
     field(:password_reset_pending, :boolean, default: false)
     field(:confirmation_token, :string, default: nil)
     field(:default_scope, :string, default: "public")
     field(:domain_blocks, {:array, :string}, default: [])
-    field(:subscribers, {:array, :string}, default: [])
     field(:deactivated, :boolean, default: false)
     field(:no_rich_text, :boolean, default: false)
     field(:ap_enabled, :boolean, default: false)
@@ -115,15 +125,14 @@ defmodule Pleroma.User do
     field(:raw_fields, {:array, :map}, default: [])
     field(:discoverable, :boolean, default: false)
     field(:invisible, :boolean, default: false)
+    field(:allow_following_move, :boolean, default: true)
     field(:skip_thread_containment, :boolean, default: false)
+    field(:also_known_as, {:array, :string}, default: [])
 
-    field(:notification_settings, :map,
-      default: %{
-        "followers" => true,
-        "follows" => true,
-        "non_follows" => true,
-        "non_followers" => true
-      }
+    embeds_one(
+      :notification_settings,
+      Pleroma.User.NotificationSetting,
+      on_replace: :update
     )
 
     has_many(:notifications, Notification)
@@ -157,8 +166,6 @@ defmodule Pleroma.User do
       has_many(incoming_relation_source, through: [incoming_relation, :source])
     end
 
-    field(:info, :map, default: %{})
-
     # `:blocks` is deprecated (replaced with `blocked_users` relation)
     field(:blocks, {:array, :string}, default: [])
     # `:mutes` is deprecated (replaced with `muted_users` relation)
@@ -167,6 +174,8 @@ defmodule Pleroma.User do
     field(:muted_reblogs, {:array, :string}, default: [])
     # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
     field(:muted_notifications, {:array, :string}, default: [])
+    # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
+    field(:subscribers, {:array, :string}, default: [])
 
     timestamps()
   end
@@ -216,6 +225,8 @@ defmodule Pleroma.User do
 
   def visible_for?(user, for_user \\ nil)
 
+  def visible_for?(%User{invisible: true}, _), do: false
+
   def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
 
   def visible_for?(%User{} = user, for_user) do
@@ -258,22 +269,6 @@ defmodule Pleroma.User do
   def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
   def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
 
-  def user_info(%User{} = user, args \\ %{}) do
-    following_count =
-      Map.get(args, :following_count, user.following_count || following_count(user))
-
-    follower_count = Map.get(args, :follower_count, user.follower_count)
-
-    %{
-      note_count: user.note_count,
-      locked: user.locked,
-      confirmation_pending: user.confirmation_pending,
-      default_scope: user.default_scope
-    }
-    |> Map.put(:following_count, following_count)
-    |> Map.put(:follower_count, follower_count)
-  end
-
   def follow_state(%User{} = user, %User{} = target) do
     case Utils.fetch_latest_follow(user, target) do
       %{data: %{"state" => state}} -> state
@@ -292,10 +287,6 @@ defmodule Pleroma.User do
     Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
   end
 
-  def set_info_cache(user, args) do
-    Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
-  end
-
   @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
   def restrict_deactivated(query) do
     from(u in query, where: u.deactivated != ^true)
@@ -326,7 +317,6 @@ defmodule Pleroma.User do
 
     params =
       params
-      |> Map.put(:info, params[:info] || %{})
       |> truncate_if_exists(:name, name_limit)
       |> truncate_if_exists(:bio, bio_limit)
       |> truncate_fields_param()
@@ -355,7 +345,8 @@ defmodule Pleroma.User do
           :fields,
           :following_count,
           :discoverable,
-          :invisible
+          :invisible,
+          :also_known_as
         ]
       )
       |> validate_required([:name, :ap_id])
@@ -397,13 +388,15 @@ defmodule Pleroma.User do
         :hide_followers_count,
         :hide_follows_count,
         :hide_favorites,
+        :allow_following_move,
         :background,
         :show_role,
         :skip_thread_containment,
         :fields,
         :raw_fields,
         :pleroma_settings_store,
-        :discoverable
+        :discoverable,
+        :also_known_as
       ]
     )
     |> unique_constraint(:nickname)
@@ -441,9 +434,11 @@ defmodule Pleroma.User do
         :hide_follows,
         :fields,
         :hide_followers,
+        :allow_following_move,
         :discoverable,
         :hide_followers_count,
-        :hide_follows_count
+        :hide_follows_count,
+        :also_known_as
       ]
     )
     |> unique_constraint(:nickname)
@@ -574,6 +569,10 @@ defmodule Pleroma.User do
     end
   end
 
+  def try_send_confirmation_email(users) do
+    Enum.each(users, &try_send_confirmation_email/1)
+  end
+
   def needs_update?(%User{local: true}), do: false
 
   def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
@@ -604,14 +603,9 @@ defmodule Pleroma.User do
   @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
   @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
   def follow_all(follower, followeds) do
-    followeds =
-      Enum.reject(followeds, fn followed ->
-        blocks?(follower, followed) || blocks?(followed, follower)
-      end)
-
-    Enum.each(followeds, &follow(follower, &1, "accept"))
-
-    Enum.each(followeds, &update_follower_count/1)
+    followeds
+    |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
+    |> Enum.each(&follow(follower, &1, "accept"))
 
     set_cache(follower)
   end
@@ -631,11 +625,11 @@ defmodule Pleroma.User do
       true ->
         FollowingRelationship.follow(follower, followed, state)
 
-        follower = maybe_update_following_count(follower)
-
         {:ok, _} = update_follower_count(followed)
 
-        set_cache(follower)
+        follower
+        |> update_following_count()
+        |> set_cache()
     end
   end
 
@@ -643,11 +637,12 @@ defmodule Pleroma.User do
     if following?(follower, followed) and follower.ap_id != followed.ap_id do
       FollowingRelationship.unfollow(follower, followed)
 
-      follower = maybe_update_following_count(follower)
-
       {:ok, followed} = update_follower_count(followed)
 
-      set_cache(follower)
+      {:ok, follower} =
+        follower
+        |> update_following_count()
+        |> set_cache()
 
       {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
     else
@@ -697,7 +692,6 @@ defmodule Pleroma.User do
   def set_cache(%User{} = user) do
     Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
     Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
-    Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
     {:ok, user}
   end
 
@@ -716,7 +710,6 @@ defmodule Pleroma.User do
   def invalidate_cache(user) do
     Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
     Cachex.del(:user_cache, "nickname:#{user.nickname}")
-    Cachex.del(:user_cache, "user_info:#{user.id}")
   end
 
   def get_cached_by_ap_id(ap_id) do
@@ -784,11 +777,6 @@ defmodule Pleroma.User do
     get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
   end
 
-  def get_cached_user_info(user) do
-    key = "user_info:#{user.id}"
-    Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
-  end
-
   def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
 
   def get_or_fetch_by_nickname(nickname) do
@@ -977,8 +965,8 @@ defmodule Pleroma.User do
     end
   end
 
-  @spec maybe_update_following_count(User.t()) :: User.t()
-  def maybe_update_following_count(%User{local: false} = user) do
+  @spec update_following_count(User.t()) :: User.t()
+  def update_following_count(%User{local: false} = user) do
     if Pleroma.Config.get([:instance, :external_user_synchronization]) do
       maybe_fetch_follow_information(user)
     else
@@ -986,7 +974,13 @@ defmodule Pleroma.User do
     end
   end
 
-  def maybe_update_following_count(user), do: user
+  def update_following_count(%User{local: true} = user) do
+    following_count = FollowingRelationship.following_count(user)
+
+    user
+    |> follow_information_changeset(%{following_count: following_count})
+    |> Repo.update!()
+  end
 
   def set_unread_conversation_count(%User{local: true} = user) do
     unread_query = Participation.unread_conversation_count_for_user(user)
@@ -1046,33 +1040,43 @@ defmodule Pleroma.User do
 
   @spec mute(User.t(), User.t(), boolean()) ::
           {:ok, list(UserRelationship.t())} | {:error, String.t()}
-  def mute(muter, %User{} = mutee, notifications? \\ true) do
+  def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
     add_to_mutes(muter, mutee, notifications?)
   end
 
-  def unmute(muter, %User{} = mutee) do
+  def unmute(%User{} = muter, %User{} = mutee) do
     remove_from_mutes(muter, mutee)
   end
 
-  def subscribe(subscriber, %{ap_id: ap_id}) do
-    with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
-      deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
+  def subscribe(%User{} = subscriber, %User{} = target) do
+    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
 
-      if blocks?(subscribed, subscriber) and deny_follow_blocked do
-        {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
-      else
-        User.add_to_subscribers(subscribed, subscriber.ap_id)
-      end
+    if blocks?(target, subscriber) and deny_follow_blocked do
+      {:error, "Could not subscribe: #{target.nickname} is blocking you"}
+    else
+      # Note: the relationship is inverse: subscriber acts as relationship target
+      UserRelationship.create_inverse_subscription(target, subscriber)
     end
   end
 
-  def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
+  def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
+    with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
+      subscribe(subscriber, subscribee)
+    end
+  end
+
+  def unsubscribe(%User{} = unsubscriber, %User{} = target) do
+    # Note: the relationship is inverse: subscriber acts as relationship target
+    UserRelationship.delete_inverse_subscription(target, unsubscriber)
+  end
+
+  def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
     with %User{} = user <- get_cached_by_ap_id(ap_id) do
-      User.remove_from_subscribers(user, unsubscriber.ap_id)
+      unsubscribe(unsubscriber, user)
     end
   end
 
-  def block(blocker, %User{} = blocked) do
+  def block(%User{} = blocker, %User{} = blocked) do
     # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
     blocker =
       if following?(blocker, blocked) do
@@ -1089,13 +1093,7 @@ defmodule Pleroma.User do
         nil -> blocked
       end
 
-    blocker =
-      if subscribed_to?(blocked, blocker) do
-        {:ok, blocker} = unsubscribe(blocked, blocker)
-        blocker
-      else
-        blocker
-      end
+    unsubscribe(blocked, blocker)
 
     if following?(blocked, blocker), do: unfollow(blocked, blocker)
 
@@ -1105,16 +1103,16 @@ defmodule Pleroma.User do
   end
 
   # helper to handle the block given only an actor's AP id
-  def block(blocker, %{ap_id: ap_id}) do
+  def block(%User{} = blocker, %{ap_id: ap_id}) do
     block(blocker, get_cached_by_ap_id(ap_id))
   end
 
-  def unblock(blocker, %User{} = blocked) do
+  def unblock(%User{} = blocker, %User{} = blocked) do
     remove_from_block(blocker, blocked)
   end
 
   # helper to handle the block given only an actor's AP id
-  def unblock(blocker, %{ap_id: ap_id}) do
+  def unblock(%User{} = blocker, %{ap_id: ap_id}) do
     unblock(blocker, get_cached_by_ap_id(ap_id))
   end
 
@@ -1128,7 +1126,7 @@ defmodule Pleroma.User do
   @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
   def muted_notifications?(nil, _), do: false
 
-  def muted_notifications?(user, %User{} = target),
+  def muted_notifications?(%User{} = user, %User{} = target),
     do: UserRelationship.notification_mute_exists?(user, target)
 
   def blocks?(nil, _), do: false
@@ -1151,18 +1149,26 @@ defmodule Pleroma.User do
 
   def blocks_domain?(_, _), do: false
 
-  def subscribed_to?(user, %{ap_id: ap_id}) do
+  def subscribed_to?(%User{} = user, %User{} = target) do
+    # Note: the relationship is inverse: subscriber acts as relationship target
+    UserRelationship.inverse_subscription_exists?(target, user)
+  end
+
+  def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
     with %User{} = target <- get_cached_by_ap_id(ap_id) do
-      Enum.member?(target.subscribers, user.ap_id)
+      subscribed_to?(user, target)
     end
   end
 
   @doc """
-  Returns map of related AP IDs list by relation type.
-  E.g. `related_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
+  Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type.
+  E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
   """
-  @spec related_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
-  def related_ap_ids(%User{} = user, relationship_types) when is_list(relationship_types) do
+  @spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
+  def outgoing_relations_ap_ids(_, []), do: %{}
+
+  def outgoing_relations_ap_ids(%User{} = user, relationship_types)
+      when is_list(relationship_types) do
     db_result =
       user
       |> assoc(:outgoing_relationships)
@@ -1180,12 +1186,6 @@ defmodule Pleroma.User do
     )
   end
 
-  @spec subscribers(User.t()) :: [User.t()]
-  def subscribers(user) do
-    User.Query.build(%{ap_id: user.subscribers, deactivated: false})
-    |> Repo.all()
-  end
-
   def deactivate_async(user, status \\ true) do
     BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
   end
@@ -1200,7 +1200,12 @@ defmodule Pleroma.User do
 
   def deactivate(%User{} = user, status) do
     with {:ok, user} <- set_activation_status(user, status) do
-      Enum.each(get_followers(user), &invalidate_cache/1)
+      user
+      |> get_followers()
+      |> Enum.filter(& &1.local)
+      |> Enum.each(fn follower ->
+        follower |> update_following_count() |> set_cache()
+      end)
 
       # Only update local user counts, remote will be update during the next pull.
       user
@@ -1213,20 +1218,9 @@ defmodule Pleroma.User do
   end
 
   def update_notification_settings(%User{} = user, settings) do
-    settings =
-      settings
-      |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
-      |> Map.new()
-
-    notification_settings =
-      user.notification_settings
-      |> Map.merge(settings)
-      |> Map.take(["followers", "follows", "non_follows", "non_followers"])
-
-    params = %{notification_settings: notification_settings}
-
     user
-    |> cast(params, [:notification_settings])
+    |> cast(%{notification_settings: settings}, [])
+    |> cast_embed(:notification_settings)
     |> validate_required([:notification_settings])
     |> update_and_set_cache()
   end
@@ -1329,7 +1323,7 @@ defmodule Pleroma.User do
   def external_users(opts \\ []) do
     query =
       external_users_query()
-      |> select([u], struct(u, [:id, :ap_id, :info]))
+      |> select([u], struct(u, [:id, :ap_id]))
 
     query =
       if opts[:max_id],
@@ -1420,22 +1414,23 @@ defmodule Pleroma.User do
     end
   end
 
-  @doc "Creates an internal service actor by URI if missing.  Optionally takes nickname for addressing."
+  @doc """
+  Creates an internal service actor by URI if missing.
+  Optionally takes nickname for addressing.
+  """
   def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
-    with %User{} = user <- get_cached_by_ap_id(uri) do
-      user
-    else
-      _ ->
-        {:ok, user} =
-          %User{}
-          |> cast(%{}, [:ap_id, :nickname, :local])
-          |> put_change(:ap_id, uri)
-          |> put_change(:nickname, nickname)
-          |> put_change(:local, true)
-          |> put_change(:follower_address, uri <> "/followers")
-          |> Repo.insert()
+    with user when is_nil(user) <- get_cached_by_ap_id(uri) do
+      {:ok, user} =
+        %User{
+          invisible: true,
+          local: true,
+          ap_id: uri,
+          nickname: nickname,
+          follower_address: uri <> "/followers"
+        }
+        |> Repo.insert()
 
-        user
+      user
     end
   end
 
@@ -1678,6 +1673,11 @@ defmodule Pleroma.User do
     |> update_and_set_cache()
   end
 
+  @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
+  def toggle_confirmation(users) do
+    Enum.map(users, &toggle_confirmation/1)
+  end
+
   def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
     mascot
   end
@@ -1916,23 +1916,6 @@ defmodule Pleroma.User do
     |> update_and_set_cache()
   end
 
-  defp set_subscribers(user, subscribers) do
-    params = %{subscribers: subscribers}
-
-    user
-    |> cast(params, [:subscribers])
-    |> validate_required([:subscribers])
-    |> update_and_set_cache()
-  end
-
-  def add_to_subscribers(user, subscribed) do
-    set_subscribers(user, Enum.uniq([subscribed | user.subscribers]))
-  end
-
-  def remove_from_subscribers(user, subscribed) do
-    set_subscribers(user, List.delete(user.subscribers, subscribed))
-  end
-
   defp set_domain_blocks(user, domain_blocks) do
     params = %{domain_blocks: domain_blocks}