Merge remote-tracking branch 'pleroma/develop' into feature/disable-account
authorEgor Kislitsyn <egor@kislitsyn.com>
Tue, 14 May 2019 11:15:56 +0000 (18:15 +0700)
committerEgor Kislitsyn <egor@kislitsyn.com>
Tue, 14 May 2019 11:15:56 +0000 (18:15 +0700)
1  2 
config/config.exs
lib/pleroma/activity.ex
lib/pleroma/user.ex
lib/pleroma/user/query.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/twitter_api.ex
test/user_test.exs

diff --combined config/config.exs
index a89afd419c35908145762bb677c289199d7d56cb,45034a77552d191a233701cbe1da128c659af793..8d44c96def995da04de471080d75d29f4b74033f
@@@ -212,6 -212,11 +212,11 @@@ config :pleroma, :instance
    registrations_open: true,
    federating: true,
    federation_reachability_timeout_days: 7,
+   federation_publisher_modules: [
+     Pleroma.Web.ActivityPub.Publisher,
+     Pleroma.Web.Websub,
+     Pleroma.Web.Salmon
+   ],
    allow_relay: true,
    rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
    public: true,
    safe_dm_mentions: false,
    healthcheck: false
  
+ config :pleroma, :app_account_creation, enabled: false, max_requests: 5, interval: 1800
  config :pleroma, :markup,
    # XXX - unfortunately, inline images must be enabled by default right now, because
    # of custom emoji.  Issue #275 discusses defanging that somehow.
@@@ -417,8 -424,7 +424,8 @@@ config :pleroma_job_queue, :queues
    mailer: 10,
    transmogrifier: 20,
    scheduled_activities: 10,
 -  background: 5
 +  background: 5,
 +  user: 10
  
  config :pleroma, :fetch_initial_posts,
    enabled: false,
diff --combined lib/pleroma/activity.ex
index 2dcb97159736b99c8cf963d6c3e11ace14203667,c121e800f6c206ca3bfd989282965d42ffd30029..4a09194786f3f58ca4c998fd67d99cb0a8dc4f40
@@@ -6,9 -6,11 +6,11 @@@ defmodule Pleroma.Activity d
    use Ecto.Schema
  
    alias Pleroma.Activity
+   alias Pleroma.Bookmark
    alias Pleroma.Notification
    alias Pleroma.Object
    alias Pleroma.Repo
+   alias Pleroma.User
  
    import Ecto.Changeset
    import Ecto.Query
@@@ -35,6 -37,8 +37,8 @@@
      field(:local, :boolean, default: true)
      field(:actor, :string)
      field(:recipients, {:array, :string}, default: [])
+     # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark
+     has_one(:bookmark, Bookmark)
      has_many(:notifications, Notification, on_delete: :delete_all)
  
      # Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
      |> preload([activity, object], object: object)
    end
  
