ActivityPub actions & side-effects in transaction
[akkoma] / lib / pleroma / user.ex
index 383402447a09d725061ddf830323dd9a0e7ae05c..55c164272d6baab662982956afde88970e4b77ce 100644 (file)
@@ -12,6 +12,7 @@ defmodule Pleroma.User do
   alias Comeonin.Pbkdf2
   alias Ecto.Multi
   alias Pleroma.Activity
   alias Comeonin.Pbkdf2
   alias Ecto.Multi
   alias Pleroma.Activity
+  alias Pleroma.Config
   alias Pleroma.Conversation.Participation
   alias Pleroma.Delivery
   alias Pleroma.FollowingRelationship
   alias Pleroma.Conversation.Participation
   alias Pleroma.Delivery
   alias Pleroma.FollowingRelationship
@@ -35,7 +36,7 @@ defmodule Pleroma.User do
   require Logger
 
   @type t :: %__MODULE__{}
   require Logger
 
   @type t :: %__MODULE__{}
-
+  @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
   @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
 
   # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
   @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
 
   # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
@@ -125,15 +126,15 @@ defmodule Pleroma.User do
     field(:raw_fields, {:array, :map}, default: [])
     field(:discoverable, :boolean, default: false)
     field(:invisible, :boolean, default: false)
     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(:skip_thread_containment, :boolean, default: false)
+    field(:actor_type, :string, default: "Person")
+    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)
     )
 
     has_many(:notifications, Notification)
@@ -167,8 +168,6 @@ defmodule Pleroma.User do
       has_many(incoming_relation_source, through: [incoming_relation, :source])
     end
 
       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)
     # `:blocks` is deprecated (replaced with `blocked_users` relation)
     field(:blocks, {:array, :string}, default: [])
     # `:mutes` is deprecated (replaced with `muted_users` relation)
@@ -218,14 +217,21 @@ defmodule Pleroma.User do
     end
   end
 
     end
   end
 
-  @doc "Returns if the user should be allowed to authenticate"
-  def auth_active?(%User{deactivated: true}), do: false
+  @doc "Returns status account"
+  @spec account_status(User.t()) :: account_status()
+  def account_status(%User{deactivated: true}), do: :deactivated
+  def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
 
 
-  def auth_active?(%User{confirmation_pending: true}),
-    do: !Pleroma.Config.get([:instance, :account_activation_required])
+  def account_status(%User{confirmation_pending: true}) do
+    case Config.get([:instance, :account_activation_required]) do
+      true -> :confirmation_pending
+      _ -> :active
+    end
+  end
 
 
-  def auth_active?(%User{}), do: true
+  def account_status(%User{}), do: :active
 
 
+  @spec visible_for?(User.t(), User.t() | nil) :: boolean()
   def visible_for?(user, for_user \\ nil)
 
   def visible_for?(%User{invisible: true}, _), do: false
   def visible_for?(user, for_user \\ nil)
 
   def visible_for?(%User{invisible: true}, _), do: false
@@ -233,15 +239,17 @@ defmodule Pleroma.User do
   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
   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
-    auth_active?(user) || superuser?(for_user)
+    account_status(user) == :active || superuser?(for_user)
   end
 
   def visible_for?(_, _), do: false
 
   end
 
   def visible_for?(_, _), do: false
 
+  @spec superuser?(User.t()) :: boolean()
   def superuser?(%User{local: true, is_admin: true}), do: true
   def superuser?(%User{local: true, is_moderator: true}), do: true
   def superuser?(_), do: false
 
   def superuser?(%User{local: true, is_admin: true}), do: true
   def superuser?(%User{local: true, is_moderator: true}), do: true
   def superuser?(_), do: false
 
+  @spec invisible?(User.t()) :: boolean()
   def invisible?(%User{invisible: true}), do: true
   def invisible?(_), do: false
 
   def invisible?(%User{invisible: true}), do: true
   def invisible?(_), do: false
 
