Merge develop
authorRoman Chvanikov <chvanikoff@gmail.com>
Tue, 30 Apr 2019 13:17:52 +0000 (20:17 +0700)
committerRoman Chvanikov <chvanikoff@gmail.com>
Tue, 30 Apr 2019 13:17:52 +0000 (20:17 +0700)
1  2 
.gitignore
config/config.exs
docs/config.md
lib/pleroma/notification.ex
lib/pleroma/user.ex
lib/pleroma/user/info.ex
lib/pleroma/web/router.ex
mix.exs
mix.lock
test/notification_test.exs
test/user_test.exs

diff --combined .gitignore
index 8166e65e9081f20399a73ffb6f4129935e2e84eb,a1e79e4be2a7a34ac0211ad65ae0df60cda1cb96..06107eaf1e259212aa1efeff5e34ae4a30814ed5
@@@ -3,7 -3,6 +3,6 @@@
  /db
  /deps
  /*.ez
- /uploads
  /test/uploads
  /.elixir_ls
  /test/fixtures/test_tmp.txt
@@@ -38,5 -37,3 +37,5 @@@ erl_crash.dum
  
  # Prevent committing docs files
  /priv/static/doc/*
 +
 +/cover
diff --combined config/config.exs
index b1d506b59381e80c783e7dc0a98f4e6da75c95da,1a9738cff4676551ea2f72c7c8b50d562be37195..eeaaedf48d5cbd9540cf12b22ab9be2201231fa9
@@@ -100,9 -100,9 +100,9 @@@ config :pleroma, :emoji
    shortcode_globs: ["/emoji/custom/**/*.png"],
    groups: [
      # Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md`
-     Finmoji: "/finmoji/128px/*-128.png",
-     Custom: ["/emoji/*.png", "/emoji/custom/*.png"]
-   ]
+     Custom: ["/emoji/*.png", "/emoji/**/*.png"]
+   ],
+   default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"
  
  config :pleroma, :uri_schemes,
    valid_schemes: [
@@@ -221,9 -221,9 +221,9 @@@ config :pleroma, :instance
    allowed_post_formats: [
      "text/plain",
      "text/html",
-     "text/markdown"
+     "text/markdown",
+     "text/bbcode"
    ],
-   finmoji_enabled: true,
    mrf_transparency: true,
    autofollowed_nicknames: [],
    max_pinned_statuses: 1,
    welcome_user_nickname: nil,
    welcome_message: nil,
    max_report_comment_size: 1000,
-   safe_dm_mentions: false
+   safe_dm_mentions: false,
+   healthcheck: false
  
  config :pleroma, :markup,
    # XXX - unfortunately, inline images must be enabled by default right now, because
@@@ -326,7 -327,8 +327,8 @@@ config :pleroma, :media_proxy
        follow_redirect: true,
        pool: :media
      ]
-   ]
+   ],
+   whitelist: []
  
  config :pleroma, :chat, enabled: true
  
@@@ -466,14 -468,6 +468,14 @@@ 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
 +  }
 +
  # Import environment specific config. This must remain at the bottom
  # of this file so it overrides the configuration defined above.
  import_config "#{Mix.env()}.exs"
diff --combined docs/config.md
index 69d3893827c6ad9ccab536c78a9a170886eee946,7e31e6fb785ddd6d9cc56469486758cab85d228e..cd0e145df67b1501e7bd5211165ade81a9679d7d
@@@ -87,7 -87,6 +87,6 @@@ config :pleroma, Pleroma.Emails.Mailer
  * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
  * `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json``
  * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML)
- * `finmoji_enabled`: Whenether to enable the finmojis in the custom emojis.
  * `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
  * `scope_copy`: Copy the scope (private/unlisted/public) in replies to posts by default.
  * `subject_line_behavior`: Allows changing the default behaviour of subject lines in replies. Valid values:
  * `welcome_user_nickname`: The nickname of the local user that sends the welcome message.
  * `max_report_comment_size`: The maximum size of the report comment (Default: `1000`)
  * `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`)