+   def with_preloaded_bookmark(query, %User{} = user) do
+     from([a] in query,
+       left_join: b in Bookmark,
+       on: b.user_id == ^user.id and b.activity_id == a.id,
+       preload: [bookmark: b]
+     )
+   end
+   def with_preloaded_bookmark(query, _), do: query
    def get_by_ap_id(ap_id) do
      Repo.one(
        from(
      )
    end
  
+   def get_bookmark(%Activity{} = activity, %User{} = user) do
+     if Ecto.assoc_loaded?(activity.bookmark) do
+       activity.bookmark
+     else
+       Bookmark.get(user.id, activity.id)
+     end
+   end
+   def get_bookmark(_, _), do: nil
    def change(struct, params \\ %{}) do
      struct
      |> cast(params, [:data])
    end
  
    def get_by_id(id) do
 -    Repo.get(Activity, id)
 +    Activity
 +    |> where([a], a.id == ^id)
 +    |> restrict_deactivated_users()
 +    |> Repo.one()
    end
  
    def get_by_id_with_object(id) do
  
    def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
      create_by_object_ap_id(ap_id)
 +    |> restrict_deactivated_users()
      |> Repo.one()
    end
  
      |> Repo.all()
    end
  
+   def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
+     from(
+       a in Activity,
+       where:
+         fragment(
+           "? ->> 'type' = 'Follow'",
+           a.data
+         ),
+       where:
+         fragment(
+           "? ->> 'state' = 'pending'",
+           a.data
+         ),
+       where:
+         fragment(
+           "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
+           a.data,
+           a.data,
+           ^ap_id
+         )
+     )
+   end
    @spec query_by_actor(actor()) :: Ecto.Query.t()
    def query_by_actor(actor) do
      from(a in Activity, where: a.actor == ^actor)
    end
 +
 +  def restrict_deactivated_users(query) do
 +    from(activity in query,
 +      where:
 +        fragment(
 +          "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
 +          activity.actor
 +        )
 +    )
 +  end
  end
diff --combined lib/pleroma/user.ex
index 10ee01b8c1307388c9cb354f2d226b879bd84c5f,474de9ba5c7ec716c99fd0ed4ec55c878dd36fba..cf378d46772d73f41bee44e5dcbb3547e3f488a1
@@@ -10,7 -10,6 +10,6 @@@ defmodule Pleroma.User d
  
    alias Comeonin.Pbkdf2
    alias Pleroma.Activity
-   alias Pleroma.Bookmark
    alias Pleroma.Notification
    alias Pleroma.Object
    alias Pleroma.Registration
@@@ -54,7 -53,6 +53,6 @@@
      field(:search_type, :integer, virtual: true)
      field(:tags, {:array, :string}, default: [])
      field(:last_refreshed_at, :naive_datetime_usec)
-     has_many(:bookmarks, Bookmark)
      has_many(:notifications, Notification)
      has_many(:registrations, Registration)
      embeds_one(:info, Pleroma.User.Info)
    def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
  
    def user_info(%User{} = user) do
 -    oneself = if user.local, do: 1, else: 0
 -
      %{
 -      following_count: length(user.following) - oneself,
 +      following_count: following_count(user),
        note_count: user.info.note_count,
        follower_count: user.info.follower_count,
        locked: user.info.locked,
      }
    end
  
-   def following_count(%User{following: following, id: id}) do
-     from(u in User,
-       where: u.follower_address in ^following,
-       where: u.id != ^id
-     )
-     |> restrict_deactivated()
 +  defp restrict_deactivated(query) do
 +    from(u in query,
 +      where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
 +    )
 +  end
 +
 +  def following_count(%User{following: []}), do: 0
 +
++  def following_count(%User{} = user) do
++    user
++    |> get_friends_query()
 +    |> Repo.aggregate(:count, :id)
 +  end
 +
    def remote_user_creation(params) do
      params =
        params
    end
  
    def register_changeset(struct, params \\ %{}, opts \\ []) do
-     confirmation_status =
-       if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
-         :confirmed
+     need_confirmation? =
+       if is_nil(opts[:need_confirmation]) do
+         Pleroma.Config.get([:instance, :account_activation_required])
        else
-         :unconfirmed
+         opts[:need_confirmation]
        end
  
-     info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
+     info_change =
+       User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
  
      changeset =
        struct
      candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
  
      autofollowed_users =
-       from(u in User,
-         where: u.local == true,
-         where: u.nickname in ^candidates
-       )
 -      User.Query.build(%{nickname: candidates, local: true})
++      User.Query.build(%{nickname: candidates, local: true, deactivated: false})
        |> Repo.all()
  
      follow_all(user, autofollowed_users)
      )
    end
  
-   def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
-     from(
-       u in User,
-       where: fragment("? <@ ?", ^[follower_address], u.following),
-       where: u.id != ^id
-     )
-     |> restrict_deactivated()
+   @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
+   def get_followers_query(%User{} = user, nil) do
 -    User.Query.build(%{followers: user})
++    User.Query.build(%{followers: user, deactivated: false})
    end
  
    def get_followers_query(user, page) do
      from(u in get_followers_query(user, nil))
-     |> paginate(page, 20)
+     |> User.Query.paginate(page, 20)
    end
  
+   @spec get_followers_query(User.t()) :: Ecto.Query.t()
    def get_followers_query(user), do: get_followers_query(user, nil)
  
    def get_followers(user, page \\ nil) do
      Repo.all(from(u in q, select: u.id))
    end
  
-   def get_friends_query(%User{id: id, following: following}, nil) do
-     from(
-       u in User,
-       where: u.follower_address in ^following,
-       where: u.id != ^id
-     )
-     |> restrict_deactivated()
+   @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
+   def get_friends_query(%User{} = user, nil) do
 -    User.Query.build(%{friends: user})
++    User.Query.build(%{friends: user, deactivated: false})
    end
  
    def get_friends_query(user, page) do
      from(u in get_friends_query(user, nil))
-     |> paginate(page, 20)
+     |> User.Query.paginate(page, 20)
    end
  
+   @spec get_friends_query(User.t()) :: Ecto.Query.t()
    def get_friends_query(user), do: get_friends_query(user, nil)
  
    def get_friends(user, page \\ nil) do
      Repo.all(from(u in q, select: u.id))
    end
  
-   def get_follow_requests_query(%User{} = user) do
-     from(
-       a in Activity,
-       where:
-         fragment(
-           "? ->> 'type' = 'Follow'",
-           a.data
-         ),
-       where:
-         fragment(
-           "? ->> 'state' = 'pending'",
-           a.data
-         ),
-       where:
-         fragment(
-           "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
-           a.data,
-           a.data,
-           ^user.ap_id
-         )
-     )
-   end
+   @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
    def get_follow_requests(%User{} = user) do
      users =