@@ -272,20 +280,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 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)
-    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,
-      follower_count: follower_count,
-      following_count: following_count
-    }
-  end
-
   def follow_state(%User{} = user, %User{} = target) do
     case Utils.fetch_latest_follow(user, target) do
       %{data: %{"state" => state}} -> state
   def follow_state(%User{} = user, %User{} = target) do
     case Utils.fetch_latest_follow(user, target) do
       %{data: %{"state" => state}} -> state
@@ -304,10 +298,6 @@ defmodule Pleroma.User do
     Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
   end
 
     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)
   @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
   def restrict_deactivated(query) do
     from(u in query, where: u.deactivated != ^true)
@@ -338,7 +328,6 @@ defmodule Pleroma.User do
 
     params =
       params
 
     params =
       params
-      |> Map.put(:info, params[:info] || %{})
       |> truncate_if_exists(:name, name_limit)
       |> truncate_if_exists(:bio, bio_limit)
       |> truncate_fields_param()
       |> truncate_if_exists(:name, name_limit)
       |> truncate_if_exists(:bio, bio_limit)
       |> truncate_fields_param()
@@ -367,7 +356,9 @@ defmodule Pleroma.User do
           :fields,
           :following_count,
           :discoverable,
           :fields,
           :following_count,
           :discoverable,
-          :invisible
+          :invisible,
+          :actor_type,
+          :also_known_as
         ]
       )
       |> validate_required([:name, :ap_id])
         ]
       )
       |> validate_required([:name, :ap_id])
@@ -409,13 +400,16 @@ defmodule Pleroma.User do
         :hide_followers_count,
         :hide_follows_count,
         :hide_favorites,
         :hide_followers_count,
         :hide_follows_count,
         :hide_favorites,
+        :allow_following_move,
         :background,
         :show_role,
         :skip_thread_containment,
         :fields,
         :raw_fields,
         :pleroma_settings_store,
         :background,
         :show_role,
         :skip_thread_containment,
         :fields,
         :raw_fields,
         :pleroma_settings_store,
-        :discoverable
+        :discoverable,
+        :actor_type,
+        :also_known_as
       ]
     )
     |> unique_constraint(:nickname)
       ]
     )
     |> unique_constraint(:nickname)
@@ -453,9 +447,12 @@ defmodule Pleroma.User do
         :hide_follows,
         :fields,
         :hide_followers,
         :hide_follows,
         :fields,
         :hide_followers,
+        :allow_following_move,
         :discoverable,
         :hide_followers_count,
         :discoverable,
         :hide_followers_count,
-        :hide_follows_count
+        :hide_follows_count,
+        :actor_type,
+        :also_known_as
       ]
     )
     |> unique_constraint(:nickname)
       ]
     )
     |> unique_constraint(:nickname)
@@ -586,6 +583,10 @@ defmodule Pleroma.User do
     end
   end
 
     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
   def needs_update?(%User{local: true}), do: false
 
   def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
@@ -646,25 +647,48 @@ defmodule Pleroma.User do
     end
   end
 
     end
   end
 
+  def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
+    {:error, "Not subscribed!"}
+  end
+
   def unfollow(%User{} = follower, %User{} = followed) do
   def unfollow(%User{} = follower, %User{} = followed) do