+ * `healthcheck`: if set to true, system data will be shown on ``/api/pleroma/healthcheck``.
  
  ## :logger
  * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack
@@@ -205,6 -205,7 +205,7 @@@ This section is used to configure Plero
  * `enabled`: Enables proxying of remote media to the instance’s proxy
  * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
  * `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
+ * `whitelist`: List of domains to bypass the mediaproxy
  
  ## :gopher
  * `enabled`: Enables the gopher interface
@@@ -435,18 -436,6 +436,18 @@@ Authentication / authorization settings
  * `oauth_consumer_template`: OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`.
  * `oauth_consumer_strategies`: the list of enabled OAuth consumer strategies; by default it's set by OAUTH_CONSUMER_STRATEGIES environment variable.
  
 +## :email_notifications
 +
 +Email notifications settings.
 +
 +  - digest - emails of "what you've missed" for users who have been
 +    inactive for a while.
 +    - active: globally enable or disable digest emails
 +    - schedule: When to send digest email, in [crontab format](https://en.wikipedia.org/wiki/Cron).
 +      "0 0 * * 0" is the default, meaning "once a week at midnight on Sunday morning"
 +    - interval: Minimum interval between digest emails to one user
 +    - inactivity_threshold: Minimum user inactivity threshold
 +
  # OAuth consumer mode
  
  OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.).