-       user
-       |> User.get_follow_requests_query()
+       Activity.follow_requests_for_actor(user)
        |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
        |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
        |> group_by([a, u], u.id)
  
      info_cng = User.Info.set_note_count(user.info, note_count)
  
 -    cng =
 -      change(user)
 -      |> put_embed(:info, info_cng)
 -
 -    update_and_set_cache(cng)
 +    user
 +    |> change()
 +    |> put_embed(:info, info_cng)
 +    |> update_and_set_cache()
    end
  
    def update_follower_count(%User{} = user) do
      follower_count_query =
-       User
-       |> where([u], ^user.follower_address in u.following)
-       |> where([u], u.id != ^user.id)
 -      User.Query.build(%{followers: user}) |> select([u], %{count: count(u.id)})
++      User.Query.build(%{followers: user, deactivated: false})
 +      |> select([u], %{count: count(u.id)})
-       |> restrict_deactivated()
  
      User
      |> where(id: ^user.id)
      end
    end
  
-   def get_users_from_set_query(ap_ids, false) do
-     from(
-       u in User,
-       where: u.ap_id in ^ap_ids
-     )
-   end
-   def get_users_from_set_query(ap_ids, true) do
-     query = get_users_from_set_query(ap_ids, false)
-     from(
-       u in query,
-       where: u.local == true
-     )
-   end
+   @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
    def get_users_from_set(ap_ids, local_only \\ true) do
-     get_users_from_set_query(ap_ids, local_only)
 -    criteria = %{ap_id: ap_ids}
++    criteria = %{ap_id: ap_ids, deactivated: false}
+     criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
+     User.Query.build(criteria)
      |> Repo.all()
    end
  
+   @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
    def get_recipients_from_activity(%Activity{recipients: to}) do
-     query =
-       from(
-         u in User,
-         where: u.ap_id in ^to,
-         or_where: fragment("? && ?", u.following, ^to)
-       )
-     query = from(u in query, where: u.local == true)
-     Repo.all(query)
 -    User.Query.build(%{recipients_from_activity: to, local: true})
++    User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
+     |> Repo.all()
    end
  
    def search(query, resolve \\ false, for_user \\ nil) do
            ^processed_query
          )
      )
 +    |> restrict_deactivated()
    end
  
    defp trigram_search_subquery(term) do
        },
        where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
      )
 +    |> restrict_deactivated()
    end
  
    def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
      end
    end
  
-   def muted_users(user),
-     do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
+   @spec muted_users(User.t()) :: [User.t()]
+   def muted_users(user) do
 -    User.Query.build(%{ap_id: user.info.mutes})
++    User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
+     |> Repo.all()
+   end
  
-   def blocked_users(user),
-     do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
+   @spec blocked_users(User.t()) :: [User.t()]
+   def blocked_users(user) do
 -    User.Query.build(%{ap_id: user.info.blocks})
++    User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
+     |> Repo.all()
+   end
  
-   def subscribers(user),
-     do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers))
+   @spec subscribers(User.t()) :: [User.t()]
+   def subscribers(user) do
 -    User.Query.build(%{ap_id: user.info.subscribers})
++    User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
+     |> Repo.all()
+   end
  
    def block_domain(user, domain) do
      info_cng =
      update_and_set_cache(cng)
    end
  
-   def maybe_local_user_query(query, local) do
-     if local, do: local_user_query(query), else: query
-   end
-   def local_user_query(query \\ User) do
-     from(
-       u in query,
-       where: u.local == true,
-       where: not is_nil(u.nickname)
-     )
-   end
-   def maybe_external_user_query(query, external) do
-     if external, do: external_user_query(query), else: query
-   end
-   def external_user_query(query \\ User) do
-     from(
-       u in query,
-       where: u.local == false,
-       where: not is_nil(u.nickname)
-     )
-   end
-   def maybe_active_user_query(query, active) do
-     if active, do: active_user_query(query), else: query
-   end
-   def active_user_query(query \\ User) do
-     from(
-       u in query,
-       where: fragment("not (?->'deactivated' @> 'true')", u.info),
-       where: not is_nil(u.nickname)
-     )
-   end
-   def maybe_deactivated_user_query(query, deactivated) do
-     if deactivated, do: deactivated_user_query(query), else: query
-   end
-   def deactivated_user_query(query \\ User) do
-     from(
-       u in query,
-       where: fragment("(?->'deactivated' @> 'true')", u.info),
-       where: not is_nil(u.nickname)
-     )
-   end
-   def active_local_user_query do
-     from(
-       u in local_user_query(),
-       where: fragment("not (?->'deactivated' @> 'true')", u.info)
-     )
-   end
-   def moderator_user_query do
-     from(
-       u in User,
-       where: u.local == true,
-       where: fragment("?->'is_moderator' @> 'true'", u.info)
-     )
-   end
 +  def deactivate_async(user, status \\ true) do