-    if following?(follower, followed) and follower.ap_id != followed.ap_id do
-      FollowingRelationship.unfollow(follower, followed)
+    case get_follow_state(follower, followed) do
+      state when state in ["accept", "pending"] ->
+        FollowingRelationship.unfollow(follower, followed)
+        {:ok, followed} = update_follower_count(followed)
 
 
-      {:ok, followed} = update_follower_count(followed)
+        {:ok, follower} =
+          follower
+          |> update_following_count()
+          |> set_cache()
 
 
-      {:ok, follower} =
-        follower
-        |> update_following_count()
-        |> set_cache()
+        {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
 
 
-      {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
-    else
-      {:error, "Not subscribed!"}
+      nil ->
+        {:error, "Not subscribed!"}
     end
   end
 
   defdelegate following?(follower, followed), to: FollowingRelationship
 
     end
   end
 
   defdelegate following?(follower, followed), to: FollowingRelationship
 
+  def get_follow_state(%User{} = follower, %User{} = following) do
+    following_relationship = FollowingRelationship.get(follower, following)
+
+    case {following_relationship, following.local} do
+      {nil, false} ->
+        case Utils.fetch_latest_follow(follower, following) do
+          %{data: %{"state" => state}} when state in ["pending", "accept"] -> state
+          _ -> nil
+        end
+
+      {%{state: state}, _} ->
+        state
+
+      {nil, _} ->
+        nil
+    end
+  end
+
   def locked?(%User{} = user) do
     user.locked || false
   end
   def locked?(%User{} = user) do
     user.locked || false
   end
@@ -705,7 +729,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)
   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
 
     {:ok, user}
   end
 
@@ -724,12 +747,20 @@ 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}")
   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
 
   end
 
+  @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
   def get_cached_by_ap_id(ap_id) do
     key = "ap_id:#{ap_id}"
   def get_cached_by_ap_id(ap_id) do
     key = "ap_id:#{ap_id}"
-    Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
+
+    with {:ok, nil} <- Cachex.get(:user_cache, key),
+         user when not is_nil(user) <- get_by_ap_id(ap_id),
+         {:ok, true} <- Cachex.put(:user_cache, key, user) do
+      user
+    else
+      {:ok, user} -> user
+      nil -> nil
+    end
   end
 
   def get_cached_by_id(id) do
   end
 
   def get_cached_by_id(id) do
@@ -792,11 +823,6 @@ defmodule Pleroma.User do
     get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
   end
 
     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
   def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
 
   def get_or_fetch_by_nickname(nickname) do
@@ -836,14 +862,14 @@ defmodule Pleroma.User do
   @spec get_followers_query(User.t()) :: Ecto.Query.t()
   def get_followers_query(user), do: get_followers_query(user, nil)
 
   @spec get_followers_query(User.t()) :: Ecto.Query.t()
   def get_followers_query(user), do: get_followers_query(user, nil)
 
-  @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
+  @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
   def get_followers(user, page \\ nil) do
     user
     |> get_followers_query(page)
     |> Repo.all()
   end
 
   def get_followers(user, page \\ nil) do
     user
     |> get_followers_query(page)
     |> Repo.all()
   end
 
-  @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
+  @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
   def get_external_followers(user, page \\ nil) do
     user
     |> get_followers_query(page)
   def get_external_followers(user, page \\ nil) do
     user
     |> get_followers_query(page)
@@ -878,6 +904,13 @@ defmodule Pleroma.User do
     |> Repo.all()
   end
 
     |> Repo.all()
   end
 
+  def get_friends_ap_ids(user) do
+    user
+    |> get_friends_query(nil)
+    |> select([u], u.ap_id)
+    |> Repo.all()
+  end
+
   def get_friends_ids(user, page \\ nil) do
     user
     |> get_friends_query(page)
   def get_friends_ids(user, page \\ nil) do
     user
     |> get_friends_query(page)
@@ -1152,7 +1185,8 @@ defmodule Pleroma.User do
   def blocks?(nil, _), do: false
 
   def blocks?(%User{} = user, %User{} = target) do
   def blocks?(nil, _), do: false
 
   def blocks?(%User{} = user, %User{} = target) do
-    blocks_user?(user, target) || blocks_domain?(user, target)
+    blocks_user?(user, target) ||
+      (!User.following?(user, target) && blocks_domain?(user, target))
   end
 
   def blocks_user?(%User{} = user, %User{} = target) do
   end
 
   def blocks_user?(%User{} = user, %User{} = target) do
