Merge develop
authorRoman Chvanikov <chvanikoff@gmail.com>
Wed, 29 May 2019 15:18:22 +0000 (18:18 +0300)
committerRoman Chvanikov <chvanikoff@gmail.com>
Wed, 29 May 2019 15:18:22 +0000 (18:18 +0300)
1  2 
config/config.exs
docs/config.md
lib/pleroma/application.ex
lib/pleroma/notification.ex
lib/pleroma/user.ex
lib/pleroma/user/info.ex
lib/pleroma/web/router.ex
mix.exs
mix.lock
test/support/factory.ex
test/user_test.exs

index f05b9db294f5e9e603036f684d56d2fb18bc7ca5,68168b279665c34e71cc20185a7a6b1216c64ac7..ac7e63956c0ce53d7529aa94c6ed7d254f7a58be
@@@ -472,17 -478,16 +478,24 @@@ config :pleroma, Pleroma.ScheduledActiv
    total_user_limit: 300,
    enabled: true
  
 +config :pleroma, :email_notifications,
 +  digest: %{
 +    active: true,
 +    schedule: "0 0 * * 0",
 +    interval: 7,
 +    inactivity_threshold: 7
 +  }
 +
  config :pleroma, :oauth2,
    token_expires_in: 600,
-   issue_new_refresh_token: true
+   issue_new_refresh_token: true,
+   clean_expired_tokens: false,
+   clean_expired_tokens_interval: 86_400_000
+ config :pleroma, :database, rum_enabled: false
+ config :http_signatures,
+   adapter: Pleroma.Signature
  
  # Import environment specific config. This must remain at the bottom
  # of this file so it overrides the configuration defined above.
diff --cc docs/config.md
Simple merge
Simple merge
Simple merge
index b23a700678b2f5d39747b7ac643ec32dfbb2af46,653dec95f2b5d0515e2267008cf1eb7efe1851fe..e5b1219b29209ca9473310b9a43377e23e2626c4
@@@ -53,10 -54,9 +54,10 @@@ defmodule Pleroma.User d
      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, Pleroma.User.Info)
+     embeds_one(:info, User.Info)
  
      timestamps()
    end
      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
+     info_changeset =
+       User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
+     user
+     |> change()
+     |> put_embed(:info, info_changeset)
+     |> update_and_set_cache()
+   end
+   def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
+     mascot
+   end
+   def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
+     # use instance-default
+     config = Pleroma.Config.get([:assets, :mascots])
+     default_mascot = Pleroma.Config.get([:assets, :default_mascot])
+     mascot = Keyword.get(config, default_mascot)
+     %{
+       "id" => "default-mascot",
+       "url" => mascot[:url],
+       "preview_url" => mascot[:url],
+       "pleroma" => %{
+         "mime_type" => mascot[:mime_type]
+       }
+     }
+   end
+   def ensure_keys_present(user) do
+     info = user.info
+     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)
+     end
+   end
  end
index ab4e8113419fefc2853c1494b2d47ccad8827b6d,6397e2737b8fddc658149b420e74db539a41f27e..e88ee416471763bd739e21fcddae1f7b63b50ca9
@@@ -43,7 -43,7 +43,8 @@@ defmodule Pleroma.User.Info d
      field(:hide_favorites, :boolean, default: true)
      field(:pinned_activities, {:array, :string}, default: [])
      field(:flavour, :string, default: nil)
 +    field(:email_notifications, :map, default: %{"digest" => false})
+     field(:mascot, :map, default: nil)
      field(:emoji, {:array, :map}, default: [])
  
      field(:notification_settings, :map,
Simple merge
diff --cc mix.exs
index 34ce8a3072740576ac4582cc5d3b102891a0e1e4,b2017ef9bff5ca9006210ff16c949b81febd5cf3..65d7fac34441bc0444fd00cd7f1fcd161eb88835
+++ b/mix.exs
@@@ -113,11 -116,11 +117,13 @@@ defmodule Pleroma.Mixfile d
        {:prometheus_process_collector, "~> 1.4"},
        {:recon, github: "ferd/recon", tag: "2.4.0"},
        {:quack, "~> 0.1.1"},
 +      {:quantum, "~> 2.3"},
 +      {:joken, "~> 2.0"},
        {:benchee, "~> 1.0"},
-       {:esshd, "~> 0.1.0"},
-       {:plug_static_index_html, "~> 1.0.0"}
+       {:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)},
+       {:ex_rated, "~> 1.2"},
+       {:plug_static_index_html, "~> 1.0.0"},
+       {:excoveralls, "~> 0.11.1", only: :test}
      ] ++ oauth_deps
    end
  
diff --cc mix.lock
index 37637618cb04076421c16800909a36d451c863ad,857bfca790090ae0acc9ac798ed28643ba3febe8..270b92fbfbc4d3e358048e31f6d021d411bb74c2
+++ b/mix.lock
    "ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"},
    "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
    "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"},