-     PleromaJobQueue.enqueue(:user, __MODULE__, [:deactivate_async, user, status])
++    PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
 +  end
 +
 +  def perform(:deactivate_async, user, status), do: deactivate(user, status)
 +
    def deactivate(%User{} = user, status \\ true) do
      info_cng = User.Info.set_activation_status(user.info, status)
  
 -    cng =
 -      change(user)
 -      |> put_embed(:info, info_cng)
 +    with {:ok, friends} <- User.get_friends(user),
 +         {:ok, followers} <- User.get_followers(user),
 +         {:ok, user} <-
 +           user
 +           |> change()
 +           |> put_embed(:info, info_cng)
 +           |> update_and_set_cache() do
 +      Enum.each(followers, &invalidate_cache(&1))
 +      Enum.each(friends, &update_follower_count(&1))
  
 -    update_and_set_cache(cng)
 +      {:ok, user}
 +    end
    end
  
    def update_notification_settings(%User{} = user, settings \\ %{}) do
    def ap_enabled?(_), do: false
  
    @doc "Gets or fetch a user by uri or nickname."
-   @spec get_or_fetch(String.t()) :: User.t()
+   @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
    def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
    def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
  
      }
    end
  
+   @spec all_superusers() :: [User.t()]
    def all_superusers do
-     from(
-       u in User,
-       where: u.local == true,
-       where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
-     )
 -    User.Query.build(%{super_users: true, local: true})
++    User.Query.build(%{super_users: true, local: true, deactivated: false})
      |> Repo.all()
    end
  
-   defp paginate(query, page, page_size) do
-     from(u in query,
-       limit: ^page_size,
-       offset: ^((page - 1) * page_size)
-     )
-   end
    def showing_reblogs?(%User{} = user, %User{} = target) do
      target.ap_id not in user.info.muted_reblogs
    end
index 0000000000000000000000000000000000000000,2dfe5ce92c4b154099890d15c0220884d3a7d4a5..3873ef80c15c007a208c54c18fcf8c0475c5337f
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,150 +1,156 @@@
 -  defp compose_query({:deactivated, _}, query) do
+ # Pleroma: A lightweight social networking server
+ # Copyright Â© 2017-2018 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
+ defmodule Pleroma.User.Query do
+   @moduledoc """
+   User query builder module. Builds query from new query or another user query.
+     ## Example:
+         query = Pleroma.User.Query(%{nickname: "nickname"})
+         another_query = Pleroma.User.Query.build(query, %{email: "email@example.com"})
+         Pleroma.Repo.all(query)
+         Pleroma.Repo.all(another_query)
+   Adding new rules:
+     - *ilike criteria*
+       - add field to @ilike_criteria list
+       - pass non empty string
+       - e.g. Pleroma.User.Query.build(%{nickname: "nickname"})
+     - *equal criteria*
+       - add field to @equal_criteria list
+       - pass non empty string
+       - e.g. Pleroma.User.Query.build(%{email: "email@example.com"})
+     - *contains criteria*
+       - add field to @containns_criteria list
+       - pass values list
+       - e.g. Pleroma.User.Query.build(%{ap_id: ["http://ap_id1", "http://ap_id2"]})
+   """
+   import Ecto.Query
+   import Pleroma.Web.AdminAPI.Search, only: [not_empty_string: 1]
+   alias Pleroma.User
+   @type criteria ::
+           %{
+             query: String.t(),
+             tags: [String.t()],
+             name: String.t(),
+             email: String.t(),
+             local: boolean(),
+             external: boolean(),
+             active: boolean(),
+             deactivated: boolean(),
+             is_admin: boolean(),
+             is_moderator: boolean(),
+             super_users: boolean(),
+             followers: User.t(),
+             friends: User.t(),
+             recipients_from_activity: [String.t()],
+             nickname: [String.t()],
+             ap_id: [String.t()]
+           }
+           | %{}
+   @ilike_criteria [:nickname, :name, :query]
+   @equal_criteria [:email]
+   @role_criteria [:is_admin, :is_moderator]
+   @contains_criteria [:ap_id, :nickname]
+   @spec build(criteria()) :: Query.t()
+   def build(query \\ base_query(), criteria) do
+     prepare_query(query, criteria)
+   end
+   @spec paginate(Ecto.Query.t(), pos_integer(), pos_integer()) :: Ecto.Query.t()
+   def paginate(query, page, page_size) do
+     from(u in query,
+       limit: ^page_size,
+       offset: ^((page - 1) * page_size)
+     )
+   end
+   defp base_query do
+     from(u in User)
+   end
+   defp prepare_query(query, criteria) do
+     Enum.reduce(criteria, query, &compose_query/2)
+   end
+   defp compose_query({key, value}, query)
+        when key in @ilike_criteria and not_empty_string(value) do
+     # hack for :query key
+     key = if key == :query, do: :nickname, else: key
+     where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
+   end
+   defp compose_query({key, value}, query)
+        when key in @equal_criteria and not_empty_string(value) do
+     where(query, [u], ^[{key, value}])
+   end
+   defp compose_query({key, values}, query) when key in @contains_criteria and is_list(values) do
+     where(query, [u], field(u, ^key) in ^values)
+   end
+   defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do
+     Enum.reduce(tags, query, &prepare_tag_criteria/2)
+   end
+   defp compose_query({key, _}, query) when key in @role_criteria do
+     where(query, [u], fragment("(?->? @> 'true')", u.info, ^to_string(key)))
+   end
+   defp compose_query({:super_users, _}, query) do
+     where(
+       query,
+       [u],
+       fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
+     )
+   end
+   defp compose_query({:local, _}, query), do: location_query(query, true)
+   defp compose_query({:external, _}, query), do: location_query(query, false)
+   defp compose_query({:active, _}, query) do
+     where(query, [u], fragment("not (?->'deactivated' @> 'true')", u.info))
+     |> where([u], not is_nil(u.nickname))
+   end
++  defp compose_query({:deactivated, false}, query) do
++    from(u in query,
++      where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
++    )
++  end
++
++  defp compose_query({:deactivated, true}, query) do
+     where(query, [u], fragment("?->'deactivated' @> 'true'", u.info))
+     |> where([u], not is_nil(u.nickname))
+   end
+   defp compose_query({:followers, %User{id: id, follower_address: follower_address}}, query) do
+     where(query, [u], fragment("? <@ ?", ^[follower_address], u.following))
+     |> where([u], u.id != ^id)
+   end
+   defp compose_query({:friends, %User{id: id, following: following}}, query) do
+     where(query, [u], u.follower_address in ^following)
+     |> where([u], u.id != ^id)
+   end
+   defp compose_query({:recipients_from_activity, to}, query) do
+     where(query, [u], u.ap_id in ^to or fragment("? && ?", u.following, ^to))
+   end
+   defp compose_query(_unsupported_param, query), do: query
+   defp prepare_tag_criteria(tag, query) do
+     or_where(query, [u], fragment("? = any(?)", ^tag, u.tags))
+   end
+   defp location_query(query, local) do
+     where(query, [u], u.local == ^local)
+     |> where([u], not is_nil(u.nickname))
+   end
+ end
index d06bc64eac30d515f965fe87eb9b466e8150a23a,11777c220cf522318a5150b70386c8f310bfcbef..9a137d8de8bb8abb2ee888565db7d228b1a6e669
@@@ -4,7 -4,7 +4,7 @@@
  
  defmodule Pleroma.Web.ActivityPub.ActivityPub do
    alias Pleroma.Activity