@@ -1238,20 +1272,9 @@ defmodule Pleroma.User do
   end
 
   def update_notification_settings(%User{} = user, settings) 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
     user
-    |> cast(params, [:notification_settings])
+    |> cast(%{notification_settings: settings}, [])
+    |> cast_embed(:notification_settings)
     |> validate_required([:notification_settings])
     |> update_and_set_cache()
   end
     |> validate_required([:notification_settings])
     |> update_and_set_cache()
   end
@@ -1290,7 +1313,6 @@ defmodule Pleroma.User do
     Repo.delete(user)
   end
 
     Repo.delete(user)
   end
 
-  @spec perform(atom(), User.t()) :: {:ok, User.t()}
   def perform(:fetch_initial_posts, %User{} = user) do
     pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
 
   def perform(:fetch_initial_posts, %User{} = user) do
     pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
 
@@ -1322,7 +1344,6 @@ defmodule Pleroma.User do
     )
   end
 
     )
   end
 
-  @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
   def perform(:follow_import, %User{} = follower, followed_identifiers)
       when is_list(followed_identifiers) do
     Enum.map(
   def perform(:follow_import, %User{} = follower, followed_identifiers)
       when is_list(followed_identifiers) do
     Enum.map(
@@ -1354,7 +1375,7 @@ defmodule Pleroma.User do
   def external_users(opts \\ []) do
     query =
       external_users_query()
   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],
 
     query =
       if opts[:max_id],
@@ -1449,20 +1470,47 @@ defmodule Pleroma.User do
   Creates an internal service actor by URI if missing.
   Optionally takes nickname for addressing.
   """
   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 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()
+  @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
+  def get_or_create_service_actor_by_ap_id(uri, nickname) do
+    {_, user} =
+      case get_cached_by_ap_id(uri) do
+        nil ->
+          with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
+            Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
+            {:error, nil}
+          end
 
 
-      user
-    end
+        %User{invisible: false} = user ->
+          set_invisible(user)
+
+        user ->
+          {:ok, user}
+      end
+
+    user
+  end
+
+  @spec set_invisible(User.t()) :: {:ok, User.t()}
+  defp set_invisible(user) do
+    user
+    |> change(%{invisible: true})
+    |> update_and_set_cache()
+  end
+
+  @spec create_service_actor(String.t(), String.t()) ::
+          {:ok, User.t()} | {:error, Ecto.Changeset.t()}
+  defp create_service_actor(uri, nickname) do
+    %User{
+      invisible: true,
+      local: true,
+      ap_id: uri,
+      nickname: nickname,
+      follower_address: uri <> "/followers"
+    }
+    |> change
+    |> unique_constraint(:nickname)
+    |> Repo.insert()
+    |> set_cache()
   end
 
   # AP style
   end
 
   # AP style
@@ -1494,7 +1542,7 @@ defmodule Pleroma.User do
     data
     |> Map.put(:name, blank?(data[:name]) || data[:nickname])
     |> remote_user_creation()
     data
     |> Map.put(:name, blank?(data[:name]) || data[:nickname])
     |> remote_user_creation()
-    |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
+    |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
     |> set_cache()
   end
 
     |> set_cache()
   end
 
@@ -1704,6 +1752,11 @@ defmodule Pleroma.User do
     |> update_and_set_cache()
   end
 
     |> 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
   def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
     mascot
   end
@@ -1870,6 +1923,12 @@ defmodule Pleroma.User do
     |> update_and_set_cache()
   end
 
     |> update_and_set_cache()
   end
 
+  @doc "Signs user out of all applications"
+  def global_sign_out(user) do
+    OAuth.Authorization.delete_user_authorizations(user)
+    OAuth.Token.delete_user_tokens(user)
+  end
+
   def mascot_update(user, url) do
     user
     |> cast(%{mascot: url}, [:mascot])
   def mascot_update(user, url) do
     user
     |> cast(%{mascot: url}, [:mascot])