+   "ex_rated": {:hex, :ex_rated, "1.3.2", "6aeb32abb46ea6076f417a9ce8cb1cf08abf35fb2d42375beaad4dd72b550bf1", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"},
    "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]},
+   "excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
    "floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
    "gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "hexpm"},
 -  "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
 +  "gen_stage": {:hex, :gen_stage, "0.14.1", "9d46723fda072d4f4bb31a102560013f7960f5d80ea44dcb96fd6304ed61e7a4", [:mix], [], "hexpm"},
 +  "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
 +  "gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"},
    "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
    "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"},
    "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
    "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
    "recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]},
    "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
 -  "swoosh": {:hex, :swoosh, "0.20.0", "9a6c13822c9815993c03b6f8fccc370fcffb3c158d9754f67b1fdee6b3a5d928", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
 +  "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"},
 +  "swoosh": {:hex, :swoosh, "0.23.1", "209b7cc6d862c09d2a064c16caa4d4d1c9c936285476459e16591e0065f8432b", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
    "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]},
-   "telemetry": {:hex, :telemetry, "0.3.0", "099a7f3ce31e4780f971b4630a3c22ec66d22208bc090fe33a2a3a6a67754a73", [:rebar3], [], "hexpm"},
+   "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
    "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
    "timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
    "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
Simple merge
index d384556a277325eab600830d829ca85a9de5e759,019f2b56d1fb648aff531e5a4916211be5cdf3a1..8a16dc495753064ab840ba6b3104067a5207587d
@@@ -1148,106 -1234,36 +1238,139 @@@ defmodule Pleroma.UserTest d
      assert Map.get(user_show, "followers_count") == 2
    end
  
 +  describe "list_inactive_users_query/1" do
 +    defp days_ago(days) do
 +      NaiveDateTime.add(
 +        NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
 +        -days * 60 * 60 * 24,
 +        :second
 +      )
 +    end
 +
 +    test "Users are inactive by default" do
 +      total = 10
 +
 +      users =
 +        Enum.map(1..total, fn _ ->
 +          insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false})
 +        end)
 +
 +      inactive_users_ids =
 +        Pleroma.User.list_inactive_users_query()
 +        |> Pleroma.Repo.all()
 +        |> Enum.map(& &1.id)
 +
 +      Enum.each(users, fn user ->
 +        assert user.id in inactive_users_ids
 +      end)
 +    end
 +
 +    test "Only includes users who has no recent activity" do
 +      total = 10
 +
 +      users =
 +        Enum.map(1..total, fn _ ->
 +          insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false})
 +        end)
 +
 +      {inactive, active} = Enum.split(users, trunc(total / 2))
 +
 +      Enum.map(active, fn user ->
 +        to = Enum.random(users -- [user])
 +
 +        {:ok, _} =
 +          Pleroma.Web.TwitterAPI.TwitterAPI.create_status(user, %{
 +            "status" => "hey @#{to.nickname}"
 +          })
 +      end)
 +
 +      inactive_users_ids =
 +        Pleroma.User.list_inactive_users_query()
 +        |> Pleroma.Repo.all()
 +        |> Enum.map(& &1.id)
 +
 +      Enum.each(active, fn user ->
 +        refute user.id in inactive_users_ids
 +      end)
 +
 +      Enum.each(inactive, fn user ->
 +        assert user.id in inactive_users_ids
 +      end)
 +    end
 +
 +    test "Only includes users with no read notifications" do
 +      total = 10
 +
 +      users =
 +        Enum.map(1..total, fn _ ->
 +          insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false})
 +        end)
 +
 +      [sender | recipients] = users
 +      {inactive, active} = Enum.split(recipients, trunc(total / 2))
 +
 +      Enum.each(recipients, fn to ->
 +        {:ok, _} =
 +          Pleroma.Web.TwitterAPI.TwitterAPI.create_status(sender, %{
 +            "status" => "hey @#{to.nickname}"
 +          })
 +
 +        {:ok, _} =
 +          Pleroma.Web.TwitterAPI.TwitterAPI.create_status(sender, %{
 +            "status" => "hey again @#{to.nickname}"
 +          })
 +      end)
 +
 +      Enum.each(active, fn user ->
 +        [n1, _n2] = Pleroma.Notification.for_user(user)
 +        {:ok, _} = Pleroma.Notification.read_one(user, n1.id)
 +      end)
 +
 +      inactive_users_ids =
 +        Pleroma.User.list_inactive_users_query()
 +        |> Pleroma.Repo.all()
 +        |> Enum.map(& &1.id)
 +
 +      Enum.each(active, fn user ->
 +        refute user.id in inactive_users_ids
 +      end)
 +
 +      Enum.each(inactive, fn user ->
 +        assert user.id in inactive_users_ids
 +      end)
 +    end
 +  end
++
+   describe "toggle_confirmation/1" do
+     test "if user is confirmed" do
+       user = insert(:user, info: %{confirmation_pending: false})
+       {:ok, user} = User.toggle_confirmation(user)
+       assert user.info.confirmation_pending
+       assert user.info.confirmation_token
+     end
+     test "if user is unconfirmed" do
+       user = insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"})
+       {:ok, user} = User.toggle_confirmation(user)
+       refute user.info.confirmation_pending
+       refute user.info.confirmation_token
+     end
+   end
+   describe "ensure_keys_present" do
+     test "it creates keys for a user and stores them in info" do
+       user = insert(:user)
+       refute is_binary(user.info.keys)
+       {:ok, user} = User.ensure_keys_present(user)
+       assert is_binary(user.info.keys)
+     end
+     test "it doesn't create keys if there already are some" do
+       user = insert(:user, %{info: %{keys: "xxx"}})
+       {:ok, user} = User.ensure_keys_present(user)
+       assert user.info.keys == "xxx"
+     end
+   end
  end