-   alias Pleroma.Instances
+   alias Pleroma.Conversation
    alias Pleroma.Notification
    alias Pleroma.Object
    alias Pleroma.Object.Fetcher
@@@ -14,7 -14,6 +14,6 @@@
    alias Pleroma.User
    alias Pleroma.Web.ActivityPub.MRF
    alias Pleroma.Web.ActivityPub.Transmogrifier
-   alias Pleroma.Web.Federator
    alias Pleroma.Web.WebFinger
  
    import Ecto.Query
@@@ -23,8 -22,6 +22,6 @@@
  
    require Logger
  
-   @httpoison Application.get_env(:pleroma, :httpoison)
    # For Announce activities, we filter the recipients based on following status for any actors
    # that match actual users.  See issue #164 for more information about why this is necessary.
    defp get_recipients(%{"type" => "Announce"} = data) do
        end)
  
        Notification.create_notifications(activity)
+       participations =
+         activity
+         |> Conversation.create_or_bump_for()
+         |> get_participations()
        stream_out(activity)
+       stream_out_participations(participations)
        {:ok, activity}
      else
        %Activity{} = activity ->
      end
    end
  
+   defp get_participations({:ok, %{participations: participations}}), do: participations
+   defp get_participations(_), do: []
+   def stream_out_participations(participations) do
+     participations =
+       participations
+       |> Repo.preload(:user)
+     Enum.each(participations, fn participation ->
+       Pleroma.Web.Streamer.stream("participation", participation)
+     end)
+   end
    def stream_out(activity) do
      public = "https://www.w3.org/ns/activitystreams#Public"
  
            end
          end
        else
