Merge branch 'develop' into feature/digest-email
[akkoma] / lib / pleroma / user.ex
index 29c87d4a9a26481a57cd9e784ab695711e9752fd..c0e418e2f8c9cdd3dd9ff20e064cfad734c1935c 100644 (file)
@@ -57,6 +57,7 @@ defmodule Pleroma.User do
     field(:search_type, :integer, virtual: true)
     field(:tags, {:array, :string}, default: [])
     field(:last_refreshed_at, :naive_datetime_usec)
+    field(:last_digest_emailed_at, :naive_datetime)
     has_many(:notifications, Notification)
     has_many(:registrations, Registration)
     embeds_one(:info, User.Info)
@@ -1157,19 +1158,18 @@ defmodule Pleroma.User do
     end
   end
 
-  def get_or_create_instance_user do
-    relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
-
-    if user = get_cached_by_ap_id(relay_uri) do
+  @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
+    if user = get_cached_by_ap_id(uri) do
       user
     else
       changes =
         %User{info: %User.Info{}}
         |> cast(%{}, [:ap_id, :nickname, :local])
-        |> put_change(:ap_id, relay_uri)
-        |> put_change(:nickname, nil)
+        |> put_change(:ap_id, uri)
+        |> put_change(:nickname, nickname)
         |> put_change(:local, true)
-        |> put_change(:follower_address, relay_uri <> "/followers")
+        |> put_change(:follower_address, uri <> "/followers")
 
       {:ok, user} = Repo.insert(changes)
       user
@@ -1190,10 +1190,12 @@ defmodule Pleroma.User do
   end
 
   # OStatus Magic Key
-  def public_key_from_info(%{magic_key: magic_key}) do
+  def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
     {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
   end
 
+  def public_key_from_info(_), do: {:error, "not found key"}
+
   def get_public_key_for_ap_id(ap_id) do
     with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
          {:ok, public_key} <- public_key_from_info(user.info) do
@@ -1210,7 +1212,7 @@ defmodule Pleroma.User do
     data
     |> Map.put(:name, blank?(data[:name]) || data[:nickname])
     |> remote_user_creation()
-    |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
+    |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
     |> set_cache()
   end
 
@@ -1346,6 +1348,80 @@ defmodule Pleroma.User do
     target.ap_id not in user.info.muted_reblogs
   end
 
+  @doc """
+  The function returns a query to get users with no activity for given interval of days.
+  Inactive users are those who didn't read any notification, or had any activity where
+  the user is the activity's actor, during `inactivity_threshold` days.
+  Deactivated users will not appear in this list.
+
+  ## Examples
+
+      iex> Pleroma.User.list_inactive_users()
+      %Ecto.Query{}
+  """
+  @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
+  def list_inactive_users_query(inactivity_threshold \\ 7) do
+    negative_inactivity_threshold = -inactivity_threshold
+    now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
+    # Subqueries are not supported in `where` clauses, join gets too complicated.
+    has_read_notifications =
+      from(n in Pleroma.Notification,
+        where: n.seen == true,
+        group_by: n.id,
+        having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
+        select: n.user_id
+      )
+      |> Pleroma.Repo.all()
+
+    from(u in Pleroma.User,
+      left_join: a in Pleroma.Activity,
+      on: u.ap_id == a.actor,
+      where: not is_nil(u.nickname),
+      where: fragment("not (?->'deactivated' @> 'true')", u.info),
+      where: u.id not in ^has_read_notifications,
+      group_by: u.id,
+      having:
+        max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
+          is_nil(max(a.inserted_at))
+    )
+  end
+
+  @doc """
+  Enable or disable email notifications for user
+
+  ## Examples
+
+      iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
+      Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
+
+      iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
+      Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
+  """
+  @spec switch_email_notifications(t(), String.t(), boolean()) ::
+          {:ok, t()} | {:error, Ecto.Changeset.t()}
+  def switch_email_notifications(user, type, status) do
+    info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
+
+    change(user)
+    |> put_embed(:info, info)
+    |> update_and_set_cache()
+  end
+
+  @doc """
+  Set `last_digest_emailed_at` value for the user to current time
+  """
+  @spec touch_last_digest_emailed_at(t()) :: t()
+  def touch_last_digest_emailed_at(user) do
+    now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
+
+    {:ok, updated_user} =
+      user
+      |> change(%{last_digest_emailed_at: now})
+      |> update_and_set_cache()
+
+    updated_user
+  end
+
   @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
   def toggle_confirmation(%User{} = user) do
     need_confirmation? = !user.info.confirmation_pending
@@ -1379,23 +1455,16 @@ defmodule Pleroma.User do
     }
   end
 
-  def ensure_keys_present(user) do
-    info = user.info
-
+  def ensure_keys_present(%User{info: info} = user) do
     if info.keys do
       {:ok, user}
     else
       {:ok, pem} = Keys.generate_rsa_pem()
 
-      info_cng =
-        info
-        |> User.Info.set_keys(pem)
-
-      cng =
-        Ecto.Changeset.change(user)
-        |> Ecto.Changeset.put_embed(:info, info_cng)
-
-      update_and_set_cache(cng)
+      user
+      |> Ecto.Changeset.change()
+      |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
+      |> update_and_set_cache()
     end
   end
 
@@ -1416,4 +1485,8 @@ defmodule Pleroma.User do
   end
 
   defp put_password_hash(changeset), do: changeset
+
+  def is_internal_user?(%User{nickname: nil}), do: true
+  def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
+  def is_internal_user?(_), do: false
 end