diff --git a/config/config.md b/config/config.md
index 47e838dd6..d90d18566 100644
--- a/config/config.md
+++ b/config/config.md
@@ -39,6 +39,7 @@ Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
   * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production
   * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See ``:mrf_simple`` section)
   * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See ``:mrf_rejectnonpublic`` section)
+  * `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:.
 * `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
 * `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``
diff --git a/config/test.exs b/config/test.exs
index 6f6227c20..ca10a616c 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -27,6 +27,12 @@ config :pleroma, :websub, Pleroma.Web.WebsubMock
 config :pleroma, :ostatus, Pleroma.Web.OStatusMock
 config :tesla, adapter: Tesla.Mock
 
+config :web_push_encryption, :vapid_details,
+  subject: "mailto:administrator@example.com",
+  public_key:
+    "BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4",
+  private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA"
+
 try do
   import_config "test.secret.exs"
 rescue
diff --git a/lib/mix/tasks/pleroma/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex
index df9d1ad65..0cd572797 100644
--- a/lib/mix/tasks/pleroma/sample_config.eex
+++ b/lib/mix/tasks/pleroma/sample_config.eex
@@ -29,6 +29,12 @@ config :pleroma, Pleroma.Repo,
   hostname: "<%= dbhost %>",
   pool_size: 10
 
+# Configure web push notifications
+config :web_push_encryption, :vapid_details,
+  subject: "mailto:<%= email %>",
+  public_key: "<%= web_push_public_key %>",
+  private_key: "<%= web_push_private_key %>"
+
 # Enable Strict-Transport-Security once SSL is working:
 # config :pleroma, :http_security,
 #   sts: true
@@ -54,9 +60,9 @@ config :pleroma, Pleroma.Repo,
 
 
 # Configure Openstack Swift support if desired.
-# 
-# Many openstack deployments are different, so config is left very open with 
-# no assumptions made on which provider you're using. This should allow very 
+#
+# Many openstack deployments are different, so config is left very open with
+# no assumptions made on which provider you're using. This should allow very
 # wide support without needing separate handlers for OVH, Rackspace, etc.
 #
 # config :pleroma, Pleroma.Uploaders.Swift,
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index cc68d9669..0b0ec0197 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -66,7 +66,8 @@ defmodule Pleroma.Application do
         ),
         worker(Pleroma.Web.Federator.RetryQueue, []),
         worker(Pleroma.Web.Federator, []),
-        worker(Pleroma.Stats, [])
+        worker(Pleroma.Stats, []),
+        worker(Pleroma.Web.Push, [])
       ] ++
         streamer_child() ++
         chat_child() ++
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 1a5c07c8a..5b03e9aeb 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -114,7 +114,7 @@ defmodule Pleroma.Formatter do
 
     subs =
       subs ++
-        Enum.map(mentions, fn {match, %User{ap_id: ap_id, info: info}, uuid} ->
+        Enum.map(mentions, fn {match, %User{id: id, ap_id: ap_id, info: info}, uuid} ->
           ap_id =
             if is_binary(info.source_data["url"]) do
               info.source_data["url"]
@@ -125,7 +125,7 @@ defmodule Pleroma.Formatter do
           short_match = String.split(match, "@") |> tl() |> hd()
 
           {uuid,
-           "<span><a class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"}
+           "<span><a data-user='#{id}' class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"}
         end)
 
     {subs, uuid_text}
@@ -147,7 +147,11 @@ defmodule Pleroma.Formatter do
     subs =
       subs ++
         Enum.map(tags, fn {tag_text, tag, uuid} ->
-          url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{tag_text}</a>"
+          url =
+            "<a data-tag='#{tag}' href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{
+              tag_text
+            }</a>"
+
           {uuid, url}
         end)
 
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 1b920d7fd..5daaa5e69 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -45,7 +45,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
   Meta.strip_comments()
 
   # links
-  Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
+  Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
   Meta.allow_tag_with_these_attributes("a", ["name", "title"])
 
   # paragraphs and linebreaks
@@ -86,7 +86,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
   Meta.remove_cdata_sections_before_scrub()
   Meta.strip_comments()
 
-  Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
+  Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
   Meta.allow_tag_with_these_attributes("a", ["name", "title"])
 
   Meta.allow_tag_with_these_attributes("abbr", ["title"])
diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex
index 5e8f2aabd..db46f9e55 100644
--- a/lib/pleroma/http/connection.ex
+++ b/lib/pleroma/http/connection.ex
@@ -3,7 +3,12 @@ defmodule Pleroma.HTTP.Connection do
   Connection for http-requests.
   """
 
-  @hackney_options [pool: :default]
+  @hackney_options [
+    pool: :default,
+    timeout: 10000,
+    recv_timeout: 20000,
+    follow_redirect: true
+  ]
   @adapter Application.get_env(:tesla, :adapter)
 
   @doc """
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index a3aeb1221..a40b8f8c9 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -110,6 +110,7 @@ defmodule Pleroma.Notification do
       notification = %Notification{user_id: user.id, activity: activity}
       {:ok, notification} = Repo.insert(notification)
       Pleroma.Web.Streamer.stream("user", notification)
+      Pleroma.Web.Push.send(notification)
       notification
     end
   end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 03a75dfbd..31c8dd5bd 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -1,6 +1,6 @@
 defmodule Pleroma.Object do
   use Ecto.Schema
-  alias Pleroma.{Repo, Object, Activity}
+  alias Pleroma.{Repo, Object, User, Activity}
   import Ecto.{Query, Changeset}
 
   schema "objects" do
@@ -31,6 +31,13 @@ defmodule Pleroma.Object do
   def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id)
   def normalize(_), do: nil
 
+  # Owned objects can only be mutated by their owner
+  def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
+    do: actor == ap_id
+
+  # Legacy objects can be mutated by anybody
+  def authorize_mutation(%Object{}, %User{}), do: true
+
   if Mix.env() == :test do
     def get_cached_by_ap_id(ap_id) do
       get_by_ap_id(ap_id)
diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex
index 630f15eec..8b99a74d1 100644
--- a/lib/pleroma/plugs/oauth_plug.ex
+++ b/lib/pleroma/plugs/oauth_plug.ex
@@ -1,30 +1,70 @@
 defmodule Pleroma.Plugs.OAuthPlug do
   import Plug.Conn
-  alias Pleroma.User
-  alias Pleroma.Repo
-  alias Pleroma.Web.OAuth.Token
+  import Ecto.Query
 
-  def init(options) do
-    options
-  end
+  alias Pleroma.{
+    User,
+    Repo,
+    Web.OAuth.Token
+  }
+
+  @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
+
+  def init(options), do: options
 
   def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
 
   def call(conn, _) do
-    token =
-      case get_req_header(conn, "authorization") do
-        ["Bearer " <> header] -> header
-        _ -> get_session(conn, :oauth_token)
-      end
-
-    with token when not is_nil(token) <- token,
-         %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
-         %User{} = user <- Repo.get(User, user_id),
-         false <- !!user.info.deactivated do
+    with {:ok, token} <- fetch_token(conn),
+         {:ok, user} <- fetch_user(token) do
       conn
+      |> assign(:token, token)
       |> assign(:user, user)
     else
       _ -> conn
     end
   end
+
+  # Gets user by token
+  #
+  @spec fetch_user(String.t()) :: {:ok, User.t()} | nil
+  defp fetch_user(token) do
+    query = from(q in Token, where: q.token == ^token, preload: [:user])
+
+    with %Token{user: %{info: %{deactivated: false} = _} = user} <- Repo.one(query) do
+      {:ok, user}
+    end
+  end
+
+  # Gets token from session by :oauth_token key
+  #
+  @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
+  defp fetch_token_from_session(conn) do
+    case get_session(conn, :oauth_token) do
+      nil -> :no_token_found
+      token -> {:ok, token}
+    end
+  end
+
+  # Gets token from headers
+  #
+  @spec fetch_token(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
+  defp fetch_token(%Plug.Conn{} = conn) do
+    headers = get_req_header(conn, "authorization")
+
+    with :no_token_found <- fetch_token(headers),
+         do: fetch_token_from_session(conn)
+  end
+
+  @spec fetch_token(Keyword.t()) :: :no_token_found | {:ok, String.t()}
+  defp fetch_token([]), do: :no_token_found
+
+  defp fetch_token([token | tail]) do
+    trimmed_token = String.trim(token)
+
+    case Regex.run(@realm_reg, trimmed_token) do
+      [_, match] -> {:ok, String.trim(match)}
+      _ -> fetch_token(tail)
+    end
+  end
 end
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
index ad9dc82fe..4ca84152a 100644
--- a/lib/pleroma/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy.ex
@@ -56,7 +56,7 @@ defmodule Pleroma.ReverseProxy do
   @hackney Application.get_env(:pleroma, :hackney, :hackney)
   @httpoison Application.get_env(:pleroma, :httpoison, HTTPoison)
 
-  @default_hackney_options [{:follow_redirect, true}]
+  @default_hackney_options []
 
   @inline_content_types [
     "image/gif",
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 74ae5ef0d..9da674982 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -62,10 +62,6 @@ defmodule Pleroma.User do
     |> validate_required([:following])
   end
 
-  def info_changeset(struct, params \\ %{}) do
-    raise "NOT VALID ANYMORE"
-  end
-
   def user_info(%User{} = user) do
     oneself = if user.local, do: 1, else: 0
 
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index 49b2f0eda..d81b45b8d 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -24,6 +24,7 @@ defmodule Pleroma.User.Info do
     field(:topic, :string, default: nil)
     field(:hub, :string, default: nil)
     field(:salmon, :string, default: nil)
+    field(:hide_network, :boolean, default: false)
 
     # Found in the wild
     # ap_id -> Where is this used?
@@ -135,6 +136,7 @@ defmodule Pleroma.User.Info do
       :no_rich_text,
       :default_scope,
       :banner,
+      :hide_network,
       :background
     ])
   end
@@ -147,6 +149,11 @@ defmodule Pleroma.User.Info do
     ])
   end
 
+  def mastodon_settings_update(info, params) do
+    info
+    |> cast(params, [:settings])
+  end
+
   def set_source_data(info, source_data) do
     params = %{source_data: source_data}
 
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 60253a715..bf81d8039 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -574,7 +574,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   def upload(file, opts \\ []) do
     with {:ok, data} <- Upload.store(file, opts) do
-      Repo.insert(%Object{data: data})
+      obj_data =
+        if opts[:actor] do
+          Map.put(data, "actor", opts[:actor])
+        else
+          data
+        end
+
+      Repo.insert(%Object{data: obj_data})
     end
   end
 
@@ -765,10 +772,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
          {:ok, %{body: body, status: code}} when code in 200..299 <-
            @httpoison.get(
              id,
-             [Accept: "application/activity+json"],
-             follow_redirect: true,
-             timeout: 10000,
-             recv_timeout: 20000
+             [{:Accept, "application/activity+json"}]
            ),
          {:ok, data} <- Jason.decode(body),
          :ok <- Transmogrifier.contain_origin_from_id(id, data) do
diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
new file mode 100644
index 000000000..c8c74ede6
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
@@ -0,0 +1,40 @@
+defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
+  alias Pleroma.Object
+
+  @behaviour Pleroma.Web.ActivityPub.MRF
+
+  @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless])
+  def filter_by_summary(
+        %{"summary" => parent_summary} = parent,
+        %{"summary" => child_summary} = child
+      )
+      when not is_nil(child_summary) and byte_size(child_summary) > 0 and
+             not is_nil(parent_summary) and byte_size(parent_summary) > 0 do
+    if (child_summary == parent_summary and not Regex.match?(@reply_prefix, child_summary)) or
+         (Regex.match?(@reply_prefix, parent_summary) &&
+            Regex.replace(@reply_prefix, parent_summary, "") == child_summary) do
+      Map.put(child, "summary", "re: " <> child_summary)
+    else
+      child
+    end
+  end
+
+  def filter_by_summary(parent, child), do: child
+
+  def filter(%{"type" => activity_type} = object) when activity_type == "Create" do
+    child = object["object"]
+    in_reply_to = Object.normalize(child["inReplyTo"])
+
+    child =
+      if(in_reply_to,
+        do: filter_by_summary(in_reply_to.data, child),
+        else: child
+      )
+
+    object = Map.put(object, "object", child)
+
+    {:ok, object}
+  end
+
+  def filter(object), do: {:ok, object}
+end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index aaa777602..869934172 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -82,7 +82,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
     query = from(user in query, select: [:ap_id])
     following = Repo.all(query)
 
-    collection(following, "#{user.ap_id}/following", page)
+    collection(following, "#{user.ap_id}/following", page, !user.info.hide_network)
     |> Map.merge(Utils.make_json_ld_header())
   end
 
@@ -95,7 +95,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
       "id" => "#{user.ap_id}/following",
       "type" => "OrderedCollection",
       "totalItems" => length(following),
-      "first" => collection(following, "#{user.ap_id}/following", 1)
+      "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_network)
     }
     |> Map.merge(Utils.make_json_ld_header())
   end
@@ -105,7 +105,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
     query = from(user in query, select: [:ap_id])
     followers = Repo.all(query)
 
-    collection(followers, "#{user.ap_id}/followers", page)
+    collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_network)
     |> Map.merge(Utils.make_json_ld_header())
   end
 
@@ -118,7 +118,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
       "id" => "#{user.ap_id}/followers",
       "type" => "OrderedCollection",
       "totalItems" => length(followers),
-      "first" => collection(followers, "#{user.ap_id}/followers", 1)
+      "first" => collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_network)
     }
     |> Map.merge(Utils.make_json_ld_header())
   end
@@ -172,7 +172,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
     end
   end
 
-  def collection(collection, iri, page, total \\ nil) do
+  def collection(collection, iri, page, show_items \\ true, total \\ nil) do
     offset = (page - 1) * 10
     items = Enum.slice(collection, offset, 10)
     items = Enum.map(items, fn user -> user.ap_id end)
@@ -183,7 +183,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
       "type" => "OrderedCollectionPage",
       "partOf" => iri,
       "totalItems" => total,
-      "orderedItems" => items
+      "orderedItems" => if(show_items, do: items, else: [])
     }
 
     if offset < total do
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index ea64f163d..2d7b1a00c 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -2,13 +2,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   use Pleroma.Web, :controller
   alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
   alias Pleroma.Web
-  alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView, FilterView}
+
+  alias Pleroma.Web.MastodonAPI.{
+    StatusView,
+    AccountView,
+    MastodonView,
+    ListView,
+    FilterView,
+    PushSubscriptionView
+  }
+
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.OAuth.{Authorization, Token, App}
   alias Pleroma.Web.MediaProxy
-  alias Comeonin.Pbkdf2
+
   import Ecto.Query
   require Logger
 
@@ -433,33 +442,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     |> json([])
   end
 
-  def update_media(%{assigns: %{user: _}} = conn, data) do
+  def update_media(%{assigns: %{user: user}} = conn, data) do
     with %Object{} = object <- Repo.get(Object, data["id"]),
+         true <- Object.authorize_mutation(object, user),
          true <- is_binary(data["description"]),
          description <- data["description"] do
       new_data = %{object.data | "name" => description}
 
-      change = Object.change(object, %{data: new_data})
-      {:ok, _} = Repo.update(change)
+      {:ok, _} =
+        object
+        |> Object.change(%{data: new_data})
+        |> Repo.update()
 
-      data =
-        new_data
-        |> Map.put("id", object.id)
-
-      render(conn, StatusView, "attachment.json", %{attachment: data})
+      attachment_data = Map.put(new_data, "id", object.id)
+      render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
     end
   end
 
-  def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do
-    with {:ok, object} <- ActivityPub.upload(file, description: Map.get(data, "description")) do
-      change = Object.change(object, %{data: object.data})
-      {:ok, object} = Repo.update(change)
-
-      objdata =
-        object.data
-        |> Map.put("id", object.id)
-
-      render(conn, StatusView, "attachment.json", %{attachment: objdata})
+  def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
+    with {:ok, object} <-
+           ActivityPub.upload(file,
+             actor: User.ap_id(user),
+             description: Map.get(data, "description")
+           ) do
+      attachment_data = Map.put(object.data, "id", object.id)
+      render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
     end
   end
 
@@ -502,17 +509,30 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
   end
 
-  # TODO: Pagination
-  def followers(conn, %{"id" => id}) do
+  def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
     with %User{} = user <- Repo.get(User, id),
          {:ok, followers} <- User.get_followers(user) do
+      followers =
+        cond do
+          for_user && user.id == for_user.id -> followers
+          user.info.hide_network -> []
+          true -> followers
+        end
+
       render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
     end
   end
 
-  def following(conn, %{"id" => id}) do
+  def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
     with %User{} = user <- Repo.get(User, id),
          {:ok, followers} <- User.get_friends(user) do
+      followers =
+        cond do
+          for_user && user.id == for_user.id -> followers
+          user.info.hide_network -> []
+          true -> followers
+        end
+
       render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
     end
   end
@@ -959,9 +979,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
-    with new_info <- Map.put(user.info, "settings", settings),
-         change <- User.info_changeset(user, %{info: new_info}),
-         {:ok, _user} <- User.update_and_set_cache(change) do
+    info_cng = User.Info.mastodon_settings_update(user.info, settings)
+
+    with changeset <- User.update_changeset(user),
+         changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
+         {:ok, user} <- User.update_and_set_cache(changeset) do
       conn
       |> json(%{})
     else
@@ -1149,6 +1171,33 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     json(conn, %{})
   end
 
+  def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
+    Pleroma.Web.Push.Subscription.delete_if_exists(user, token)
+    {:ok, subscription} = Pleroma.Web.Push.Subscription.create(user, token, params)
+    view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
+    json(conn, view)
+  end
+
+  def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
+    subscription = Pleroma.Web.Push.Subscription.get(user, token)
+    view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
+    json(conn, view)
+  end
+
+  def update_push_subscription(
+        %{assigns: %{user: user, token: token}} = conn,
+        params
+      ) do
+    {:ok, subscription} = Pleroma.Web.Push.Subscription.update(user, token, params)
+    view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
+    json(conn, view)
+  end
+
+  def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
+    {:ok, _response} = Pleroma.Web.Push.Subscription.delete(user, token)
+    json(conn, %{})
+  end
+
   def errors(conn, _) do
     conn
     |> put_status(500)
@@ -1169,7 +1218,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
 
       with {:ok, %{status: 200, body: body}} <-
-             @httpoison.get(url, [], timeout: timeout, recv_timeout: timeout),
+             @httpoison.get(
+               url,
+               [],
+               adapter: [
+                 timeout: timeout,
+                 recv_timeout: timeout
+               ]
+             ),
            {:ok, data} <- Jason.decode(body) do
         data2 =
           Enum.slice(data, 0, limit)
diff --git a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex b/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
new file mode 100644
index 000000000..68bb45494
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
@@ -0,0 +1,12 @@
+defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
+  use Pleroma.Web, :view
+  alias Pleroma.Web.MastodonAPI.PushSubscriptionView
+
+  def render("push_subscription.json", %{subscription: subscription}) do
+    %{
+      id: to_string(subscription.id),
+      endpoint: subscription.endpoint,
+      alerts: Map.get(subscription.data, "alerts")
+    }
+  end
+end
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
index 67df354db..53d71440e 100644
--- a/lib/pleroma/web/ostatus/ostatus.ex
+++ b/lib/pleroma/web/ostatus/ostatus.ex
@@ -349,12 +349,7 @@ defmodule Pleroma.Web.OStatus do
          {:ok, %{body: body, status: code}} when code in 200..299 <-
            @httpoison.get(
              url,
-             [Accept: "application/atom+xml"],
-             follow_redirect: true,
-             adapter: [
-               timeout: 10000,
-               recv_timeout: 20000
-             ]
+             [{:Accept, "application/atom+xml"}]
            ) do
       Logger.debug("Got document from #{url}, handling...")
       handle_incoming(body)
@@ -369,8 +364,7 @@ defmodule Pleroma.Web.OStatus do
     Logger.debug("Trying to fetch #{url}")
 
     with true <- String.starts_with?(url, "http"),
-         {:ok, %{body: body}} <-
-           @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000),
+         {:ok, %{body: body}} <- @httpoison.get(url, []),
          {:ok, atom_url} <- get_atom_url(body) do
       fetch_activity_from_atom_url(atom_url)
     else
diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex
new file mode 100644
index 000000000..5a873ec19
--- /dev/null
+++ b/lib/pleroma/web/push/push.ex
@@ -0,0 +1,116 @@
+defmodule Pleroma.Web.Push do
+  use GenServer
+
+  alias Pleroma.{Repo, User}
+  alias Pleroma.Web.Push.Subscription
+
+  require Logger
+  import Ecto.Query
+
+  @types ["Create", "Follow", "Announce", "Like"]
+
+  @gcm_api_key nil
+
+  def start_link() do
+    GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
+  end
+
+  def init(:ok) do
+    case Application.get_env(:web_push_encryption, :vapid_details) do
+      nil ->
+        Logger.warn(
+          "VAPID key pair is not found. Please, add VAPID configuration to config. Run `mix web_push.gen.keypair` mix task to create a key pair"
+        )
+
+        :ignore
+
+      _ ->
+        {:ok, %{}}
+    end
+  end
+
+  def send(notification) do
+    if Application.get_env(:web_push_encryption, :vapid_details) do
+      GenServer.cast(Pleroma.Web.Push, {:send, notification})
+    end
+  end
+
+  def handle_cast(
+        {:send, %{activity: %{data: %{"type" => type}}, user_id: user_id} = notification},
+        state
+      )
+      when type in @types do
+    actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
+    body = notification |> format(actor) |> Jason.encode!()
+
+    Subscription
+    |> where(user_id: ^user_id)
+    |> Repo.all()
+    |> Enum.each(fn record ->
+      subscription = %{
+        keys: %{
+          p256dh: record.key_p256dh,
+          auth: record.key_auth
+        },
+        endpoint: record.endpoint
+      }
+
+      case WebPushEncryption.send_web_push(body, subscription, @gcm_api_key) do
+        {:ok, %{status_code: code}} when 400 <= code and code < 500 ->
+          Logger.debug("Removing subscription record")
+          Repo.delete!(record)
+          :ok
+
+        {:ok, %{status_code: code}} when 200 <= code and code < 300 ->
+          :ok
+
+        {:ok, %{status_code: code}} ->
+          Logger.error("Web Push Nonification failed with code: #{code}")
+          :error
+
+        _ ->
+          Logger.error("Web Push Nonification failed with unknown error")
+          :error
+      end
+    end)
+
+    {:noreply, state}
+  end
+
+  def handle_cast({:send, _}, state) do
+    Logger.warn("Unknown notification type")
+    {:noreply, state}
+  end
+
+  def format(%{activity: %{data: %{"type" => "Create"}}}, actor) do
+    %{
+      title: "New Mention",
+      body: "@#{actor.nickname} has mentiond you",
+      icon: User.avatar_url(actor)
+    }
+  end
+
+  def format(%{activity: %{data: %{"type" => "Follow"}}}, actor) do
+    %{
+      title: "New Follower",
+      body: "@#{actor.nickname} has followed you",
+      icon: User.avatar_url(actor)
+    }
+  end
+
+  def format(%{activity: %{data: %{"type" => "Announce"}}}, actor) do
+    %{
+      title: "New Announce",
+      body: "@#{actor.nickname} has announced your post",
+      icon: User.avatar_url(actor)
+    }
+  end
+
+  def format(%{activity: %{data: %{"type" => "Like"}}}, actor) do
+    %{
+      title: "New Like",
+      body: "@#{actor.nickname} has liked your post",
+      icon: User.avatar_url(actor)
+    }
+  end
+end
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
new file mode 100644
index 000000000..cfab7a98e
--- /dev/null
+++ b/lib/pleroma/web/push/subscription.ex
@@ -0,0 +1,66 @@
+defmodule Pleroma.Web.Push.Subscription do
+  use Ecto.Schema
+  import Ecto.Changeset
+  alias Pleroma.{Repo, User}
+  alias Pleroma.Web.OAuth.Token
+  alias Pleroma.Web.Push.Subscription
+
+  schema "push_subscriptions" do
+    belongs_to(:user, User)
+    belongs_to(:token, Token)
+    field(:endpoint, :string)
+    field(:key_p256dh, :string)
+    field(:key_auth, :string)
+    field(:data, :map, default: %{})
+
+    timestamps()
+  end
+
+  @supported_alert_types ~w[follow favourite mention reblog]
+
+  defp alerts(%{"data" => %{"alerts" => alerts}}) do
+    alerts = Map.take(alerts, @supported_alert_types)
+    %{"alerts" => alerts}
+  end
+
+  def create(
+        %User{} = user,
+        %Token{} = token,
+        %{
+          "subscription" => %{
+            "endpoint" => endpoint,
+            "keys" => %{"auth" => key_auth, "p256dh" => key_p256dh}
+          }
+        } = params
+      ) do
+    Repo.insert(%Subscription{
+      user_id: user.id,
+      token_id: token.id,
+      endpoint: endpoint,
+      key_auth: key_auth,
+      key_p256dh: key_p256dh,
+      data: alerts(params)
+    })
+  end
+
+  def get(%User{id: user_id}, %Token{id: token_id}) do
+    Repo.get_by(Subscription, user_id: user_id, token_id: token_id)
+  end
+
+  def update(user, token, params) do
+    get(user, token)
+    |> change(data: alerts(params))
+    |> Repo.update()
+  end
+
+  def delete(user, token) do
+    Repo.delete(get(user, token))
+  end
+
+  def delete_if_exists(user, token) do
+    case get(user, token) do
+      nil -> {:ok, nil}
+      sub -> Repo.delete(sub)
+    end
+  end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index d6a9d5779..75d965c6d 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -198,6 +198,11 @@ defmodule Pleroma.Web.Router do
     put("/filters/:id", MastodonAPIController, :update_filter)
     delete("/filters/:id", MastodonAPIController, :delete_filter)
 
+    post("/push/subscription", MastodonAPIController, :create_push_subscription)
+    get("/push/subscription", MastodonAPIController, :get_push_subscription)
+    put("/push/subscription", MastodonAPIController, :update_push_subscription)
+    delete("/push/subscription", MastodonAPIController, :delete_push_subscription)
+
     get("/suggestions", MastodonAPIController, :suggestions)
 
     get("/endorsements", MastodonAPIController, :empty_array)
@@ -324,6 +329,7 @@ defmodule Pleroma.Web.Router do
 
     post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
     post("/media/upload", TwitterAPI.Controller, :upload_json)
+    post("/media/metadata/create", TwitterAPI.Controller, :update_media)
 
     post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
     post("/favorites/create", TwitterAPI.Controller, :favorite)
diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex
index 97251c05e..0e2cfddd0 100644
--- a/lib/pleroma/web/salmon/salmon.ex
+++ b/lib/pleroma/web/salmon/salmon.ex
@@ -162,12 +162,7 @@ defmodule Pleroma.Web.Salmon do
            poster.(
              salmon,
              feed,
-             [{"Content-Type", "application/magic-envelope+xml"}],
-             adapter: [
-               timeout: 10000,
-               recv_timeout: 20000,
-               pool: :default
-             ]
+             [{"Content-Type", "application/magic-envelope+xml"}]
            ) do
       Logger.debug(fn -> "Pushed to #{salmon}, code #{code}" end)
     else
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index b0ed8387e..092779010 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -157,13 +157,17 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
         |> send_resp(200, response)
 
       _ ->
+        vapid_public_key =
+          Keyword.get(Application.get_env(:web_push_encryption, :vapid_details), :public_key)
+
         data = %{
           name: Keyword.get(instance, :name),
           description: Keyword.get(instance, :description),
           server: Web.base_url(),
           textlimit: to_string(Keyword.get(instance, :limit)),
           closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"),
-          private: if(Keyword.get(instance, :public, true), do: "0", else: "1")
+          private: if(Keyword.get(instance, :public, true), do: "0", else: "1"),
+          vapidPublicKey: vapid_public_key
         }
 
         pleroma_fe = %{
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index c19a4f084..9c485d965 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -93,8 +93,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
     end
   end
 
-  def upload(%Plug.Upload{} = file, format \\ "xml") do
-    {:ok, object} = ActivityPub.upload(file)
+  def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do
+    {:ok, object} = ActivityPub.upload(file, actor: User.ap_id(user))
 
     url = List.first(object.data["url"])
     href = url["href"]
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 961250d92..0ccf937b0 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -4,7 +4,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
   alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
-  alias Pleroma.{Repo, Activity, User, Notification}
+  alias Pleroma.{Repo, Activity, Object, User, Notification}
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Utils
   alias Ecto.Changeset
@@ -226,16 +226,51 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     end
   end
 
-  def upload(conn, %{"media" => media}) do
-    response = TwitterAPI.upload(media)
+  @doc """
+  Updates metadata of uploaded media object.
+  Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
+  """
+  def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
+    object = Repo.get(Object, id)
+    description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
+
+    {conn, status, response_body} =
+      cond do
+        !object ->
+          {halt(conn), :not_found, ""}
+
+        !Object.authorize_mutation(object, user) ->
+          {halt(conn), :forbidden, "You can only update your own uploads."}
+
+        !is_binary(description) ->
+          {conn, :not_modified, ""}
+
+        true ->
+          new_data = Map.put(object.data, "name", description)
+
+          {:ok, _} =
+            object
+            |> Object.change(%{data: new_data})
+            |> Repo.update()
+
+          {conn, :no_content, ""}
+      end
+
+    conn
+    |> put_status(status)
+    |> json(response_body)
+  end
+
+  def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
+    response = TwitterAPI.upload(media, user)
 
     conn
     |> put_resp_content_type("application/atom+xml")
     |> send_resp(200, response)
   end
 
-  def upload_json(conn, %{"media" => media}) do
-    response = TwitterAPI.upload(media, "json")
+  def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
+    response = TwitterAPI.upload(media, user, "json")
 
     conn
     |> json_reply(200, response)
@@ -340,18 +375,32 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     end
   end
 
-  def followers(conn, params) do
-    with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
+  def followers(%{assigns: %{user: for_user}} = conn, params) do
+    with {:ok, user} <- TwitterAPI.get_user(for_user, params),
          {:ok, followers} <- User.get_followers(user) do
+      followers =
+        cond do
+          for_user && user.id == for_user.id -> followers
+          user.info.hide_network -> []
+          true -> followers
+        end
+
       render(conn, UserView, "index.json", %{users: followers, for: conn.assigns[:user]})
     else
       _e -> bad_request_reply(conn, "Can't get followers")
     end
   end
 
-  def friends(conn, params) do
+  def friends(%{assigns: %{user: for_user}} = conn, params) do
     with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
          {:ok, friends} <- User.get_friends(user) do
+      friends =
+        cond do
+          for_user && user.id == for_user.id -> friends
+          user.info.hide_network -> []
+          true -> friends
+        end
+
       render(conn, UserView, "index.json", %{users: friends, for: conn.assigns[:user]})
     else
       _e -> bad_request_reply(conn, "Can't get friends")
@@ -429,7 +478,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
 
   defp build_info_cng(user, params) do
     info_params =
-      ["no_rich_text", "locked"]
+      ["no_rich_text", "locked", "hide_network"]
       |> Enum.reduce(%{}, fn key, res ->
         if value = params[key] do
           Map.put(res, key, value == "true")
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 99c65a6bf..0ff3b8b5f 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -221,7 +221,7 @@ defmodule Pleroma.Web.WebFinger do
 
   def find_lrdd_template(domain) do
     with {:ok, %{status: status, body: body}} when status in 200..299 <-
-           @httpoison.get("http://#{domain}/.well-known/host-meta", [], follow_redirect: true) do
+           @httpoison.get("http://#{domain}/.well-known/host-meta", []) do
       get_template_from_xml(body)
     else
       _ ->
diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex
index 0761b5475..8cb07006f 100644
--- a/lib/pleroma/web/websub/websub.ex
+++ b/lib/pleroma/web/websub/websub.ex
@@ -264,11 +264,6 @@ defmodule Pleroma.Web.Websub do
              [
                {"Content-Type", "application/atom+xml"},
                {"X-Hub-Signature", "sha1=#{signature}"}
-             ],
-             adapter: [
-               timeout: 10000,
-               recv_timeout: 20000,
-               pool: :default
              ]
            ) do
       Logger.info(fn -> "Pushed to #{callback}, code #{code}" end)
diff --git a/mix.exs b/mix.exs
index 1a28b6710..bd9bce766 100644
--- a/mix.exs
+++ b/mix.exs
@@ -68,7 +68,8 @@ defmodule Pleroma.Mixfile do
       {:crypt,
        git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"},
       {:cors_plug, "~> 1.5"},
-      {:ex_doc, "> 0.18.3 and < 0.20.0", only: :dev, runtime: false}
+      {:ex_doc, "> 0.18.3 and < 0.20.0", only: :dev, runtime: false},
+      {:web_push_encryption, "~> 0.2.1"}
     ]
   end
 
diff --git a/mix.lock b/mix.lock
index 4c70061d3..ff8e9fdca 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,4 +1,5 @@
 %{
+  "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "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"},
@@ -25,6 +26,7 @@
   "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
   "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
   "jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [: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"},
   "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
   "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
   "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"},
@@ -52,4 +54,5 @@
   "tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
   "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
   "unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [: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"},
 }
diff --git a/priv/repo/migrations/20180918182427_create_push_subscriptions.exs b/priv/repo/migrations/20180918182427_create_push_subscriptions.exs
new file mode 100644
index 000000000..0cc7afa54
--- /dev/null
+++ b/priv/repo/migrations/20180918182427_create_push_subscriptions.exs
@@ -0,0 +1,18 @@
+defmodule Pleroma.Repo.Migrations.CreatePushSubscriptions do
+  use Ecto.Migration
+
+  def change do
+    create table("push_subscriptions") do
+      add :user_id, references("users", on_delete: :delete_all)
+      add :token_id, references("oauth_tokens", on_delete: :delete_all)
+      add :endpoint, :string
+      add :key_p256dh, :string
+      add :key_auth, :string
+      add :data, :map
+
+      timestamps()
+    end
+
+    create index("push_subscriptions", [:user_id, :token_id], unique: true)
+  end
+end
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index 5d745510f..abb9d882c 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -15,7 +15,7 @@ defmodule Pleroma.FormatterTest do
       text = "I love #cofe and #2hu"
 
       expected_text =
-        "I love <a href='http://localhost:4001/tag/cofe' rel='tag'>#cofe</a> and <a href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a>"
+        "I love <a data-tag='cofe' href='http://localhost:4001/tag/cofe' rel='tag'>#cofe</a> and <a data-tag='2hu' href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a>"
 
       tags = Formatter.parse_tags(text)
 
@@ -128,11 +128,11 @@ defmodule Pleroma.FormatterTest do
       Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end)
 
       expected_text =
-        "<span><a class='mention' href='#{gsimg.ap_id}'>@<span>gsimg</span></a></span> According to <span><a class='mention' href='#{
-          "https://archeme/@archaeme"
-        }'>@<span>archaeme</span></a></span>, that is @daggsy. Also hello <span><a class='mention' href='#{
-          archaeme_remote.ap_id
-        }'>@<span>archaeme</span></a></span>"
+        "<span><a data-user='#{gsimg.id}' class='mention' href='#{gsimg.ap_id}'>@<span>gsimg</span></a></span> According to <span><a data-user='#{
+          archaeme.id
+        }' class='mention' href='#{"https://archeme/@archaeme"}'>@<span>archaeme</span></a></span>, that is @daggsy. Also hello <span><a data-user='#{
+          archaeme_remote.id
+        }' class='mention' href='#{archaeme_remote.ap_id}'>@<span>archaeme</span></a></span>"
 
       assert expected_text == Formatter.finalize({subs, text})
     end
@@ -150,7 +150,7 @@ defmodule Pleroma.FormatterTest do
       Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end)
 
       expected_text =
-        "<span><a class='mention' href='#{mike.ap_id}'>@<span>mike</span></a></span> test"
+        "<span><a data-user='#{mike.id}' class='mention' href='#{mike.ap_id}'>@<span>mike</span></a></span> test"
 
       assert expected_text == Formatter.finalize({subs, text})
     end
@@ -166,7 +166,9 @@ defmodule Pleroma.FormatterTest do
       assert length(subs) == 1
       Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end)
 
-      expected_text = "<span><a class='mention' href='#{o.ap_id}'>@<span>o</span></a></span> hi"
+      expected_text =
+        "<span><a data-user='#{o.id}' class='mention' href='#{o.ap_id}'>@<span>o</span></a></span> hi"
+
       assert expected_text == Formatter.finalize({subs, text})
     end
 
diff --git a/test/plugs/oauth_plug_test.exs b/test/plugs/oauth_plug_test.exs
new file mode 100644
index 000000000..4dd12f207
--- /dev/null
+++ b/test/plugs/oauth_plug_test.exs
@@ -0,0 +1,56 @@
+defmodule Pleroma.Plugs.OAuthPlugTest do
+  use Pleroma.Web.ConnCase, async: true
+
+  alias Pleroma.Plugs.OAuthPlug
+  import Pleroma.Factory
+
+  @session_opts [
+    store: :cookie,
+    key: "_test",
+    signing_salt: "cooldude"
+  ]
+
+  setup %{conn: conn} do
+    user = insert(:user)
+    {:ok, %{token: token}} = Pleroma.Web.OAuth.Token.create_token(insert(:oauth_app), user)
+    %{user: user, token: token, conn: conn}
+  end
+
+  test "with valid token(uppercase), it assigns the user", %{conn: conn} = opts do
+    conn =
+      conn
+      |> put_req_header("authorization", "BEARER #{opts[:token]}")
+      |> OAuthPlug.call(%{})
+
+    assert conn.assigns[:user] == opts[:user]
+  end
+
+  test "with valid token(downcase), it assigns the user", %{conn: conn} = opts do
+    conn =
+      conn
+      |> put_req_header("authorization", "bearer #{opts[:token]}")
+      |> OAuthPlug.call(%{})
+
+    assert conn.assigns[:user] == opts[:user]
+  end
+
+  test "with invalid token, it not assigns the user", %{conn: conn} do
+    conn =
+      conn
+      |> put_req_header("authorization", "bearer TTTTT")
+      |> OAuthPlug.call(%{})
+
+    refute conn.assigns[:user]
+  end
+
+  test "when token is missed but token in session, it assigns the user", %{conn: conn} = opts do
+    conn =
+      conn
+      |> Plug.Session.call(Plug.Session.init(@session_opts))
+      |> fetch_session()
+      |> put_session(:oauth_token, opts[:token])
+      |> OAuthPlug.call(%{})
+
+    assert conn.assigns[:user] == opts[:user]
+  end
+end
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
index 8eff0fd94..9dde6b5e5 100644
--- a/test/support/data_case.ex
+++ b/test/support/data_case.ex
@@ -36,6 +36,23 @@ defmodule Pleroma.DataCase do
     :ok
   end
 
+  def ensure_local_uploader(_context) do
+    uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
+    filters = Pleroma.Config.get([Pleroma.Upload, :filters])
+
+    unless uploader == Pleroma.Uploaders.Local || filters != [] do
+      Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
+      Pleroma.Config.put([Pleroma.Upload, :filters], [])
+
+      on_exit(fn ->
+        Pleroma.Config.put([Pleroma.Upload, :uploader], uploader)
+        Pleroma.Config.put([Pleroma.Upload, :filters], filters)
+      end)
+    end
+
+    :ok
+  end
+
   @doc """
   A helper that transform changeset errors to a map of messages.
 
diff --git a/test/upload_test.exs b/test/upload_test.exs
index b2ce755d2..f2cad4cf0 100644
--- a/test/upload_test.exs
+++ b/test/upload_test.exs
@@ -3,22 +3,7 @@ defmodule Pleroma.UploadTest do
   use Pleroma.DataCase
 
   describe "Storing a file with the Local uploader" do
-    setup do
-      uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
-      filters = Pleroma.Config.get([Pleroma.Upload, :filters])
-
-      unless uploader == Pleroma.Uploaders.Local || filters != [] do
-        Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
-        Pleroma.Config.put([Pleroma.Upload, :filters], [])
-
-        on_exit(fn ->
-          Pleroma.Config.put([Pleroma.Upload, :uploader], uploader)
-          Pleroma.Config.put([Pleroma.Upload, :filters], filters)
-        end)
-      end
-
-      :ok
-    end
+    setup [:ensure_local_uploader]
 
     test "returns a media url" do
       File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index 980f43553..b4af2df5a 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -150,6 +150,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
       assert result["first"]["orderedItems"] == [user.ap_id]
     end
 
+    test "it returns returns empty if the user has 'hide_network' set", %{conn: conn} do
+      user = insert(:user)
+      user_two = insert(:user, %{info: %{hide_network: true}})
+      User.follow(user, user_two)
+
+      result =
+        conn
+        |> get("/users/#{user_two.nickname}/followers")
+        |> json_response(200)
+
+      assert result["first"]["orderedItems"] == []
+      assert result["totalItems"] == 1
+    end
+
     test "it works for more than 10 users", %{conn: conn} do
       user = insert(:user)
 
@@ -191,6 +205,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
       assert result["first"]["orderedItems"] == [user_two.ap_id]
     end
 
+    test "it returns returns empty if the user has 'hide_network' set", %{conn: conn} do
+      user = insert(:user, %{info: %{hide_network: true}})
+      user_two = insert(:user)
+      User.follow(user, user_two)
+
+      result =
+        conn
+        |> get("/users/#{user.nickname}/following")
+        |> json_response(200)
+
+      assert result["first"]["orderedItems"] == []
+      assert result["totalItems"] == 1
+    end
+
     test "it works for more than 10 users", %{conn: conn} do
       user = insert(:user)
 
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 7cd98cde8..092f0c9fc 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -2,7 +2,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
   use Pleroma.Web.ConnCase
 
   alias Pleroma.Web.TwitterAPI.TwitterAPI
-  alias Pleroma.{Repo, User, Activity, Notification}
+  alias Pleroma.{Repo, User, Object, Activity, Notification}
   alias Pleroma.Web.{OStatus, CommonAPI}
   alias Pleroma.Web.ActivityPub.ActivityPub
 
@@ -590,7 +590,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
         |> get("/api/v1/notifications")
 
       expected_response =
-        "hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
+        "hi <span><a data-user=\"#{user.id}\" href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
 
       assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
       assert response == expected_response
@@ -611,7 +611,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
         |> get("/api/v1/notifications/#{notification.id}")
 
       expected_response =
-        "hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
+        "hi <span><a data-user=\"#{user.id}\" href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
 
       assert %{"status" => %{"content" => response}} = json_response(conn, 200)
       assert response == expected_response
@@ -810,7 +810,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       }
 
       media =
-        TwitterAPI.upload(file, "json")
+        TwitterAPI.upload(file, user, "json")
         |> Poison.decode!()
 
       {:ok, image_post} =
@@ -965,6 +965,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
 
     assert media["type"] == "image"
     assert media["description"] == desc
+    assert media["id"]
+
+    object = Repo.get(Object, media["id"])
+    assert object.data["actor"] == User.ap_id(user)
   end
 
   test "hashtag timeline", %{conn: conn} do
@@ -1008,6 +1012,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     assert id == to_string(user.id)
   end
 
+  test "getting followers, hide_network", %{conn: conn} do
+    user = insert(:user)
+    other_user = insert(:user, %{info: %{hide_network: true}})
+    {:ok, user} = User.follow(user, other_user)
+
+    conn =
+      conn
+      |> get("/api/v1/accounts/#{other_user.id}/followers")
+
+    assert [] == json_response(conn, 200)
+  end
+
+  test "getting followers, hide_network, same user requesting", %{conn: conn} do
+    user = insert(:user)
+    other_user = insert(:user, %{info: %{hide_network: true}})
+    {:ok, user} = User.follow(user, other_user)
+
+    conn =
+      conn
+      |> assign(:user, other_user)
+      |> get("/api/v1/accounts/#{other_user.id}/followers")
+
+    refute [] == json_response(conn, 200)
+  end
+
   test "getting following", %{conn: conn} do
     user = insert(:user)
     other_user = insert(:user)
@@ -1021,6 +1050,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     assert id == to_string(other_user.id)
   end
 
+  test "getting following, hide_network", %{conn: conn} do
+    user = insert(:user, %{info: %{hide_network: true}})
+    other_user = insert(:user)
+    {:ok, user} = User.follow(user, other_user)
+
+    conn =
+      conn
+      |> get("/api/v1/accounts/#{user.id}/following")
+
+    assert [] == json_response(conn, 200)
+  end
+
+  test "getting following, hide_network, same user requesting", %{conn: conn} do
+    user = insert(:user, %{info: %{hide_network: true}})
+    other_user = insert(:user)
+    {:ok, user} = User.follow(user, other_user)
+
+    conn =
+      conn
+      |> assign(:user, user)
+      |> get("/api/v1/accounts/#{user.id}/following")
+
+    refute [] == json_response(conn, 200)
+  end
+
   test "following / unfollowing a user", %{conn: conn} do
     user = insert(:user)
     other_user = insert(:user)
@@ -1271,9 +1325,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       assert user = json_response(conn, 200)
 
       assert user["note"] ==
-               "I drink <a href=\"http://localhost:4001/tag/cofe\">#cofe</a> with <span><a href=\"#{
-                 user2.ap_id
-               }\">@<span>#{user2.nickname}</span></a></span>"
+               "I drink <a data-tag=\"cofe\" href=\"http://localhost:4001/tag/cofe\">#cofe</a> with <span><a data-user=\"#{
+                 user2.id
+               }\" href=\"#{user2.ap_id}\">@<span>#{user2.nickname}</span></a></span>"
     end
 
     test "updates the user's locking status", %{conn: conn} do
diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs
index a6495ffc1..4119d1dd8 100644
--- a/test/web/twitter_api/twitter_api_controller_test.exs
+++ b/test/web/twitter_api/twitter_api_controller_test.exs
@@ -861,6 +861,67 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
       result = json_response(conn, 200)
       assert Enum.sort(expected) == Enum.sort(result)
     end
+
+    test "it returns a given user's followers with user_id", %{conn: conn} do
+      user = insert(:user)
+      follower_one = insert(:user)
+      follower_two = insert(:user)
+      not_follower = insert(:user)
+
+      {:ok, follower_one} = User.follow(follower_one, user)
+      {:ok, follower_two} = User.follow(follower_two, user)
+
+      conn =
+        conn
+        |> assign(:user, not_follower)
+        |> get("/api/statuses/followers", %{"user_id" => user.id})
+
+      assert MapSet.equal?(
+               MapSet.new(json_response(conn, 200)),
+               MapSet.new(
+                 UserView.render("index.json", %{
+                   users: [follower_one, follower_two],
+                   for: not_follower
+                 })
+               )
+             )
+    end
+
+    test "it returns empty for a hidden network", %{conn: conn} do
+      user = insert(:user, %{info: %{hide_network: true}})
+      follower_one = insert(:user)
+      follower_two = insert(:user)
+      not_follower = insert(:user)
+
+      {:ok, follower_one} = User.follow(follower_one, user)
+      {:ok, follower_two} = User.follow(follower_two, user)
+
+      conn =
+        conn
+        |> assign(:user, not_follower)
+        |> get("/api/statuses/followers", %{"user_id" => user.id})
+
+      assert [] == json_response(conn, 200)
+    end
+
+    test "it returns the followers for a hidden network if requested by the user themselves", %{
+      conn: conn
+    } do
+      user = insert(:user, %{info: %{hide_network: true}})
+      follower_one = insert(:user)
+      follower_two = insert(:user)
+      not_follower = insert(:user)
+
+      {:ok, follower_one} = User.follow(follower_one, user)
+      {:ok, follower_two} = User.follow(follower_two, user)
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> get("/api/statuses/followers", %{"user_id" => user.id})
+
+      refute [] == json_response(conn, 200)
+    end
   end
 
   describe "GET /api/statuses/friends" do
@@ -905,6 +966,42 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
              )
     end
 
+    test "it returns empty for a hidden network", %{conn: conn} do
+      user = insert(:user, %{info: %{hide_network: true}})
+      followed_one = insert(:user)
+      followed_two = insert(:user)
+      not_followed = insert(:user)
+
+      {:ok, user} = User.follow(user, followed_one)
+      {:ok, user} = User.follow(user, followed_two)
+
+      conn =
+        conn
+        |> assign(:user, not_followed)
+        |> get("/api/statuses/friends", %{"user_id" => user.id})
+
+      assert [] == json_response(conn, 200)
+    end
+
+    test "it returns friends for a hidden network if the user themselves request it", %{
+      conn: conn
+    } do
+      user = insert(:user, %{info: %{hide_network: true}})
+      followed_one = insert(:user)
+      followed_two = insert(:user)
+      not_followed = insert(:user)
+
+      {:ok, user} = User.follow(user, followed_one)
+      {:ok, user} = User.follow(user, followed_two)
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> get("/api/statuses/friends", %{"user_id" => user.id})
+
+      refute [] == json_response(conn, 200)
+    end
+
     test "it returns a given user's friends with screen_name", %{conn: conn} do
       user = insert(:user)
       followed_one = insert(:user)
@@ -969,11 +1066,37 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
       assert user.name == "new name"
 
       assert user.bio ==
-               "hi <span><a class='mention' href='#{user2.ap_id}'>@<span>#{user2.nickname}</span></a></span>"
+               "hi <span><a data-user='#{user2.id}' class='mention' href='#{user2.ap_id}'>@<span>#{
+                 user2.nickname
+               }</span></a></span>"
 
       assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
     end
 
+    test "it sets and un-sets hide_network", %{conn: conn} do
+      user = insert(:user)
+
+      conn
+      |> assign(:user, user)
+      |> post("/api/account/update_profile.json", %{
+        "hide_network" => "true"
+      })
+
+      user = Repo.get!(User, user.id)
+      assert user.info.hide_network == true
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/account/update_profile.json", %{
+          "hide_network" => "false"
+        })
+
+      user = Repo.get!(User, user.id)
+      assert user.info.hide_network == false
+      assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
+    end
+
     test "it locks an account", %{conn: conn} do
       user = insert(:user)
 
@@ -1253,4 +1376,82 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
       assert [user.id, user_two.id, user_three.id] == Enum.map(resp, fn %{"id" => id} -> id end)
     end
   end
+
+  describe "POST /api/media/upload" do
+    setup context do
+      Pleroma.DataCase.ensure_local_uploader(context)
+    end
+
+    test "it performs the upload and sets `data[actor]` with AP id of uploader user", %{
+      conn: conn
+    } do
+      user = insert(:user)
+
+      upload_filename = "test/fixtures/image_tmp.jpg"
+      File.cp!("test/fixtures/image.jpg", upload_filename)
+
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname(upload_filename),
+        filename: "image.jpg"
+      }
+
+      response =
+        conn
+        |> assign(:user, user)
+        |> put_req_header("content-type", "application/octet-stream")
+        |> post("/api/media/upload", %{
+          "media" => file
+        })
+        |> json_response(:ok)
+
+      assert response["media_id"]
+      object = Repo.get(Object, response["media_id"])
+      assert object
+      assert object.data["actor"] == User.ap_id(user)
+    end
+  end
+
+  describe "POST /api/media/metadata/create" do
+    setup do
+      object = insert(:note)
+      user = User.get_by_ap_id(object.data["actor"])
+      %{object: object, user: user}
+    end
+
+    test "it returns :forbidden status on attempt to modify someone else's upload", %{
+      conn: conn,
+      object: object
+    } do
+      initial_description = object.data["name"]
+      another_user = insert(:user)
+
+      conn
+      |> assign(:user, another_user)
+      |> post("/api/media/metadata/create", %{"media_id" => object.id})
+      |> json_response(:forbidden)
+
+      object = Repo.get(Object, object.id)
+      assert object.data["name"] == initial_description
+    end
+
+    test "it updates `data[name]` of referenced Object with provided value", %{
+      conn: conn,
+      object: object,
+      user: user
+    } do
+      description = "Informative description of the image. Initial value: #{object.data["name"]}}"
+
+      conn
+      |> assign(:user, user)
+      |> post("/api/media/metadata/create", %{
+        "media_id" => object.id,
+        "alt_text" => %{"text" => description}
+      })
+      |> json_response(:no_content)
+
+      object = Repo.get(Object, object.id)
+      assert object.data["name"] == description
+    end
+  end
 end
diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs
index 28230699f..05f832de0 100644
--- a/test/web/twitter_api/twitter_api_test.exs
+++ b/test/web/twitter_api/twitter_api_test.exs
@@ -10,7 +10,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
 
   test "create a status" do
     user = insert(:user)
-    _mentioned_user = insert(:user, %{nickname: "shp", ap_id: "shp"})
+    mentioned_user = insert(:user, %{nickname: "shp", ap_id: "shp"})
 
     object_data = %{
       "type" => "Image",
@@ -35,7 +35,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
     {:ok, activity = %Activity{}} = TwitterAPI.create_status(user, input)
 
     expected_text =
-      "Hello again, <span><a class='mention' href='shp'>@<span>shp</span></a></span>.&lt;script&gt;&lt;/script&gt;<br>This is on another :moominmamma: line. <a href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a> <a href='http://localhost:4001/tag/epic' rel='tag'>#epic</a> <a href='http://localhost:4001/tag/phantasmagoric' rel='tag'>#phantasmagoric</a><br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>"
+      "Hello again, <span><a data-user='#{mentioned_user.id}' class='mention' href='shp'>@<span>shp</span></a></span>.&lt;script&gt;&lt;/script&gt;<br>This is on another :moominmamma: line. <a data-tag='2hu' href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a> <a data-tag='epic' href='http://localhost:4001/tag/epic' rel='tag'>#epic</a> <a data-tag='phantasmagoric' href='http://localhost:4001/tag/phantasmagoric' rel='tag'>#phantasmagoric</a><br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>"
 
     assert get_in(activity.data, ["object", "content"]) == expected_text
     assert get_in(activity.data, ["object", "type"]) == "Note"
@@ -182,13 +182,15 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
   end
 
   test "upload a file" do
+    user = insert(:user)
+
     file = %Plug.Upload{
       content_type: "image/jpg",
       path: Path.absname("test/fixtures/image.jpg"),
       filename: "an_image.jpg"
     }
 
-    response = TwitterAPI.upload(file)
+    response = TwitterAPI.upload(file, user)
 
     assert is_binary(response)
   end
@@ -281,7 +283,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
     {:ok, user2} = TwitterAPI.register_user(data2)
 
     expected_text =
-      "<span><a class='mention' href='#{user1.ap_id}'>@<span>john</span></a></span> test"
+      "<span><a data-user='#{user1.id}' class='mention' href='#{user1.ap_id}'>@<span>john</span></a></span> test"
 
     assert user2.bio == expected_text
   end
diff --git a/test/web/twitter_api/views/activity_view_test.exs b/test/web/twitter_api/views/activity_view_test.exs
index 5cef06f88..bc36b0e90 100644
--- a/test/web/twitter_api/views/activity_view_test.exs
+++ b/test/web/twitter_api/views/activity_view_test.exs
@@ -47,7 +47,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do
       "repeated" => false,
       "statusnet_conversation_id" => convo_id,
       "statusnet_html" =>
-        "Hey <span><a href=\"#{other_user.ap_id}\">@<span>shp</span></a></span>!",
+        "Hey <span><a data-user=\"#{other_user.id}\" href=\"#{other_user.ap_id}\">@<span>shp</span></a></span>!",
       "tags" => [],
       "text" => "Hey @shp!",
       "uri" => activity.data["object"]["id"],