@@@ -499,3 -488,8 +500,8 @@@ config :ueberauth, Ueberauth
      microsoft: {Ueberauth.Strategy.Microsoft, [callback_params: []]}
    ]
  ```
+ ## :emoji
+ * `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]`
+ * `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]`
+ * `default_manifest`: Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays).
index d79f0f563b261a17a4d891d90aa42e98f86d278b,dd274cf6b22b8bdb2412b79d759f1bd7189e2239..bf45be96168c647902b8acb1386f04f059baaeae
@@@ -17,8 -17,6 +17,8 @@@ defmodule Pleroma.Notification d
    import Ecto.Query
    import Ecto.Changeset
  
 +  @type t :: %__MODULE__{}
 +
    schema "notifications" do
      field(:seen, :boolean, default: false)
      belongs_to(:user, User, type: Pleroma.FlakeId)
      |> Pagination.fetch_paginated(opts)
    end
  
 +  @doc """
 +  Returns notifications for user received since given date.
 +
 +  ## Examples
 +
 +      iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
 +      [%Pleroma.Notification{}, %Pleroma.Notification{}]
 +
 +      iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
 +      []
 +  """
 +  @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
 +  def for_user_since(user, date) do
 +    from(n in for_user_query(user),
 +      where: n.updated_at > ^date
 +    )
 +    |> Repo.all()
 +  end
 +
    def set_read_up_to(%{id: user_id} = _user, id) do
      query =
        from(
          where: n.user_id == ^user_id,
          where: n.id <= ^id,
          update: [
 -          set: [seen: true]
 +          set: [
 +            seen: true,
 +            updated_at: ^NaiveDateTime.utc_now()
 +          ]
          ]
        )
  
  
    def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do
      actor = activity.data["actor"]
-     followed = User.get_by_ap_id(actor)
+     followed = User.get_cached_by_ap_id(actor)
      User.following?(user, followed)
    end
  
diff --combined lib/pleroma/user.ex
index 2509d23666f00ae5e144a76dbce13d591e708c20,c5b1ddc5da0991b15d4ba93a44a693c00b927b74..4417a12ddfa93edf3ed53b00e57d7dcfa94005c0
@@@ -10,6 -10,7 +10,7 @@@ defmodule Pleroma.User d
  
    alias Comeonin.Pbkdf2
    alias Pleroma.Activity
+   alias Pleroma.Bookmark
    alias Pleroma.Formatter
    alias Pleroma.Notification
    alias Pleroma.Object
@@@ -53,9 -54,8 +54,9 @@@
      field(:search_rank, :float, virtual: true)
      field(:search_type, :integer, virtual: true)
      field(:tags, {:array, :string}, default: [])
-     field(:bookmarks, {:array, :string}, default: [])
      field(:last_refreshed_at, :naive_datetime_usec)
 +    field(:last_digest_emailed_at, :naive_datetime)
+     has_many(:bookmarks, Bookmark)
      has_many(:notifications, Notification)
      has_many(:registrations, Registration)
      embeds_one(:info, Pleroma.User.Info)
    def register(%Ecto.Changeset{} = changeset) do
      with {:ok, user} <- Repo.insert(changeset),
           {:ok, user} <- autofollow_users(user),
+          {:ok, user} <- set_cache(user),
           {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
           {:ok, _} <- try_send_confirmation_email(user) do
        {:ok, user}
      name = List.last(String.split(ap_id, "/"))
      nickname = "#{name}@#{domain}"
  
-     get_by_nickname(nickname)
+     get_cached_by_nickname(nickname)
    end
  
-   def set_cache(user) do
+   def set_cache({:ok, user}), do: set_cache(user)
+   def set_cache({:error, err}), do: {:error, err}
+   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))
          with [_nick, _domain] <- String.split(nickname, "@"),
               {:ok, user} <- fetch_by_nickname(nickname) do
            if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
+             # TODO turn into job
              {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
            end
  
  
    # helper to handle the block given only an actor's AP id
    def block(blocker, %{ap_id: ap_id}) do
-     block(blocker, User.get_by_ap_id(ap_id))
+     block(blocker, get_cached_by_ap_id(ap_id))
    end
  
    def unblock(blocker, %{ap_id: ap_id}) do
    end
  
    def subscribed_to?(user, %{ap_id: ap_id}) do
-     with %User{} = target <- User.get_by_ap_id(ap_id) do
+     with %User{} = target <- get_cached_by_ap_id(ap_id) do
        Enum.member?(target.info.subscribers, user.ap_id)
      end
    end
    end
  
    def get_or_fetch_by_ap_id(ap_id) do
-     user = get_by_ap_id(ap_id)
+     user = get_cached_by_ap_id(ap_id)
  
      if !is_nil(user) and !User.needs_update?(user) do
        user
    def get_or_create_instance_user do
      relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
  
-     if user = get_by_ap_id(relay_uri) do
+     if user = get_cached_by_ap_id(relay_uri) do
        user
      else
        changes =
    defp blank?(n), do: n
  
    def insert_or_update_user(data) do
-     data =
-       data
-       |> Map.put(:name, blank?(data[:name]) || data[:nickname])
-     cs = User.remote_user_creation(data)
-     Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
+     data
+     |> Map.put(:name, blank?(data[:name]) || data[:nickname])
+     |> remote_user_creation()
+     |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
+     |> set_cache()
    end
  
    def ap_enabled?(%User{local: true}), do: true
    # this is because we have synchronous follow APIs and need to simulate them
    # with an async handshake
    def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
-     with %User{} = a <- User.get_by_id(a.id),
-          %User{} = b <- User.get_by_id(b.id) do
+     with %User{} = a <- User.get_cached_by_id(a.id),
+          %User{} = b <- User.get_cached_by_id(b.id) do
        {:ok, a, b}
      else
        _e ->
  
    def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
      with :ok <- :timer.sleep(timeout),
-          %User{} = a <- User.get_by_id(a.id),
-          %User{} = b <- User.get_by_id(b.id) do
+          %User{} = a <- User.get_cached_by_id(a.id),
+          %User{} = b <- User.get_cached_by_id(b.id) do
        {:ok, a, b}
      else
        _e ->
    end
  
    def tag(nickname, tags) when is_binary(nickname),
-     do: tag(User.get_by_nickname(nickname), tags)
+     do: tag(get_by_nickname(nickname), tags)
  
    def tag(%User{} = user, tags),
      do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
    end
  
    def untag(nickname, tags) when is_binary(nickname),
-     do: untag(User.get_by_nickname(nickname), tags)
+     do: untag(get_by_nickname(nickname), tags)
  
    def untag(%User{} = user, tags),
      do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
      updated_user
    end
  
-   def bookmark(%User{} = user, status_id) do
-     bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
-     update_bookmarks(user, bookmarks)
-   end
-   def unbookmark(%User{} = user, status_id) do
-     bookmarks = Enum.uniq(user.bookmarks -- [status_id])
-     update_bookmarks(user, bookmarks)
-   end
-   def update_bookmarks(%User{} = user, bookmarks) do
-     user
-     |> change(%{bookmarks: bookmarks})
-     |> update_and_set_cache
-   end
    defp normalize_tags(tags) do
      [tags]
      |> List.flatten()
    def showing_reblogs?(%User{} = user, %User{} = target) 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
  end
diff --combined lib/pleroma/user/info.ex
index d827293b8de6a3bdb44b479df09ebb854651e6d1,a3658d57ff495cd65a8fe1e9bb45412318d66072..2d360d6507d3e50c18456583f70887641c807e04
@@@ -8,8 -8,6 +8,8 @@@ defmodule Pleroma.User.Info d
  
    alias Pleroma.User.Info
  
 +  @type t :: %__MODULE__{}
 +
    embedded_schema do
      field(:banner, :map, default: %{})
      field(:background, :map, default: %{})
      field(:salmon, :string, default: nil)
      field(:hide_followers, :boolean, default: false)
      field(:hide_follows, :boolean, default: false)
+     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(:notification_settings, :map,
        default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true}
      |> validate_required([:notification_settings])
    end
  
 +  @doc """
 +  Update email notifications in the given User.Info struct.
 +
 +  Examples:
 +
 +      iex> update_email_notifications(%Pleroma.User.Info{email_notifications: %{"digest" => false}}, %{"digest" => true})
 +      %Pleroma.User.Info{email_notifications: %{"digest" => true}}
 +
 +  """
 +  @spec update_email_notifications(t(), map()) :: Ecto.Changeset.t()
 +  def update_email_notifications(info, settings) do
 +    email_notifications =
 +      info.email_notifications
 +      |> Map.merge(settings)
 +      |> Map.take(["digest"])
 +
 +    params = %{email_notifications: email_notifications}
 +    fields = [:email_notifications]
 +
 +    info
 +    |> cast(params, fields)
 +    |> validate_required(fields)
 +  end
 +
    def add_to_note_count(info, number) do
      set_note_count(info, info.note_count + number)
    end
        :banner,
        :hide_follows,
        :hide_followers,
+       :hide_favorites,
        :background,
        :show_role
      ])
      cast(info, params, [:confirmation_pending, :confirmation_token])
    end
  
-   def mastodon_profile_update(info, params) do
-     info
-     |> cast(params, [
-       :locked,
-       :banner
-     ])
-   end
    def mastodon_settings_update(info, settings) do
      params = %{settings: settings}
  
index 09e51e60268d45dffc03c34f724d32bbc466b4c7,ff4f08af57a58a16cf7db7d033577bece2349649..9b833bc4883c232d155918931a1389f346715753
@@@ -135,6 -135,7 +135,7 @@@ defmodule Pleroma.Web.Router d
      post("/password_reset", UtilController, :password_reset)
      get("/emoji", UtilController, :emoji)
      get("/captcha", UtilController, :captcha)
+     get("/healthcheck", UtilController, :healthcheck)
    end
  
    scope "/api/pleroma", Pleroma.Web do
        get("/accounts/:id", MastodonAPIController, :user)
  
        get("/search", MastodonAPIController, :search)
+       get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
      end
    end
  
      post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
      get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
      post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
 +
 +    get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
    end
  
    scope "/", Pleroma.Web do
diff --combined mix.exs
index 2cdfb139209c04379f9d3cdcf41313076248b5a8,efaa06a1c473b5d4ed792ee27e4ff27fccee42b3..bc2e552253fc3e85349f3b087f78c984e1563879
+++ b/mix.exs
@@@ -84,6 -84,7 +84,7 @@@ defmodule Pleroma.Mixfile d
        {:ex_aws, "~> 2.0"},
        {:ex_aws_s3, "~> 2.0"},
        {:earmark, "~> 1.3"},
+       {:bbcode, "~> 0.1"},
        {:ex_machina, "~> 2.3", only: :test},
        {:credo, "~> 0.9.3", only: [:dev, :test]},
        {:mock, "~> 0.3.1", only: :test},
@@@ -93,7 -94,6 +94,7 @@@
        {:ex_doc, "~> 0.20.2", only: :dev, runtime: false},
        {:web_push_encryption, "~> 0.2.1"},
        {:swoosh, "~> 0.20"},
 +      {:phoenix_swoosh, "~> 0.2"},
        {:gen_smtp, "~> 0.13"},
        {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test},
        {:floki, "~> 0.20.0"},
        {:prometheus_ecto, "~> 1.4"},
        {:prometheus_process_collector, "~> 1.4"},
        {:recon, github: "ferd/recon", tag: "2.4.0"},
 -      {:quack, "~> 0.1.1"}
 +      {:quack, "~> 0.1.1"},
 +      {:quantum, "~> 2.3"},
 +      {:joken, "~> 2.0"}
      ] ++ oauth_deps
    end
  
diff --combined mix.lock
index 73aed012f2b47195e6532874535561173fa9eb45,979d599b465fb35b5d7c90d7a41549d6d848c4e8..e70d45b88290aadb79097030288f9a7795edff00
+++ b/mix.lock
@@@ -2,25 -2,25 +2,26 @@@
    "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm"},
    "auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "90613b4bae875a3610c275b7056b61ffdd53210d", [ref: "90613b4bae875a3610c275b7056b61ffdd53210d"]},
    "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
+   "bbcode": {:hex, :bbcode, "0.1.0", "400e618b640b635261611d7fb7f79d104917fc5b084aae371ab6b08477cb035b", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
    "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
 -  "cachex": {:hex, :cachex, "3.0.2", "1351caa4e26e29f7d7ec1d29b53d6013f0447630bbf382b4fb5d5bad0209f203", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
 -  "calendar": {:hex, :calendar, "0.17.4", "22c5e8d98a4db9494396e5727108dffb820ee0d18fed4b0aa8ab76e4f5bc32f1", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
 +  "cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
 +  "calendar": {:hex, :calendar, "0.17.5", "0ff5b09a60b9677683aa2a6fee948558660501c74a289103ea099806bc41a352", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
    "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
    "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
 -  "comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
 +  "comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
    "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
    "cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
 -  "cowboy": {:hex, :cowboy, "2.6.1", "f2e06f757c337b3b311f9437e6e072b678fcd71545a7b2865bdaa154d078593f", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
 -  "cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"},
 +  "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
 +  "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"},
    "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
 +  "crontab": {:hex, :crontab, "1.1.5", "2c9439506ceb0e9045de75879e994b88d6f0be88bfe017d58cb356c66c4a5482", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
    "crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]},
 -  "db_connection": {:hex, :db_connection, "2.0.5", "ddb2ba6761a08b2bb9ca0e7d260e8f4dd39067426d835c24491a321b7f92a4da", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
 +  "db_connection": {:hex, :db_connection, "2.0.6", "bde2f85d047969c5b5800cb8f4b3ed6316c8cb11487afedac4aa5f93fd39abfa", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
    "decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"},
    "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"},
 -  "ecto": {:hex, :ecto, "3.0.7", "44dda84ac6b17bbbdeb8ac5dfef08b7da253b37a453c34ab1a98de7f7e5fec7f", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
 +  "ecto": {:hex, :ecto, "3.0.8", "9eb6a1fcfc593e6619d45ef51afe607f1554c21ca188a1cd48eecc27223567f1", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
    "ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
 -  "eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"},
 +  "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"},
    "ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"},
    "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_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]},
    "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"},
    "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
    "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
    "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
 -  "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
 +  "joken": {:hex, :joken, "2.0.1", "ec9ab31bf660f343380da033b3316855197c8d4c6ef597fa3fcb451b326beb14", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"},
 +  "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
 +  "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"},
    "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
    "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
    "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"},
    "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
    "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
    "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
 -  "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"},
 -  "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
 +  "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
 +  "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
    "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"},
    "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
    "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
 -  "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"},
 -  "phoenix": {:hex, :phoenix, "1.4.1", "801f9d632808657f1f7c657c8bbe624caaf2ba91429123ebe3801598aea4c3d9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
 +  "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm"},
 +  "phoenix": {:hex, :phoenix, "1.4.3", "8eed4a64ff1e12372cd634724bddd69185938f52c18e1396ebac76375d85677d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
    "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
 -  "phoenix_html": {:hex, :phoenix_html, "2.13.1", "fa8f034b5328e2dfa0e4131b5569379003f34bc1fafdaa84985b0b9d2f12e68b", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
 -  "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.1", "6668d787e602981f24f17a5fbb69cc98f8ab085114ebfac6cc36e10a90c8e93c", [:mix], [], "hexpm"},
 +  "phoenix_html": {:hex, :phoenix_html, "2.13.2", "f5d27c9b10ce881a60177d2b5227314fc60881e6b66b41dfe3349db6ed06cf57", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
 +  "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"},
 +  "phoenix_swoosh": {:hex, :phoenix_swoosh, "0.2.0", "a7e0b32077cd6d2323ae15198839b05d9caddfa20663fd85787479e81f89520e", [:mix], [{:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 0.1", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm"},
    "pleroma_job_queue": {:hex, :pleroma_job_queue, "0.2.0", "879e660aa1cebe8dc6f0aaaa6aa48b4875e89cd961d4a585fd128e0773b31a18", [:mix], [], "hexpm"},
    "plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"},
 -  "plug_cowboy": {:hex, :plug_cowboy, "2.0.1", "d798f8ee5acc86b7d42dbe4450b8b0dadf665ce588236eb0a751a132417a980e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
 +  "plug_cowboy": {:hex, :plug_cowboy, "2.0.2", "6055f16868cc4882b24b6e1d63d2bada94fb4978413377a3b32ac16c18dffba2", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
    "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
    "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
 -  "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"},
 -  "postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
 +  "postgrex": {:hex, :postgrex, "0.14.2", "6680591bbce28d92f043249205e8b01b36cab9ef2a7911abc43649242e1a3b78", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
    "prometheus": {:hex, :prometheus, "4.2.2", "a830e77b79dc6d28183f4db050a7cac926a6c58f1872f9ef94a35cd989aceef8", [:mix, :rebar3], [], "hexpm"},
    "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.1", "6c768ea9654de871e5b32fab2eac348467b3021604ebebbcbd8bcbe806a65ed5", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"},
    "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"},
    "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.2.1", "964a74dfbc055f781d3a75631e06ce3816a2913976d1df7830283aa3118a797a", [:mix], [{:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"},
    "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm"},
 -  "prometheus_process_collector": {:hex, :prometheus_process_collector, "1.4.0", "6dbd39e3165b9ef1c94a7a820e9ffe08479f949dcdd431ed4aaea7b250eebfde", [:rebar3], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"},
 +  "prometheus_process_collector": {:hex, :prometheus_process_collector, "1.4.3", "657386e8f142fc817347d95c1f3a05ab08710f7df9e7f86db6facaed107ed929", [:rebar3], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"},
    "quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm"},
 +  "quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "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"},
    "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"},
 -  "tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
 +  "tzdata": {:hex, :tzdata, "0.5.20", "304b9e98a02840fb32a43ec111ffbe517863c8566eb04a061f1c4dbb90b4d84c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
    "ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
    "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
 -  "unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"},
 +  "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm"},
    "web_push_encryption": {:hex, :web_push_encryption, "0.2.1", "d42cecf73420d9dc0053ba3299cc8c8d6ff2be2487d67ca2a57265868e4d9a98", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
    "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},
  }
index 3bbce8fcf87e54cd3d7d312ced568a9e3971a084,581db58a80296f51817ed2260a0b874440bd6eb8..eec0a48292ccb382aa587c32775cc909fe6715cb
@@@ -4,14 -4,12 +4,14 @@@
  
  defmodule Pleroma.NotificationTest do
    use Pleroma.DataCase
 +
 +  import Pleroma.Factory
 +
    alias Pleroma.Notification
    alias Pleroma.User
    alias Pleroma.Web.ActivityPub.Transmogrifier
    alias Pleroma.Web.CommonAPI
    alias Pleroma.Web.TwitterAPI.TwitterAPI
 -  import Pleroma.Factory
  
    describe "create_notifications" do
      test "notifies someone when they are directly addressed" do
@@@ -48,7 -46,7 +48,7 @@@
    describe "create_notification" do
      test "it doesn't create a notification for user if the user blocks the activity author" do
        activity = insert(:note_activity)
-       author = User.get_by_ap_id(activity.data["actor"])
+       author = User.get_cached_by_ap_id(activity.data["actor"])
        user = insert(:user)
        {:ok, user} = User.block(user, author)
  
  
      test "it doesn't create a notification for user if he is the activity author" do
        activity = insert(:note_activity)
-       author = User.get_by_ap_id(activity.data["actor"])
+       author = User.get_cached_by_ap_id(activity.data["actor"])
  
        assert nil == Notification.create_notification(activity, author)
      end
      end
    end
  
 +  describe "for_user_since/2" do
 +    defp days_ago(days) do
 +      NaiveDateTime.add(
 +        NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
 +        -days * 60 * 60 * 24,
 +        :second
 +      )
 +    end
 +
 +    test "Returns recent notifications" do
 +      user1 = insert(:user)
 +      user2 = insert(:user)
 +
 +      Enum.each(0..10, fn i ->
 +        {:ok, _activity} =
 +          CommonAPI.post(user1, %{
 +            "status" => "hey ##{i} @#{user2.nickname}!"
 +          })
 +      end)
 +
 +      {old, new} = Enum.split(Notification.for_user(user2), 5)
 +
 +      Enum.each(old, fn notification ->
 +        notification
 +        |> cast(%{updated_at: days_ago(10)}, [:updated_at])
 +        |> Pleroma.Repo.update!()
 +      end)
 +
 +      recent_notifications_ids =
 +        user2
 +        |> Notification.for_user_since(
 +          NaiveDateTime.add(NaiveDateTime.utc_now(), -5 * 86_400, :second)
 +        )
 +        |> Enum.map(& &1.id)
 +
 +      Enum.each(old, fn %{id: id} ->
 +        refute id in recent_notifications_ids
 +      end)
 +
 +      Enum.each(new, fn %{id: id} ->
 +        assert id in recent_notifications_ids
 +      end)
 +    end
 +  end
 +
    describe "notification target determination" do
      test "it sends notifications to addressed users in new messages" do
        user = insert(:user)
diff --combined test/user_test.exs
index d11d942637a319f11d53876ae4c2f801e1a74d71,7be47e5fb505d814263eb666afc537e61fb15587..a4d51ac95a81bd9dc19bb5ebb6aa036fcf5ad9ae
@@@ -123,9 -123,9 +123,9 @@@ defmodule Pleroma.UserTest d
  
      {:ok, user} = User.follow(user, followed)
  
-     user = User.get_by_id(user.id)
+     user = User.get_cached_by_id(user.id)
  
-     followed = User.get_by_ap_id(followed.ap_id)
+     followed = User.get_cached_by_ap_id(followed.ap_id)
      assert followed.info.follower_count == 1
  
      assert User.ap_followers(followed) in user.following
  
      {:ok, user, _activity} = User.unfollow(user, followed)
  
-     user = User.get_by_id(user.id)
+     user = User.get_cached_by_id(user.id)
  
      assert user.following == []
    end
  
      {:error, _} = User.unfollow(user, user)
  
-     user = User.get_by_id(user.id)
+     user = User.get_cached_by_id(user.id)
      assert user.following == [user.ap_id]
    end
  
  
        {:ok, res} = User.get_friends(user)
  
-       followed_one = User.get_by_ap_id(followed_one.ap_id)
-       followed_two = User.get_by_ap_id(followed_two.ap_id)
+       followed_one = User.get_cached_by_ap_id(followed_one.ap_id)
+       followed_two = User.get_cached_by_ap_id(followed_two.ap_id)
        assert Enum.member?(res, followed_one)
        assert Enum.member?(res, followed_two)
        refute Enum.member?(res, not_followed)
      test "it sets the info->note_count property" do
        note = insert(:note)
  
-       user = User.get_by_ap_id(note.data["actor"])
+       user = User.get_cached_by_ap_id(note.data["actor"])
  
        assert user.info.note_count == 0
  
  
      test "it increases the info->note_count property" do
        note = insert(:note)
-       user = User.get_by_ap_id(note.data["actor"])
+       user = User.get_cached_by_ap_id(note.data["actor"])
  
        assert user.info.note_count == 0
  
  
      test "it decreases the info->note_count property" do
        note = insert(:note)
-       user = User.get_by_ap_id(note.data["actor"])
+       user = User.get_cached_by_ap_id(note.data["actor"])
  
        assert user.info.note_count == 0
  
        assert User.following?(blocked, blocker)
  
        {:ok, blocker} = User.block(blocker, blocked)
-       blocked = User.get_by_id(blocked.id)
+       blocked = User.get_cached_by_id(blocked.id)
  
        assert User.blocks?(blocker, blocked)
  
        refute User.following?(blocked, blocker)
  
        {:ok, blocker} = User.block(blocker, blocked)
-       blocked = User.get_by_id(blocked.id)
+       blocked = User.get_cached_by_id(blocked.id)
  
        assert User.blocks?(blocker, blocked)
  
        assert User.following?(blocked, blocker)
  
        {:ok, blocker} = User.block(blocker, blocked)
-       blocked = User.get_by_id(blocked.id)
+       blocked = User.get_cached_by_id(blocked.id)
  
        assert User.blocks?(blocker, blocked)
  
  
      {:ok, _} = User.delete(user)
  
-     followed = User.get_by_id(followed.id)
-     follower = User.get_by_id(follower.id)
-     user = User.get_by_id(user.id)
+     followed = User.get_cached_by_id(followed.id)
+     follower = User.get_cached_by_id(follower.id)
+     user = User.get_cached_by_id(user.id)
  
      assert user.info.deactivated
  
      end
  
      test "works with URIs" do
--      results = User.search("http://mastodon.example.org/users/admin", resolve: true)
--      result = results |> List.first()
++      [result] = User.search("http://mastodon.example.org/users/admin", resolve: true)
 +
-       user = User.get_by_ap_id("http://mastodon.example.org/users/admin")
++      expected =
++        result
++        |> Map.put(:search_rank, nil)
++        |> Map.put(:search_type, nil)
++        |> Map.put(:last_digest_emailed_at, nil)
  
-       assert length(results) == 1
-       assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
+       user = User.get_cached_by_ap_id("http://mastodon.example.org/users/admin")
 -      assert length(results) == 1
 -      assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
++      assert user == expected
      end
    end
  
      end
    end
  
-   test "bookmarks" do
-     user = insert(:user)
-     {:ok, activity1} =
-       CommonAPI.post(user, %{
-         "status" => "heweoo!"
-       })
-     id1 = Object.normalize(activity1).data["id"]
-     {:ok, activity2} =
-       CommonAPI.post(user, %{
-         "status" => "heweoo!"
-       })
-     id2 = Object.normalize(activity2).data["id"]
-     assert {:ok, user_state1} = User.bookmark(user, id1)
-     assert user_state1.bookmarks == [id1]
-     assert {:ok, user_state2} = User.unbookmark(user, id1)
-     assert user_state2.bookmarks == []
-     assert {:ok, user_state3} = User.bookmark(user, id2)
-     assert user_state3.bookmarks == [id2]
-   end
    test "follower count is updated when a follower is blocked" do
      user = insert(:user)
      follower = insert(:user)
  
      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
  end