+         # TODO: Write test, replace with visibility test
          if !Enum.member?(activity.data["cc"] || [], public) &&
               !Enum.member?(
                 activity.data["to"],
      end
    end
  
-   def fetch_activities_for_context(context, opts \\ %{}) do
+   defp fetch_activities_for_context_query(context, opts) do
      public = ["https://www.w3.org/ns/activitystreams#Public"]
  
      recipients =
        if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
  
-     query = from(activity in Activity)
-     query =
-       query
-       |> restrict_blocked(opts)
-       |> restrict_recipients(recipients, opts["user"])
-     query =
-       from(
-         activity in query,
-         where:
-           fragment(
-             "?->>'type' = ? and ?->>'context' = ?",
-             activity.data,
-             "Create",
-             activity.data,
-             ^context
-           ),
-         order_by: [desc: :id]
+     from(activity in Activity)
+     |> restrict_blocked(opts)
+     |> restrict_recipients(recipients, opts["user"])
+     |> where(
+       [activity],
+       fragment(
+         "?->>'type' = ? and ?->>'context' = ?",
+         activity.data,
+         "Create",
+         activity.data,
+         ^context
        )
-       |> Activity.with_preloaded_object()
+     )
+     |> order_by([activity], desc: activity.id)
+   end
  
-     Repo.all(query)
+   @spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()]
+   def fetch_activities_for_context(context, opts \\ %{}) do
+     context
+     |> fetch_activities_for_context_query(opts)
+     |> Activity.with_preloaded_object()
+     |> Repo.all()
+   end
+   @spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
+           Pleroma.FlakeId.t() | nil
+   def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
+     context
+     |> fetch_activities_for_context_query(opts)
+     |> limit(1)
+     |> select([a], a.id)
+     |> Repo.one()
    end
  
    def fetch_public_activities(opts \\ %{}) do
      |> Activity.with_preloaded_object()
    end
  
+   defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query
+   defp maybe_preload_bookmarks(query, opts) do
+     query
+     |> Activity.with_preloaded_bookmark(opts["user"])
+   end
+   defp maybe_order(query, %{order: :desc}) do
+     query
+     |> order_by(desc: :id)
+   end
+   defp maybe_order(query, %{order: :asc}) do
+     query
+     |> order_by(asc: :id)
+   end
+   defp maybe_order(query, _), do: query
    def fetch_activities_query(recipients, opts \\ %{}) do
      base_query = from(activity in Activity)
  
      base_query
      |> maybe_preload_objects(opts)
+     |> maybe_preload_bookmarks(opts)
+     |> maybe_order(opts)
      |> restrict_recipients(recipients, opts["user"])
      |> restrict_tag(opts)
      |> restrict_tag_reject(opts)
      |> restrict_reblogs(opts)
      |> restrict_pinned(opts)
      |> restrict_muted_reblogs(opts)
 +    |> Activity.restrict_deactivated_users()
    end
  
    def fetch_activities(recipients, opts \\ %{}) do
      end
    end
  
-   def should_federate?(inbox, public) do
-     if public do
-       true
-     else
-       inbox_info = URI.parse(inbox)
-       !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
-     end
-   end
-   def publish(actor, activity) do
-     remote_followers =
-       if actor.follower_address in activity.recipients do
-         {:ok, followers} = User.get_followers(actor)
-         followers |> Enum.filter(&(!&1.local))
-       else
-         []
-       end
-     public = is_public?(activity)
-     {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
-     json = Jason.encode!(data)
-     (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
-     |> Enum.filter(fn user -> User.ap_enabled?(user) end)
-     |> Enum.map(fn %{info: %{source_data: data}} ->
-       (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
-     end)
-     |> Enum.uniq()
-     |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
-     |> Instances.filter_reachable()
-     |> Enum.each(fn {inbox, unreachable_since} ->
-       Federator.publish_single_ap(%{
-         inbox: inbox,
-         json: json,
-         actor: actor,
-         id: activity.data["id"],
-         unreachable_since: unreachable_since
-       })
-     end)
-   end
-   def publish_one(%{inbox: inbox, json: json, actor: actor, id: id} = params) do
-     Logger.info("Federating #{id} to #{inbox}")
-     host = URI.parse(inbox).host
-     digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
-     date =
-       NaiveDateTime.utc_now()
-       |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
-     signature =
-       Pleroma.Web.HTTPSignatures.sign(actor, %{
-         host: host,
-         "content-length": byte_size(json),
-         digest: digest,
-         date: date
-       })
-     with {:ok, %{status: code}} when code in 200..299 <-
-            result =
-              @httpoison.post(
-                inbox,
-                json,
-                [
-                  {"Content-Type", "application/activity+json"},
-                  {"Date", date},
-                  {"signature", signature},
-                  {"digest", digest}
-                ]
-              ) do
-       if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
-         do: Instances.set_reachable(inbox)
-       result
-     else
-       {_post_result, response} ->
-         unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
-         {:error, response}
-     end
-   end
    # filter out broken threads
    def contain_broken_threads(%Activity{} = activity, %User{} = user) do
      entire_thread_visible_for_user?(activity, user)
index 5f7617ece7bb011913c3cc9b741e4fd06b11d62b,51146d010606fd0734a221241ca91cbb0e918c55..80af0afe17f8011f651bf9fae252314b60450921
@@@ -146,34 -146,52 +146,52 @@@ defmodule Pleroma.Web.Router d
    scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
      pipe_through([:admin_api, :oauth_write])
  
-     post("/user/follow", AdminAPIController, :user_follow)
-     post("/user/unfollow", AdminAPIController, :user_unfollow)
-     get("/users", AdminAPIController, :list_users)
-     get("/users/:nickname", AdminAPIController, :user_show)
+     post("/users/follow", AdminAPIController, :user_follow)
+     post("/users/unfollow", AdminAPIController, :user_unfollow)
  
+     # TODO: to be removed at version 1.0
      delete("/user", AdminAPIController, :user_delete)
-     patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
      post("/user", AdminAPIController, :user_create)
+     delete("/users", AdminAPIController, :user_delete)
+     post("/users", AdminAPIController, :user_create)
+     patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
      put("/users/tag", AdminAPIController, :tag_users)
      delete("/users/tag", AdminAPIController, :untag_users)
  
+     # TODO: to be removed at version 1.0
      get("/permission_group/:nickname", AdminAPIController, :right_get)
      get("/permission_group/:nickname/:permission_group", AdminAPIController, :right_get)
      post("/permission_group/:nickname/:permission_group", AdminAPIController, :right_add)
      delete("/permission_group/:nickname/:permission_group", AdminAPIController, :right_delete)
  
-     put("/activation_status/:nickname", AdminAPIController, :set_activation_status)
+     get("/users/:nickname/permission_group", AdminAPIController, :right_get)
+     get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
+     post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
+     delete(
+       "/users/:nickname/permission_group/:permission_group",
+       AdminAPIController,
+       :right_delete
+     )
+     put("/users/:nickname/activation_status", AdminAPIController, :set_activation_status)
  
      post("/relay", AdminAPIController, :relay_follow)
      delete("/relay", AdminAPIController, :relay_unfollow)
  
-     get("/invite_token", AdminAPIController, :get_invite_token)
-     get("/invites", AdminAPIController, :invites)
-     post("/revoke_invite", AdminAPIController, :revoke_invite)
-     post("/email_invite", AdminAPIController, :email_invite)
+     get("/users/invite_token", AdminAPIController, :get_invite_token)
+     get("/users/invites", AdminAPIController, :invites)
+     post("/users/revoke_invite", AdminAPIController, :revoke_invite)
+     post("/users/email_invite", AdminAPIController, :email_invite)
  
+     # TODO: to be removed at version 1.0
      get("/password_reset", AdminAPIController, :get_password_reset)
+     get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
+     get("/users", AdminAPIController, :list_users)
+     get("/users/:nickname", AdminAPIController, :user_show)
    end
  
    scope "/", Pleroma.Web.TwitterAPI do
        post("/change_password", UtilController, :change_password)
        post("/delete_account", UtilController, :delete_account)
        put("/notification_settings", UtilController, :update_notificaton_settings)
 +      post("/disable_account", UtilController, :disable_account)
      end
  
      scope [] do
  
        get("/suggestions", MastodonAPIController, :suggestions)
  
+       get("/conversations", MastodonAPIController, :conversations)
+       post("/conversations/:id/read", MastodonAPIController, :conversation_read)
        get("/endorsements", MastodonAPIController, :empty_array)
  
        get("/pleroma/flavour", MastodonAPIController, :get_flavour)
    scope "/api/v1", Pleroma.Web.MastodonAPI do
      pipe_through(:api)
  
+     post("/accounts", MastodonAPIController, :account_register)
      get("/instance", MastodonAPIController, :masto_instance)
      get("/instance/peers", MastodonAPIController, :peers)
      post("/apps", MastodonAPIController, :create_app)
index 1e48b0b39e5ac848e7b59ceeb849d5faf9f11ad8,1362ef57cf81894c2251721e98511844a7733eae..41e1c287744b48813b0f1a582b420e4855adf341
@@@ -128,7 -128,7 +128,7 @@@ defmodule Pleroma.Web.TwitterAPI.Twitte
      end
    end
  
-   def register_user(params) do
+   def register_user(params, opts \\ []) do
      token = params["token"]
  
      params = %{
        # I have no idea how this error handling works
        {:error, %{error: Jason.encode!(%{captcha: [error]})}}
      else
-       registrations_open = Pleroma.Config.get([:instance, :registrations_open])
-       registration_process(registrations_open, params, token)
+       registration_process(
+         params,
+         %{
+           registrations_open: Pleroma.Config.get([:instance, :registrations_open]),
+           token: token
+         },
+         opts
+       )
      end
    end
  
-   defp registration_process(registration_open, params, token)
-        when registration_open == false or is_nil(registration_open) do
+   defp registration_process(params, %{registrations_open: true}, opts) do
+     create_user(params, opts)
+   end
+   defp registration_process(params, %{token: token}, opts) do
      invite =
        unless is_nil(token) do
          Repo.get_by(UserInviteToken, %{token: token})
  
        invite when valid_invite? ->
          UserInviteToken.update_usage!(invite)
-         create_user(params)
+         create_user(params, opts)
  
        _ ->
          {:error, "Expired token"}
      end
    end
  
-   defp registration_process(true, params, _token) do
-     create_user(params)
-   end
-   defp create_user(params) do
-     changeset = User.register_changeset(%User{}, params)
+   defp create_user(params, opts) do
+     changeset = User.register_changeset(%User{}, params, opts)
  
      case User.register(changeset) do
        {:ok, user} ->
    def get_user(user \\ nil, params) do
      case params do
        %{"user_id" => user_id} ->
 -        case target = User.get_cached_by_nickname_or_id(user_id) do
 +        case User.get_cached_by_nickname_or_id(user_id) do
            nil ->
              {:error, "No user with such user_id"}
  
 -          _ ->
 -            {:ok, target}
 +          %User{info: %{deactivated: true}} ->
 +            {:error, "User has been disabled"}
 +
 +          user ->
 +            {:ok, user}
          end
  
        %{"screen_name" => nickname} ->
diff --combined test/user_test.exs
index 00c06dfaac6a5c2689a849ebbc51e89ed76dcaab,60de0206e551552594cd59d05f827a9ebc072112..0b65e89e9bdceaffe3aa86f2477935c5ac6a209a
@@@ -8,7 -8,6 +8,7 @@@ defmodule Pleroma.UserTest d
    alias Pleroma.Object
    alias Pleroma.Repo
    alias Pleroma.User
 +  alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.CommonAPI
  
    use Pleroma.DataCase
    test "fetches correct profile for nickname beginning with number" do
      # Use old-style integer ID to try to reproduce the problem
      user = insert(:user, %{id: 1080})
 -    userwithnumbers = insert(:user, %{nickname: "#{user.id}garbage"})
 -    assert userwithnumbers == User.get_cached_by_nickname_or_id(userwithnumbers.nickname)
 +    user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"})
 +    assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname)
    end
  
    describe "user registration" do
      end
  
      test "it creates confirmed user if :confirmed option is given" do
-       changeset = User.register_changeset(%User{}, @full_user_data, confirmed: true)
+       changeset = User.register_changeset(%User{}, @full_user_data, need_confirmation: false)
        assert changeset.valid?
  
        {:ok, user} = Repo.insert(changeset)
      assert addressed in recipients
    end
  
 -  test ".deactivate can de-activate then re-activate a user" do
 -    user = insert(:user)
 -    assert false == user.info.deactivated
 -    {:ok, user} = User.deactivate(user)
 -    assert true == user.info.deactivated
 -    {:ok, user} = User.deactivate(user, false)
 -    assert false == user.info.deactivated
 +  describe ".deactivate" do
 +    test "can de-activate then re-activate a user" do
 +      user = insert(:user)
 +      assert false == user.info.deactivated
 +      {:ok, user} = User.deactivate(user)
 +      assert true == user.info.deactivated
 +      {:ok, user} = User.deactivate(user, false)
 +      assert false == user.info.deactivated
 +    end
 +
 +    test "hide a user from followers " do
 +      user = insert(:user)
 +      user2 = insert(:user)
 +
 +      {:ok, user} = User.follow(user, user2)
 +      {:ok, _user} = User.deactivate(user)
 +
 +      info = User.get_cached_user_info(user2)
 +
 +      assert info.follower_count == 0
 +      assert {:ok, []} = User.get_followers(user2)
 +    end
 +
 +    test "hide a user from friends" do
 +      user = insert(:user)
 +      user2 = insert(:user)
 +
 +      {:ok, user2} = User.follow(user2, user)
 +      assert User.following_count(user2) == 1
 +
 +      {:ok, _user} = User.deactivate(user)
 +
 +      info = User.get_cached_user_info(user2)
 +
 +      assert info.following_count == 0
 +      assert User.following_count(user2) == 0
 +      assert {:ok, []} = User.get_friends(user2)
 +    end
 +
 +    test "hide a user's statuses from timelines and notifications" do
 +      user = insert(:user)
 +      user2 = insert(:user)
 +
 +      {:ok, user2} = User.follow(user2, user)
 +
 +      {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}"})
 +
++      activity = Repo.preload(activity, :bookmark)
++
 +      [notification] = Pleroma.Notification.for_user(user2)
 +      assert notification.activity.id == activity.id
 +
-       assert [activity] == ActivityPub.fetch_public_activities(%{})
++      assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark)
 +
 +      assert [activity] ==
 +               ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
 +               |> ActivityPub.contain_timeline(user2)
 +
 +      {:ok, _user} = User.deactivate(user)
 +
 +      assert [] == ActivityPub.fetch_public_activities(%{})
 +      assert [] == Pleroma.Notification.for_user(user2)
 +
 +      assert [] ==
 +               ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
 +               |> ActivityPub.contain_timeline(user2)
 +    end
    end
  
    test ".delete_user_activities deletes all create activities" do