diff --git a/README.md b/README.md
index 234a4b6c4..d9896f7ba 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ Client applications that are known to work well:
 
 * Twidere
 * Tusky
+* Mastalab
 * Pawoo (Android + iOS)
 * Subway Tooter
 * Amaroq (iOS)
diff --git a/config/config.exs b/config/config.exs
index c0a936b17..8131d9b18 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -209,6 +209,8 @@
   ip: {0, 0, 0, 0},
   port: 9999
 
+config :pleroma, Pleroma.Web.Metadata, providers: [], unfurl_nsfw: false
+
 config :pleroma, :suggestions,
   enabled: false,
   third_party_engine:
diff --git a/docs/Pleroma-API.md b/docs/Pleroma-API.md
index da58babf9..0c4586dd3 100644
--- a/docs/Pleroma-API.md
+++ b/docs/Pleroma-API.md
@@ -15,6 +15,7 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
 * Params: none
 * Response: JSON
 * Example response: `{"kalsarikannit_f":"/finmoji/128px/kalsarikannit_f-128.png","perkele":"/finmoji/128px/perkele-128.png","blobdab":"/emoji/blobdab.png","happiness":"/finmoji/128px/happiness-128.png"}`
+* Note: Same data as Mastodon API’s `/api/v1/custom_emojis` but in a different format
 
 ## `/api/pleroma/follow_import`
 ### Imports your follows, for example from a Mastodon CSV file.
diff --git a/docs/config.md b/docs/config.md
index 3f4588299..26ce842d4 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -212,3 +212,9 @@ curl "http://localhost:4000/api/pleroma/admin/invite_token?admin_token=somerando
 * `max_jobs`: The maximum amount of parallel federation jobs running at the same time.
 * `initial_timeout`: The initial timeout in seconds
 * `max_retries`: The maximum number of times a federation job is retried
+
+## Pleroma.Web.Metadata
+* `providers`: a list of metadata providers to enable. Providers availible:
+  * Pleroma.Web.Metadata.Providers.OpenGraph
+  * Pleroma.Web.Metadata.Providers.TwitterCard
+* `unfurl_nsfw`: If set to `true` nsfw attachments will be shown in previews
diff --git a/installation/init.d/pleroma b/installation/init.d/pleroma
index 9582d65d4..2b211df65 100755
--- a/installation/init.d/pleroma
+++ b/installation/init.d/pleroma
@@ -12,7 +12,7 @@ export PORT=4000
 export MIX_ENV=prod
 
 # Ask process to terminate within 30 seconds, otherwise kill it
-retry="SIGTERM/30 SIGKILL/5"
+retry="SIGTERM/30/SIGKILL/5"
 
 pidfile="/var/run/pleroma.pid"
 
diff --git a/lib/pleroma/PasswordResetToken.ex b/lib/pleroma/PasswordResetToken.ex
index 1dccdadae..c3c0384d2 100644
--- a/lib/pleroma/PasswordResetToken.ex
+++ b/lib/pleroma/PasswordResetToken.ex
@@ -10,7 +10,7 @@ defmodule Pleroma.PasswordResetToken do
   alias Pleroma.{User, PasswordResetToken, Repo}
 
   schema "password_reset_tokens" do
-    belongs_to(:user, User)
+    belongs_to(:user, User, type: Pleroma.FlakeId)
     field(:token, :string)
     field(:used, :boolean, default: false)
 
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index cd61f6ac8..f0aa3ce97 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Activity do
   import Ecto.Query
 
   @type t :: %__MODULE__{}
+  @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
 
   # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
   @mastodon_notification_types %{
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index ad2797209..47c0e5b68 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -99,6 +99,7 @@ def start(_type, _args) do
           ],
           id: :cachex_idem
         ),
+        worker(Pleroma.FlakeId, []),
         worker(Pleroma.Web.Federator.RetryQueue, []),
         worker(Pleroma.Web.Federator, []),
         worker(Pleroma.Stats, []),
diff --git a/lib/pleroma/clippy.ex b/lib/pleroma/clippy.ex
new file mode 100644
index 000000000..4e9bdbe19
--- /dev/null
+++ b/lib/pleroma/clippy.ex
@@ -0,0 +1,155 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Clippy do
+  @moduledoc false
+  # No software is complete until they have a Clippy implementation.
+  # A ballmer peak _may_ be required to change this module.
+
+  def tip() do
+    tips()
+    |> Enum.random()
+    |> puts()
+  end
+
+  def tips() do
+    host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
+
+    [
+      "“πλήρωμα” is “pleroma” in greek",
+      "For an extended Pleroma Clippy Experience, use the “Redmond” themes in Pleroma FE settings",
+      "Staff accounts and MRF policies of Pleroma instances are disclosed on the NodeInfo endpoints for easy transparency!\n
+- https://catgirl.science/misc/nodeinfo.lua?#{host}
+- https://fediverse.network/#{host}/federation",
+      "Pleroma can federate to the Dark Web!\n
+- Tor: https://git.pleroma.social/pleroma/pleroma/wikis/Easy%20Onion%20Federation%20(Tor)
+- i2p: https://git.pleroma.social/pleroma/pleroma/wikis/I2p%20federation",
+      "Lists of Pleroma instances:\n\n- http://distsn.org/pleroma-instances.html\n- https://fediverse.network/pleroma\n- https://the-federation.info/pleroma",
+      "Pleroma uses the LitePub protocol - https://litepub.social",
+      "To receive more federated posts, subscribe to relays!\n
+- How-to: https://git.pleroma.social/pleroma/pleroma/wikis/Admin%20tasks#relay-managment
+- Relays: https://fediverse.network/activityrelay"
+    ]
+  end
+
+  @spec puts(String.t() | [[IO.ANSI.ansicode() | String.t(), ...], ...]) :: nil
+  def puts(text_or_lines) do
+    import IO.ANSI
+
+    lines =
+      if is_binary(text_or_lines) do
+        String.split(text_or_lines, ~r/\n/)
+      else
+        text_or_lines
+      end
+
+    longest_line_size =
+      lines
+      |> Enum.map(&charlist_count_text/1)
+      |> Enum.sort(&>=/2)
+      |> List.first()
+
+    pad_text = longest_line_size
+
+    pad =
+      for(_ <- 1..pad_text, do: "_")
+      |> Enum.join("")
+
+    pad_spaces =
+      for(_ <- 1..pad_text, do: " ")
+      |> Enum.join("")
+
+    spaces = "      "
+
+    pre_lines = [
+      "  /  \\#{spaces}  _#{pad}___",
+      "  |  |#{spaces} / #{pad_spaces}   \\"
+    ]
+
+    for l <- pre_lines do
+      IO.puts(l)
+    end
+
+    clippy_lines = [
+      "  #{bright()}@  @#{reset()}#{spaces} ",
+      "  || ||#{spaces}",
+      "  || ||   <--",
+      "  |\\_/|      ",
+      "  \\___/      "
+    ]
+
+    noclippy_line = "             "
+
+    env = %{
+      max_size: pad_text,
+      pad: pad,
+      pad_spaces: pad_spaces,
+      spaces: spaces,
+      pre_lines: pre_lines,
+      noclippy_line: noclippy_line
+    }
+
+    # surrond one/five line clippy with blank lines around to not fuck up the layout
+    #
+    # yes this fix sucks but it's good enough, have you ever seen a release of windows wihtout some butched
+    # features anyway?
+    lines =
+      if length(lines) == 1 or length(lines) == 5 do
+        [""] ++ lines ++ [""]
+      else
+        lines
+      end
+
+    clippy_line(lines, clippy_lines, env)
+  rescue
+    e ->
+      IO.puts("(Clippy crashed, sorry: #{inspect(e)})")
+      IO.puts(text_or_lines)
+  end
+
+  defp clippy_line([line | lines], [prefix | clippy_lines], env) do
+    IO.puts([prefix <> "| ", rpad_line(line, env.max_size)])
+    clippy_line(lines, clippy_lines, env)
+  end
+
+  # more text lines but clippy's complete
+  defp clippy_line([line | lines], [], env) do
+    IO.puts([env.noclippy_line, "| ", rpad_line(line, env.max_size)])
+
+    if lines == [] do
+      IO.puts(env.noclippy_line <> "\\_#{env.pad}___/")
+    end
+
+    clippy_line(lines, [], env)
+  end
+
+  # no more text lines but clippy's not complete
+  defp clippy_line([], [clippy | clippy_lines], env) do
+    if env.pad do
+      IO.puts(clippy <> "\\_#{env.pad}___/")
+      clippy_line([], clippy_lines, %{env | pad: nil})
+    else
+      IO.puts(clippy)
+      clippy_line([], clippy_lines, env)
+    end
+  end
+
+  defp clippy_line(_, _, _) do
+  end
+
+  defp rpad_line(line, max) do
+    pad = max - (charlist_count_text(line) - 2)
+    pads = Enum.join(for(_ <- 1..pad, do: " "))
+    [IO.ANSI.format(line), pads <> " |"]
+  end
+
+  defp charlist_count_text(line) do
+    if is_list(line) do
+      text = Enum.join(Enum.filter(line, &is_binary/1))
+      String.length(text)
+    else
+      String.length(line)
+    end
+  end
+end
diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index df5374a5c..308bd70e1 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Filter do
   alias Pleroma.{User, Repo}
 
   schema "filters" do
-    belongs_to(:user, User)
+    belongs_to(:user, User, type: Pleroma.FlakeId)
     field(:filter_id, :integer)
     field(:hide, :boolean, default: false)
     field(:whole_word, :boolean, default: true)
diff --git a/lib/pleroma/flake_id.ex b/lib/pleroma/flake_id.ex
new file mode 100644
index 000000000..69ab8ccf9
--- /dev/null
+++ b/lib/pleroma/flake_id.ex
@@ -0,0 +1,172 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.FlakeId do
+  @moduledoc """
+  Flake is a decentralized, k-ordered id generation service.
+
+  Adapted from:
+
+  * [flaky](https://github.com/nirvana/flaky), released under the terms of the Truly Free License,
+  * [Flake](https://github.com/boundary/flake), Copyright 2012, Boundary, Apache License, Version 2.0
+  """
+
+  @type t :: binary
+
+  @behaviour Ecto.Type
+  use GenServer
+  require Logger
+  alias __MODULE__
+  import Kernel, except: [to_string: 1]
+
+  defstruct node: nil, time: 0, sq: 0
+
+  @doc "Converts a binary Flake to a String"
+  def to_string(<<0::integer-size(64), id::integer-size(64)>>) do
+    Kernel.to_string(id)
+  end
+
+  def to_string(flake = <<_::integer-size(64), _::integer-size(48), _::integer-size(16)>>) do
+    encode_base62(flake)
+  end
+
+  def to_string(s), do: s
+
+  def from_string(int) when is_integer(int) do
+    from_string(Kernel.to_string(int))
+  end
+
+  for i <- [-1, 0] do
+    def from_string(unquote(i)), do: <<0::integer-size(128)>>
+    def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>>
+  end
+
+  def from_string(flake = <<_::integer-size(128)>>), do: flake
+
+  def from_string(string) when is_binary(string) and byte_size(string) < 18 do
+    case Integer.parse(string) do
+      {id, _} -> <<0::integer-size(64), id::integer-size(64)>>
+      _ -> nil
+    end
+  end
+
+  def from_string(string) do
+    string |> decode_base62 |> from_integer
+  end
+
+  def to_integer(<<integer::integer-size(128)>>), do: integer
+
+  def from_integer(integer) do
+    <<_time::integer-size(64), _node::integer-size(48), _seq::integer-size(16)>> =
+      <<integer::integer-size(128)>>
+  end
+
+  @doc "Generates a Flake"
+  @spec get :: binary
+  def get, do: to_string(:gen_server.call(:flake, :get))
+
+  # -- Ecto.Type API
+  @impl Ecto.Type
+  def type, do: :uuid
+
+  @impl Ecto.Type
+  def cast(value) do
+    {:ok, FlakeId.to_string(value)}
+  end
+
+  @impl Ecto.Type
+  def load(value) do
+    {:ok, FlakeId.to_string(value)}
+  end
+
+  @impl Ecto.Type
+  def dump(value) do
+    {:ok, FlakeId.from_string(value)}
+  end
+
+  def autogenerate(), do: get()
+
+  # -- GenServer API
+  def start_link do
+    :gen_server.start_link({:local, :flake}, __MODULE__, [], [])
+  end
+
+  @impl GenServer
+  def init([]) do
+    {:ok, %FlakeId{node: worker_id(), time: time()}}
+  end
+
+  @impl GenServer
+  def handle_call(:get, _from, state) do
+    {flake, new_state} = get(time(), state)
+    {:reply, flake, new_state}
+  end
+
+  # Matches when the calling time is the same as the state time. Incr. sq
+  defp get(time, %FlakeId{time: time, node: node, sq: seq}) do
+    new_state = %FlakeId{time: time, node: node, sq: seq + 1}
+    {gen_flake(new_state), new_state}
+  end
+
+  # Matches when the times are different, reset sq
+  defp get(newtime, %FlakeId{time: time, node: node}) when newtime > time do
+    new_state = %FlakeId{time: newtime, node: node, sq: 0}
+    {gen_flake(new_state), new_state}
+  end
+
+  # Error when clock is running backwards
+  defp get(newtime, %FlakeId{time: time}) when newtime < time do
+    {:error, :clock_running_backwards}
+  end
+
+  defp gen_flake(%FlakeId{time: time, node: node, sq: seq}) do
+    <<time::integer-size(64), node::integer-size(48), seq::integer-size(16)>>
+  end
+
+  defp nthchar_base62(n) when n <= 9, do: ?0 + n
+  defp nthchar_base62(n) when n <= 35, do: ?A + n - 10
+  defp nthchar_base62(n), do: ?a + n - 36
+
+  defp encode_base62(<<integer::integer-size(128)>>) do
+    integer
+    |> encode_base62([])
+    |> List.to_string()
+  end
+
+  defp encode_base62(int, acc) when int < 0, do: encode_base62(-int, acc)
+  defp encode_base62(int, []) when int == 0, do: '0'
+  defp encode_base62(int, acc) when int == 0, do: acc
+
+  defp encode_base62(int, acc) do
+    r = rem(int, 62)
+    id = div(int, 62)
+    acc = [nthchar_base62(r) | acc]
+    encode_base62(id, acc)
+  end
+
+  defp decode_base62(s) do
+    decode_base62(String.to_charlist(s), 0)
+  end
+
+  defp decode_base62([c | cs], acc) when c >= ?0 and c <= ?9,
+    do: decode_base62(cs, 62 * acc + (c - ?0))
+
+  defp decode_base62([c | cs], acc) when c >= ?A and c <= ?Z,
+    do: decode_base62(cs, 62 * acc + (c - ?A + 10))
+
+  defp decode_base62([c | cs], acc) when c >= ?a and c <= ?z,
+    do: decode_base62(cs, 62 * acc + (c - ?a + 36))
+
+  defp decode_base62([], acc), do: acc
+
+  defp time do
+    {mega_seconds, seconds, micro_seconds} = :erlang.timestamp()
+    1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000)
+  end
+
+  defp worker_id() do
+    <<worker::integer-size(48)>> = :crypto.strong_rand_bytes(6)
+    worker
+  end
+end
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 37737853a..386096a52 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -43,7 +43,7 @@ def emojify(text) do
 
   def emojify(text, nil), do: text
 
-  def emojify(text, emoji) do
+  def emojify(text, emoji, strip \\ false) do
     Enum.reduce(emoji, text, fn {emoji, file}, text ->
       emoji = HTML.strip_tags(emoji)
       file = HTML.strip_tags(file)
@@ -51,14 +51,24 @@ def emojify(text, emoji) do
       String.replace(
         text,
         ":#{emoji}:",
-        "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
-          MediaProxy.url(file)
-        }' />"
+        if not strip do
+          "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
+            MediaProxy.url(file)
+          }' />"
+        else
+          ""
+        end
       )
       |> HTML.filter_tags()
     end)
   end
 
+  def demojify(text) do
+    emojify(text, Emoji.get_all(), true)
+  end
+
+  def demojify(text, nil), do: text
+
   def get_emoji(text) when is_binary(text) do
     Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
   end
@@ -189,4 +199,16 @@ def finalize({subs, text}) do
       String.replace(result_text, uuid, replacement)
     end)
   end
+
+  def truncate(text, max_length \\ 200, omission \\ "...") do
+    # Remove trailing whitespace
+    text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}")
+
+    if String.length(text) < max_length do
+      text
+    else
+      length_with_omission = max_length - String.length(omission)
+      String.slice(text, 0, length_with_omission) <> omission
+    end
+  end
 end
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index f5c6e5033..fb602d6b6 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -58,6 +58,20 @@ defp generate_scrubber_signature(scrubbers) do
       "#{signature}#{to_string(scrubber)}"
     end)
   end
+
+  def extract_first_external_url(object, content) do
+    key = "URL|#{object.id}"
+
+    Cachex.fetch!(:scrubber_cache, key, fn _key ->
+      result =
+        content
+        |> Floki.filter_out("a.mention")
+        |> Floki.attribute("a", "href")
+        |> Enum.at(0)
+
+      {:commit, result}
+    end)
+  end
 end
 
 defmodule Pleroma.HTML.Scrubber.TwitterText do
diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex
index a75dc006e..ca66c6916 100644
--- a/lib/pleroma/list.ex
+++ b/lib/pleroma/list.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.List do
   alias Pleroma.{User, Repo, Activity}
 
   schema "lists" do
-    belongs_to(:user, Pleroma.User)
+    belongs_to(:user, User, type: Pleroma.FlakeId)
     field(:title, :string)
     field(:following, {:array, :string}, default: [])
 
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index c7d01f63b..2364d36da 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -4,13 +4,14 @@
 
 defmodule Pleroma.Notification do
   use Ecto.Schema
-  alias Pleroma.{User, Activity, Notification, Repo, Object}
+  alias Pleroma.{User, Activity, Notification, Repo}
+  alias Pleroma.Web.CommonAPI.Utils
   import Ecto.Query
 
   schema "notifications" do
     field(:seen, :boolean, default: false)
-    belongs_to(:user, Pleroma.User)
-    belongs_to(:activity, Pleroma.Activity)
+    belongs_to(:user, User, type: Pleroma.FlakeId)
+    belongs_to(:activity, Activity, type: Pleroma.FlakeId)
 
     timestamps()
   end
@@ -34,7 +35,8 @@ def for_user(user, opts \\ %{}) do
         n in Notification,
         where: n.user_id == ^user.id,
         order_by: [desc: n.id],
-        preload: [:activity],
+        join: activity in assoc(n, :activity),
+        preload: [activity: activity],
         limit: 20
       )
 
@@ -65,7 +67,8 @@ def get(%{id: user_id} = _user, id) do
       from(
         n in Notification,
         where: n.id == ^id,
-        preload: [:activity]
+        join: activity in assoc(n, :activity),
+        preload: [activity: activity]
       )
 
     notification = Repo.one(query)
@@ -96,7 +99,7 @@ def dismiss(%{id: user_id} = _user, id) do
     end
   end
 
-  def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity)
+  def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
       when type in ["Create", "Like", "Announce", "Follow"] do
     users = get_notified_from_activity(activity)
 
@@ -132,54 +135,12 @@ def get_notified_from_activity(
       when type in ["Create", "Like", "Announce", "Follow"] do
     recipients =
       []
-      |> maybe_notify_to_recipients(activity)
-      |> maybe_notify_mentioned_recipients(activity)
+      |> Utils.maybe_notify_to_recipients(activity)
+      |> Utils.maybe_notify_mentioned_recipients(activity)
       |> Enum.uniq()
 
     User.get_users_from_set(recipients, local_only)
   end
 
   def get_notified_from_activity(_, _local_only), do: []
-
-  defp maybe_notify_to_recipients(
-         recipients,
-         %Activity{data: %{"to" => to, "type" => _type}} = _activity
-       ) do
-    recipients ++ to
-  end
-
-  defp maybe_notify_mentioned_recipients(
-         recipients,
-         %Activity{data: %{"to" => _to, "type" => type} = data} = _activity
-       )
-       when type == "Create" do
-    object = Object.normalize(data["object"])
-
-    object_data =
-      cond do
-        !is_nil(object) ->
-          object.data
-
-        is_map(data["object"]) ->
-          data["object"]
-
-        true ->
-          %{}
-      end
-
-    tagged_mentions = maybe_extract_mentions(object_data)
-
-    recipients ++ tagged_mentions
-  end
-
-  defp maybe_notify_mentioned_recipients(recipients, _), do: recipients
-
-  defp maybe_extract_mentions(%{"tag" => tag}) do
-    tag
-    |> Enum.filter(fn x -> is_map(x) end)
-    |> Enum.filter(fn x -> x["type"] == "Mention" end)
-    |> Enum.map(fn x -> x["href"] end)
-  end
-
-  defp maybe_extract_mentions(_), do: []
 end
diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex
index 437aa95b3..945a1d49f 100644
--- a/lib/pleroma/plugs/oauth_plug.ex
+++ b/lib/pleroma/plugs/oauth_plug.ex
@@ -33,7 +33,12 @@ def call(conn, _) do
   #
   @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil
   defp fetch_user_and_token(token) do
-    query = from(q in Token, where: q.token == ^token, preload: [:user])
+    query =
+      from(t in Token,
+        where: t.token == ^token,
+        join: user in assoc(t, :user),
+        preload: [user: user]
+      )
 
     with %Token{user: %{info: %{deactivated: false} = _} = user} = token_record <- Repo.one(query) do
       {:ok, user, token_record}
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 18137106e..1468cc133 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -17,6 +17,8 @@ defmodule Pleroma.User do
 
   @type t :: %__MODULE__{}
 
+  @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
+
   @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
 
   @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
@@ -404,6 +406,10 @@ def locked?(%User{} = user) do
     user.info.locked || false
   end
 
+  def get_by_id(id) do
+    Repo.get_by(User, id: id)
+  end
+
   def get_by_ap_id(ap_id) do
     Repo.get_by(User, ap_id: ap_id)
   end
@@ -439,11 +445,33 @@ def get_cached_by_ap_id(ap_id) do
     Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
   end
 
+  def get_cached_by_id(id) do
+    key = "id:#{id}"
+
+    ap_id =
+      Cachex.fetch!(:user_cache, key, fn _ ->
+        user = get_by_id(id)
+
+        if user do
+          Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
+          {:commit, user.ap_id}
+        else
+          {:ignore, ""}
+        end
+      end)
+
+    get_cached_by_ap_id(ap_id)
+  end
+
   def get_cached_by_nickname(nickname) do
     key = "nickname:#{nickname}"
     Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
   end
 
+  def get_cached_by_nickname_or_id(nickname_or_id) do
+    get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
+  end
+
   def get_by_nickname(nickname) do
     Repo.get_by(User, nickname: nickname) ||
       if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index fb1791c20..c6c923aac 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -31,7 +31,7 @@ defmodule Pleroma.User.Info do
     field(:hub, :string, default: nil)
     field(:salmon, :string, default: nil)
     field(:hide_network, :boolean, default: false)
-    field(:pinned_activities, {:array, :integer}, default: [])
+    field(:pinned_activities, {:array, :string}, default: [])
 
     # Found in the wild
     # ap_id -> Where is this used?
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 6cad02da6..22c7824fa 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -36,6 +36,14 @@ defp get_recipients(%{"type" => "Announce"} = data) do
     {recipients, to, cc}
   end
 
+  defp get_recipients(%{"type" => "Create"} = data) do
+    to = data["to"] || []
+    cc = data["cc"] || []
+    actor = data["actor"] || []
+    recipients = (to ++ cc ++ [actor]) |> Enum.uniq()
+    {recipients, to, cc}
+  end
+
   defp get_recipients(data) do
     to = data["to"] || []
     cc = data["cc"] || []
@@ -56,7 +64,7 @@ defp check_actor_is_active(actor) do
     end
   end
 
-  defp check_remote_limit(%{"object" => %{"content" => content}}) do
+  defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
     limit = Pleroma.Config.get([:instance, :remote_limit])
     String.length(content) <= limit
   end
@@ -410,13 +418,42 @@ def fetch_user_activities(user, reading_user, params \\ %{}) do
     |> Enum.reverse()
   end
 
+  defp restrict_since(query, %{"since_id" => ""}), do: query
+
   defp restrict_since(query, %{"since_id" => since_id}) do
     from(activity in query, where: activity.id > ^since_id)
   end
 
   defp restrict_since(query, _), do: query
 
-  defp restrict_tag(query, %{"tag" => tag}) do
+  defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
+       when is_list(tag_reject) and tag_reject != [] do
+    from(
+      activity in query,
+      where: fragment("(not (? #> '{\"object\",\"tag\"}') \\?| ?)", activity.data, ^tag_reject)
+    )
+  end
+
+  defp restrict_tag_reject(query, _), do: query
+
+  defp restrict_tag_all(query, %{"tag_all" => tag_all})
+       when is_list(tag_all) and tag_all != [] do
+    from(
+      activity in query,
+      where: fragment("(? #> '{\"object\",\"tag\"}') \\?& ?", activity.data, ^tag_all)
+    )
+  end
+
+  defp restrict_tag_all(query, _), do: query
+
+  defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
+    from(
+      activity in query,
+      where: fragment("(? #> '{\"object\",\"tag\"}') \\?| ?", activity.data, ^tag)
+    )
+  end
+
+  defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
     from(
       activity in query,
       where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data)
@@ -465,6 +502,8 @@ defp restrict_local(query, %{"local_only" => true}) do
 
   defp restrict_local(query, _), do: query
 
+  defp restrict_max(query, %{"max_id" => ""}), do: query
+
   defp restrict_max(query, %{"max_id" => max_id}) do
     from(activity in query, where: activity.id < ^max_id)
   end
@@ -563,6 +602,8 @@ def fetch_activities_query(recipients, opts \\ %{}) do
     base_query
     |> restrict_recipients(recipients, opts["user"])
     |> restrict_tag(opts)
+    |> restrict_tag_reject(opts)
+    |> restrict_tag_all(opts)
     |> restrict_since(opts)
     |> restrict_local(opts)
     |> restrict_limit(opts)
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
new file mode 100644
index 000000000..7c6ad582a
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
+  alias Pleroma.User
+
+  @behaviour Pleroma.Web.ActivityPub.MRF
+
+  # XXX: this should become User.normalize_by_ap_id() or similar, really.
+  defp normalize_by_ap_id(%{"id" => id}), do: User.get_cached_by_ap_id(id)
+  defp normalize_by_ap_id(uri) when is_binary(uri), do: User.get_cached_by_ap_id(uri)
+  defp normalize_by_ap_id(_), do: nil
+
+  defp score_nickname("followbot@" <> _), do: 1.0
+  defp score_nickname("federationbot@" <> _), do: 1.0
+  defp score_nickname("federation_bot@" <> _), do: 1.0
+  defp score_nickname(_), do: 0.0
+
+  defp score_displayname("federation bot"), do: 1.0
+  defp score_displayname("federationbot"), do: 1.0
+  defp score_displayname("fedibot"), do: 1.0
+  defp score_displayname(_), do: 0.0
+
+  defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do
+    nick_score =
+      nickname
+      |> String.downcase()
+      |> score_nickname()
+
+    name_score =
+      displayname
+      |> String.downcase()
+      |> score_displayname()
+
+    nick_score + name_score
+  end
+
+  defp determine_if_followbot(_), do: 0.0
+
+  @impl true
+  def filter(%{"type" => "Follow", "actor" => actor_id} = message) do
+    %User{} = actor = normalize_by_ap_id(actor_id)
+
+    score = determine_if_followbot(actor)
+
+    # TODO: scan biography data for keywords and score it somehow.
+    if score < 0.8 do
+      {:ok, message}
+    else
+      {:reject, nil}
+    end
+  end
+
+  @impl true
+  def filter(message), do: {:ok, message}
+end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 46b1646f7..c2ced51d8 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -141,11 +141,11 @@ def fix_actor(%{"attributedTo" => actor} = object) do
     |> Map.put("actor", get_actor(%{"actor" => actor}))
   end
 
-  def fix_likes(%{"likes" => likes} = object)
-      when is_bitstring(likes) do
-    # Check for standardisation
-    # This is what Peertube does
-    # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
+  # Check for standardisation
+  # This is what Peertube does
+  # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
+  # Prismo returns only an integer (count) as "likes"
+  def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do
     object
     |> Map.put("likes", [])
     |> Map.put("like_count", 0)
@@ -900,15 +900,10 @@ defp user_upgrade_task(user) do
 
     maybe_retire_websub(user.ap_id)
 
-    # Only do this for recent activties, don't go through the whole db.
-    # Only look at the last 1000 activities.
-    since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000
-
     q =
       from(
         a in Activity,
         where: ^old_follower_address in a.recipients,
-        where: a.id > ^since,
         update: [
           set: [
             recipients:
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index fe8248107..dcf681b6d 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -160,7 +160,7 @@ def render("outbox.json", %{user: user, max_id: max_qid}) do
       "partOf" => iri,
       "totalItems" => info.note_count,
       "orderedItems" => collection,
-      "next" => "#{iri}?max_id=#{min_id - 1}"
+      "next" => "#{iri}?max_id=#{min_id}"
     }
 
     if max_qid == nil do
@@ -207,7 +207,7 @@ def render("inbox.json", %{user: user, max_id: max_qid}) do
       "partOf" => iri,
       "totalItems" => -1,
       "orderedItems" => collection,
-      "next" => "#{iri}?max_id=#{min_id - 1}"
+      "next" => "#{iri}?max_id=#{min_id}"
     }
 
     if max_qid == nil do
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index a0f59d900..208677bd7 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -261,4 +261,46 @@ def emoji_from_profile(%{info: _info} = user) do
       }
     end)
   end
+
+  def maybe_notify_to_recipients(
+        recipients,
+        %Activity{data: %{"to" => to, "type" => _type}} = _activity
+      ) do
+    recipients ++ to
+  end
+
+  def maybe_notify_mentioned_recipients(
+        recipients,
+        %Activity{data: %{"to" => _to, "type" => type} = data} = _activity
+      )
+      when type == "Create" do
+    object = Object.normalize(data["object"])
+
+    object_data =
+      cond do
+        !is_nil(object) ->
+          object.data
+
+        is_map(data["object"]) ->
+          data["object"]
+
+        true ->
+          %{}
+      end
+
+    tagged_mentions = maybe_extract_mentions(object_data)
+
+    recipients ++ tagged_mentions
+  end
+
+  def maybe_notify_mentioned_recipients(recipients, _), do: recipients
+
+  def maybe_extract_mentions(%{"tag" => tag}) do
+    tag
+    |> Enum.filter(fn x -> is_map(x) end)
+    |> Enum.filter(fn x -> x["type"] == "Mention" end)
+    |> Enum.map(fn x -> x["href"] end)
+  end
+
+  def maybe_extract_mentions(_), do: []
 end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index f4736fcb5..a366a149f 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   use Pleroma.Web, :controller
   alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
   alias Pleroma.Web
+  alias Pleroma.HTML
 
   alias Pleroma.Web.MastodonAPI.{
     StatusView,
@@ -540,15 +541,34 @@ def reblogged_by(conn, %{"id" => id}) do
   def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
     local_only = params["local"] in [true, "True", "true", "1"]
 
-    params =
+    tags =
+      [params["tag"], params["any"]]
+      |> List.flatten()
+      |> Enum.uniq()
+      |> Enum.filter(& &1)
+      |> Enum.map(&String.downcase(&1))
+
+    tag_all =
+      params["all"] ||
+        []
+        |> Enum.map(&String.downcase(&1))
+
+    tag_reject =
+      params["none"] ||
+        []
+        |> Enum.map(&String.downcase(&1))
+
+    query_params =
       params
       |> Map.put("type", "Create")
       |> Map.put("local_only", local_only)
       |> Map.put("blocking_user", user)
-      |> Map.put("tag", String.downcase(params["tag"]))
+      |> Map.put("tag", tags)
+      |> Map.put("tag_all", tag_all)
+      |> Map.put("tag_reject", tag_reject)
 
     activities =
-      ActivityPub.fetch_public_activities(params)
+      ActivityPub.fetch_public_activities(query_params)
       |> Enum.reverse()
 
     conn
@@ -1322,6 +1342,29 @@ def suggestions(%{assigns: %{user: user}} = conn, _) do
     end
   end
 
+  def get_status_card(status_id) do
+    with %Activity{} = activity <- Repo.get(Activity, status_id),
+         true <- ActivityPub.is_public?(activity),
+         %Object{} = object <- Object.normalize(activity.data["object"]),
+         page_url <- HTML.extract_first_external_url(object, object.data["content"]),
+         {:ok, rich_media} <- Pleroma.Web.RichMedia.Parser.parse(page_url) do
+      page_url = rich_media[:url] || page_url
+      site_name = rich_media[:site_name] || URI.parse(page_url).host
+
+      rich_media
+      |> Map.take([:image, :title, :description])
+      |> Map.put(:type, "link")
+      |> Map.put(:provider_name, site_name)
+      |> Map.put(:url, page_url)
+    else
+      _ -> %{}
+    end
+  end
+
+  def status_card(conn, %{"id" => status_id}) do
+    json(conn, get_status_card(status_id))
+  end
+
   def try_render(conn, target, params)
       when is_binary(target) do
     res = render(conn, target, params)
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index bfd6b8b22..0ba4289da 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -112,7 +112,9 @@ defp do_render("account.json", %{user: user} = opts) do
       # Pleroma extension
       pleroma: %{
         confirmation_pending: user_info.confirmation_pending,
-        tags: user.tags
+        tags: user.tags,
+        is_moderator: user.info.is_moderator,
+        is_admin: user.info.is_admin
       }
     }
   end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 7a384e941..ddfe6788c 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -49,12 +49,11 @@ def render("index.json", opts) do
     replied_to_activities = get_replied_to_activities(opts.activities)
 
     opts.activities
-    |> render_many(
+    |> safe_render_many(
       StatusView,
       "status.json",
       Map.put(opts, :replied_to_activities, replied_to_activities)
     )
-    |> Enum.filter(fn x -> not is_nil(x) end)
   end
 
   def render(
diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex
new file mode 100644
index 000000000..8761260f2
--- /dev/null
+++ b/lib/pleroma/web/metadata.ex
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata do
+  alias Phoenix.HTML
+
+  def build_tags(params) do
+    Enum.reduce(Pleroma.Config.get([__MODULE__, :providers], []), "", fn parser, acc ->
+      rendered_html =
+        params
+        |> parser.build_tags()
+        |> Enum.map(&to_tag/1)
+        |> Enum.map(&HTML.safe_to_string/1)
+        |> Enum.join()
+
+      acc <> rendered_html
+    end)
+  end
+
+  def to_tag(data) do
+    with {name, attrs, _content = []} <- data do
+      HTML.Tag.tag(name, attrs)
+    else
+      {name, attrs, content} ->
+        HTML.Tag.content_tag(name, content, attrs)
+
+      _ ->
+        raise ArgumentError, message: "make_tag invalid args"
+    end
+  end
+
+  def activity_nsfw?(%{data: %{"sensitive" => sensitive}}) do
+    Pleroma.Config.get([__MODULE__, :unfurl_nsfw], false) == false and sensitive
+  end
+
+  def activity_nsfw?(_) do
+    false
+  end
+end
diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex
new file mode 100644
index 000000000..30333785e
--- /dev/null
+++ b/lib/pleroma/web/metadata/opengraph.ex
@@ -0,0 +1,154 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
+  alias Pleroma.Web.Metadata.Providers.Provider
+  alias Pleroma.Web.Metadata
+  alias Pleroma.{HTML, Formatter, User}
+  alias Pleroma.Web.MediaProxy
+
+  @behaviour Provider
+
+  @impl Provider
+  def build_tags(%{
+        object: object,
+        url: url,
+        user: user
+      }) do
+    attachments = build_attachments(object)
+    scrubbed_content = scrub_html_and_truncate(object)
+    # Zero width space
+    content =
+      if scrubbed_content != "" and scrubbed_content != "\u200B" do
+        ": “" <> scrubbed_content <> "”"
+      else
+        ""
+      end
+
+    # Most previews only show og:title which is inconvenient. Instagram
+    # hacks this by putting the description in the title and making the
+    # description longer prefixed by how many likes and shares the post
+    # has. Here we use the descriptive nickname in the title, and expand
+    # the full account & nickname in the description. We also use the cute^Wevil
+    # smart quotes around the status text like Instagram, too.
+    [
+      {:meta,
+       [
+         property: "og:title",
+         content: "#{user.name}" <> content
+       ], []},
+      {:meta, [property: "og:url", content: url], []},
+      {:meta,
+       [
+         property: "og:description",
+         content: "#{user_name_string(user)}" <> content
+       ], []},
+      {:meta, [property: "og:type", content: "website"], []}
+    ] ++
+      if attachments == [] or Metadata.activity_nsfw?(object) do
+        [
+          {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []},
+          {:meta, [property: "og:image:width", content: 150], []},
+          {:meta, [property: "og:image:height", content: 150], []}
+        ]
+      else
+        attachments
+      end
+  end
+
+  @impl Provider
+  def build_tags(%{user: user}) do
+    with truncated_bio = scrub_html_and_truncate(user.bio || "") do
+      [
+        {:meta,
+         [
+           property: "og:title",
+           content: user_name_string(user)
+         ], []},
+        {:meta, [property: "og:url", content: User.profile_url(user)], []},
+        {:meta, [property: "og:description", content: truncated_bio], []},
+        {:meta, [property: "og:type", content: "website"], []},
+        {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []},
+        {:meta, [property: "og:image:width", content: 150], []},
+        {:meta, [property: "og:image:height", content: 150], []}
+      ]
+    end
+  end
+
+  defp build_attachments(%{data: %{"attachment" => attachments}}) do
+    Enum.reduce(attachments, [], fn attachment, acc ->
+      rendered_tags =
+        Enum.reduce(attachment["url"], [], fn url, acc ->
+          media_type =
+            Enum.find(["image", "audio", "video"], fn media_type ->
+              String.starts_with?(url["mediaType"], media_type)
+            end)
+
+          # TODO: Add additional properties to objects when we have the data available.
+          # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image
+          # object when a Video or GIF is attached it will display that in the Whatsapp Rich Preview.
+          case media_type do
+            "audio" ->
+              [
+                {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []}
+                | acc
+              ]
+
+            "image" ->
+              [
+                {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])],
+                 []},
+                {:meta, [property: "og:image:width", content: 150], []},
+                {:meta, [property: "og:image:height", content: 150], []}
+                | acc
+              ]
+
+            "video" ->
+              [
+                {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []}
+                | acc
+              ]
+
+            _ ->
+              acc
+          end
+        end)
+
+      acc ++ rendered_tags
+    end)
+  end
+
+  defp scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
+    content
+    # html content comes from DB already encoded, decode first and scrub after
+    |> HtmlEntities.decode()
+    |> String.replace(~r/<br\s?\/?>/, " ")
+    |> HTML.get_cached_stripped_html_for_object(object, __MODULE__)
+    |> Formatter.demojify()
+    |> Formatter.truncate()
+  end
+
+  defp scrub_html_and_truncate(content) when is_binary(content) do
+    content
+    # html content comes from DB already encoded, decode first and scrub after
+    |> HtmlEntities.decode()
+    |> String.replace(~r/<br\s?\/?>/, " ")
+    |> HTML.strip_tags()
+    |> Formatter.demojify()
+    |> Formatter.truncate()
+  end
+
+  defp attachment_url(url) do
+    MediaProxy.url(url)
+  end
+
+  defp user_name_string(user) do
+    "#{user.name} " <>
+      if user.local do
+        "(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})"
+      else
+        "(@#{user.nickname})"
+      end
+  end
+end
diff --git a/lib/pleroma/web/metadata/provider.ex b/lib/pleroma/web/metadata/provider.ex
new file mode 100644
index 000000000..197fb2a77
--- /dev/null
+++ b/lib/pleroma/web/metadata/provider.ex
@@ -0,0 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.Provider do
+  @callback build_tags(map()) :: list()
+end
diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex
new file mode 100644
index 000000000..32b979357
--- /dev/null
+++ b/lib/pleroma/web/metadata/twitter_card.ex
@@ -0,0 +1,46 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
+  alias Pleroma.Web.Metadata.Providers.Provider
+  alias Pleroma.Web.Metadata
+
+  @behaviour Provider
+
+  @impl Provider
+  def build_tags(%{object: object}) do
+    if Metadata.activity_nsfw?(object) or object.data["attachment"] == [] do
+      build_tags(nil)
+    else
+      case find_first_acceptable_media_type(object) do
+        "image" ->
+          [{:meta, [property: "twitter:card", content: "summary_large_image"], []}]
+
+        "audio" ->
+          [{:meta, [property: "twitter:card", content: "player"], []}]
+
+        "video" ->
+          [{:meta, [property: "twitter:card", content: "player"], []}]
+
+        _ ->
+          build_tags(nil)
+      end
+    end
+  end
+
+  @impl Provider
+  def build_tags(_) do
+    [{:meta, [property: "twitter:card", content: "summary"], []}]
+  end
+
+  def find_first_acceptable_media_type(%{data: %{"attachment" => attachment}}) do
+    Enum.find_value(attachment, fn attachment ->
+      Enum.find_value(attachment["url"], fn url ->
+        Enum.find(["image", "audio", "video"], fn media_type ->
+          String.starts_with?(url["mediaType"], media_type)
+        end)
+      end)
+    end)
+  end
+end
diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex
index cc4b74bc5..f8c65602d 100644
--- a/lib/pleroma/web/oauth/authorization.ex
+++ b/lib/pleroma/web/oauth/authorization.ex
@@ -14,7 +14,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
     field(:token, :string)
     field(:valid_until, :naive_datetime)
     field(:used, :boolean, default: false)
-    belongs_to(:user, Pleroma.User)
+    belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
     belongs_to(:app, App)
 
     timestamps()
diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex
index 1eeda3d24..f0fe3b578 100644
--- a/lib/pleroma/web/oauth/fallback_controller.ex
+++ b/lib/pleroma/web/oauth/fallback_controller.ex
@@ -9,7 +9,8 @@ defmodule Pleroma.Web.OAuth.FallbackController do
   # No user/password
   def call(conn, _) do
     conn
+    |> put_status(:unauthorized)
     |> put_flash(:error, "Invalid Username/Password")
-    |> OAuthController.authorize(conn.params)
+    |> OAuthController.authorize(conn.params["authorization"])
   end
 end
diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex
index f0ebc63f6..4e01b123b 100644
--- a/lib/pleroma/web/oauth/token.ex
+++ b/lib/pleroma/web/oauth/token.ex
@@ -14,7 +14,7 @@ defmodule Pleroma.Web.OAuth.Token do
     field(:token, :string)
     field(:refresh_token, :string)
     field(:valid_until, :naive_datetime)
-    belongs_to(:user, Pleroma.User)
+    belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
     belongs_to(:app, App)
 
     timestamps()
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index e483447ed..9392a97f0 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -7,7 +7,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do
 
   alias Pleroma.{User, Activity, Object}
   alias Pleroma.Web.OStatus.{FeedRepresenter, ActivityRepresenter}
-  alias Pleroma.Repo
   alias Pleroma.Web.{OStatus, Federator}
   alias Pleroma.Web.XML
   alias Pleroma.Web.ActivityPub.ObjectView
@@ -22,7 +21,11 @@ defmodule Pleroma.Web.OStatus.OStatusController do
   def feed_redirect(conn, %{"nickname" => nickname}) do
     case get_format(conn) do
       "html" ->
-        Fallback.RedirectController.redirector(conn, nil)
+        with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
+          Fallback.RedirectController.redirector_with_meta(conn, %{user: user})
+        else
+          nil -> {:error, :not_found}
+        end
 
       "activity+json" ->
         ActivityPubController.call(conn, :user)
@@ -138,24 +141,40 @@ def activity(conn, %{"uuid" => uuid}) do
   end
 
   def notice(conn, %{"id" => id}) do
-    with {_, %Activity{} = activity} <- {:activity, Repo.get(Activity, id)},
+    with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(id)},
          {_, true} <- {:public?, ActivityPub.is_public?(activity)},
          %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
       case format = get_format(conn) do
         "html" ->
-          conn
-          |> put_resp_content_type("text/html")
-          |> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html"))
+          if activity.data["type"] == "Create" do
+            %Object{} = object = Object.normalize(activity.data["object"])
+
+            Fallback.RedirectController.redirector_with_meta(conn, %{
+              object: object,
+              url:
+                Pleroma.Web.Router.Helpers.o_status_url(
+                  Pleroma.Web.Endpoint,
+                  :notice,
+                  activity.id
+                ),
+              user: user
+            })
+          else
+            Fallback.RedirectController.redirector(conn, nil)
+          end
 
         _ ->
           represent_activity(conn, format, activity, user)
       end
     else
       {:public?, false} ->
-        {:error, :not_found}
+        conn
+        |> put_status(404)
+        |> Fallback.RedirectController.redirector(nil, 404)
 
       {:activity, nil} ->
-        {:error, :not_found}
+        conn
+        |> Fallback.RedirectController.redirector(nil, 404)
 
       e ->
         e
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
index 82b30950c..bd9d9f3a7 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -10,7 +10,7 @@ defmodule Pleroma.Web.Push.Subscription do
   alias Pleroma.Web.Push.Subscription
 
   schema "push_subscriptions" do
-    belongs_to(:user, User)
+    belongs_to(:user, User, type: Pleroma.FlakeId)
     belongs_to(:token, Token)
     field(:endpoint, :string)
     field(:key_p256dh, :string)
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
index 6da83c6e4..947dc0c3c 100644
--- a/lib/pleroma/web/rich_media/parser.ex
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -5,11 +5,19 @@ defmodule Pleroma.Web.RichMedia.Parser do
     Pleroma.Web.RichMedia.Parsers.OEmbed
   ]
 
+  def parse(nil), do: {:error, "No URL provided"}
+
   if Mix.env() == :test do
     def parse(url), do: parse_url(url)
   else
-    def parse(url),
-      do: Cachex.fetch!(:rich_media_cache, url, fn _ -> parse_url(url) end)
+    def parse(url) do
+      with {:ok, data} <- Cachex.fetch(:rich_media_cache, url, fn _ -> parse_url(url) end) do
+        data
+      else
+        _e ->
+          {:error, "Parsing error"}
+      end
+    end
   end
 
   defp parse_url(url) do
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 69ab58c6a..31f739738 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -258,7 +258,7 @@ defmodule Pleroma.Web.Router do
 
     get("/statuses/:id", MastodonAPIController, :get_status)
     get("/statuses/:id/context", MastodonAPIController, :get_context)
-    get("/statuses/:id/card", MastodonAPIController, :empty_object)
+    get("/statuses/:id/card", MastodonAPIController, :status_card)
     get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
     get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
 
@@ -396,7 +396,11 @@ defmodule Pleroma.Web.Router do
   end
 
   pipeline :ostatus do
-    plug(:accepts, ["xml", "atom", "html", "activity+json"])
+    plug(:accepts, ["html", "xml", "atom", "activity+json"])
+  end
+
+  pipeline :oembed do
+    plug(:accepts, ["json", "xml"])
   end
 
   scope "/", Pleroma.Web do
@@ -414,6 +418,12 @@ defmodule Pleroma.Web.Router do
     post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
   end
 
+  scope "/", Pleroma.Web do
+    pipe_through(:oembed)
+
+    get("/oembed", OEmbed.OEmbedController, :url)
+  end
+
   pipeline :activitypub do
     plug(:accepts, ["activity+json"])
     plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
@@ -501,6 +511,7 @@ defmodule Pleroma.Web.Router do
 
   scope "/", Fallback do
     get("/registration/:token", RedirectController, :registration_page)
+    get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
     get("/*path", RedirectController, :redirector)
 
     options("/*path", RedirectController, :empty)
@@ -509,11 +520,36 @@ defmodule Pleroma.Web.Router do
 
 defmodule Fallback.RedirectController do
   use Pleroma.Web, :controller
+  alias Pleroma.Web.Metadata
+  alias Pleroma.User
 
-  def redirector(conn, _params) do
+  def redirector(conn, _params, code \\ 200) do
     conn
     |> put_resp_content_type("text/html")
-    |> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html"))
+    |> send_file(code, index_file_path())
+  end
+
+  def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do
+    with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do
+      redirector_with_meta(conn, %{user: user})
+    else
+      nil ->
+        redirector(conn, params)
+    end
+  end
+
+  def redirector_with_meta(conn, params) do
+    {:ok, index_content} = File.read(index_file_path())
+    tags = Metadata.build_tags(params)
+    response = String.replace(index_content, "<!--server-generated-meta-->", tags)
+
+    conn
+    |> put_resp_content_type("text/html")
+    |> send_resp(200, response)
+  end
+
+  def index_file_path do
+    Pleroma.Plugs.InstanceStatic.file_path("index.html")
   end
 
   def registration_page(conn, params) do
diff --git a/lib/pleroma/web/twitter_api/representers/activity_representer.ex b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
index 4f8f228ab..19b723586 100644
--- a/lib/pleroma/web/twitter_api/representers/activity_representer.ex
+++ b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
@@ -158,7 +158,9 @@ def to_map(
     mentions = opts[:mentioned] || []
 
     attentions =
-      activity.recipients
+      []
+      |> Utils.maybe_notify_to_recipients(activity)
+      |> Utils.maybe_notify_mentioned_recipients(activity)
       |> Enum.map(fn ap_id -> Enum.find(mentions, fn user -> ap_id == user.ap_id end) end)
       |> Enum.filter(& &1)
       |> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 8c9060cf2..3064d61ea 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -265,8 +265,6 @@ def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
   end
 
   def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    id = String.to_integer(id)
-
     with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id),
          activities <-
            ActivityPub.fetch_activities_for_context(context, %{
@@ -340,44 +338,47 @@ def get_by_id_or_ap_id(id) do
   end
 
   def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
-         {:ok, activity} <- TwitterAPI.fav(user, id) do
+    with {:ok, activity} <- TwitterAPI.fav(user, id) do
       conn
       |> put_view(ActivityView)
       |> render("activity.json", %{activity: activity, for: user})
+    else
+      _ -> json_reply(conn, 400, Jason.encode!(%{}))
     end
   end
 
   def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
-         {:ok, activity} <- TwitterAPI.unfav(user, id) do
+    with {:ok, activity} <- TwitterAPI.unfav(user, id) do
       conn
       |> put_view(ActivityView)
       |> render("activity.json", %{activity: activity, for: user})
+    else
+      _ -> json_reply(conn, 400, Jason.encode!(%{}))
     end
   end
 
   def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
-         {:ok, activity} <- TwitterAPI.repeat(user, id) do
+    with {:ok, activity} <- TwitterAPI.repeat(user, id) do
       conn
       |> put_view(ActivityView)
       |> render("activity.json", %{activity: activity, for: user})
+    else
+      _ -> json_reply(conn, 400, Jason.encode!(%{}))
     end
   end
 
   def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
-         {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
+    with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
       conn
       |> put_view(ActivityView)
       |> render("activity.json", %{activity: activity, for: user})
+    else
+      _ -> json_reply(conn, 400, Jason.encode!(%{}))
     end
   end
 
   def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
-         {:ok, activity} <- TwitterAPI.pin(user, id) do
+    with {:ok, activity} <- TwitterAPI.pin(user, id) do
       conn
       |> put_view(ActivityView)
       |> render("activity.json", %{activity: activity, for: user})
@@ -388,8 +389,7 @@ def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
   end
 
   def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
-         {:ok, activity} <- TwitterAPI.unpin(user, id) do
+    with {:ok, activity} <- TwitterAPI.unpin(user, id) do
       conn
       |> put_view(ActivityView)
       |> render("activity.json", %{activity: activity, for: user})
@@ -556,7 +556,6 @@ def friend_requests(conn, params) do
 
   def approve_friend_request(conn, %{"user_id" => uid} = _params) do
     with followed <- conn.assigns[:user],
-         uid when is_number(uid) <- String.to_integer(uid),
          %User{} = follower <- Repo.get(User, uid),
          {:ok, follower} <- User.maybe_follow(follower, followed),
          %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
@@ -578,7 +577,6 @@ def approve_friend_request(conn, %{"user_id" => uid} = _params) do
 
   def deny_friend_request(conn, %{"user_id" => uid} = _params) do
     with followed <- conn.assigns[:user],
-         uid when is_number(uid) <- String.to_integer(uid),
          %User{} = follower <- Repo.get(User, uid),
          %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
          {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex
index 5eb06a26e..a01ee0010 100644
--- a/lib/pleroma/web/twitter_api/views/activity_view.ex
+++ b/lib/pleroma/web/twitter_api/views/activity_view.ex
@@ -114,7 +114,7 @@ def render("index.json", opts) do
       |> Map.put(:context_ids, context_ids)
       |> Map.put(:users, users)
 
-    render_many(
+    safe_render_many(
       opts.activities,
       ActivityView,
       "activity.json",
@@ -236,7 +236,9 @@ def render(
     pinned = activity.id in user.info.pinned_activities
 
     attentions =
-      activity.recipients
+      []
+      |> Utils.maybe_notify_to_recipients(activity)
+      |> Utils.maybe_notify_mentioned_recipients(activity)
       |> Enum.map(fn ap_id -> get_user(ap_id, opts) end)
       |> Enum.filter(& &1)
       |> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex
index a8cf83613..15682db8f 100644
--- a/lib/pleroma/web/twitter_api/views/user_view.ex
+++ b/lib/pleroma/web/twitter_api/views/user_view.ex
@@ -108,6 +108,7 @@ defp do_render("user.json", %{user: user = %User{}} = assigns) do
       "locked" => user.info.locked,
       "default_scope" => user.info.default_scope,
       "no_rich_text" => user.info.no_rich_text,
+      "hide_network" => user.info.hide_network,
       "fields" => fields,
 
       # Pleroma extension
diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex
index 74b13f929..30558e692 100644
--- a/lib/pleroma/web/web.ex
+++ b/lib/pleroma/web/web.ex
@@ -38,6 +38,33 @@ def view do
       import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
 
       import Pleroma.Web.{ErrorHelpers, Gettext, Router.Helpers}
+
+      require Logger
+
+      @doc "Same as `render/3` but wrapped in a rescue block"
+      def safe_render(view, template, assigns \\ %{}) do
+        Phoenix.View.render(view, template, assigns)
+      rescue
+        error ->
+          Logger.error(
+            "#{__MODULE__} failed to render #{inspect({view, template})}: #{inspect(error)}"
+          )
+
+          Logger.error(inspect(__STACKTRACE__))
+          nil
+      end
+
+      @doc """
+      Same as `render_many/4` but wrapped in rescue block.
+      """
+      def safe_render_many(collection, view, template, assigns \\ %{}) do
+        Enum.map(collection, fn resource ->
+          as = Map.get(assigns, :as) || view.__resource__
+          assigns = Map.put(assigns, as, resource)
+          safe_render(view, template, assigns)
+        end)
+        |> Enum.filter(& &1)
+      end
     end
   end
 
diff --git a/lib/pleroma/web/websub/websub_client_subscription.ex b/lib/pleroma/web/websub/websub_client_subscription.ex
index 105b0069f..969ee0684 100644
--- a/lib/pleroma/web/websub/websub_client_subscription.ex
+++ b/lib/pleroma/web/websub/websub_client_subscription.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.Websub.WebsubClientSubscription do
     field(:state, :string)
     field(:subscribers, {:array, :string}, default: [])
     field(:hub, :string)
-    belongs_to(:user, User)
+    belongs_to(:user, User, type: Pleroma.FlakeId)
 
     timestamps()
   end
diff --git a/mix.exs b/mix.exs
index ccf7790b0..d46998891 100644
--- a/mix.exs
+++ b/mix.exs
@@ -59,6 +59,7 @@ defp deps do
       {:pbkdf2_elixir, "~> 0.12.3"},
       {:trailing_format_plug, "~> 0.0.7"},
       {:html_sanitize_ex, "~> 1.3.0"},
+      {:html_entities, "~> 0.4"},
       {:phoenix_html, "~> 2.10"},
       {:calendar, "~> 0.17.4"},
       {:cachex, "~> 3.0.2"},
diff --git a/priv/repo/migrations/20181218172826_users_and_activities_flake_id.exs b/priv/repo/migrations/20181218172826_users_and_activities_flake_id.exs
new file mode 100644
index 000000000..47d2d02da
--- /dev/null
+++ b/priv/repo/migrations/20181218172826_users_and_activities_flake_id.exs
@@ -0,0 +1,125 @@
+defmodule Pleroma.Repo.Migrations.UsersAndActivitiesFlakeId do
+  use Ecto.Migration
+  alias Pleroma.Clippy
+  require Integer
+  import Ecto.Query
+  alias Pleroma.Repo
+
+  # This migrates from int serial IDs to custom Flake:
+  #   1- create a temporary uuid column
+  #   2- fill this column with compatibility ids (see below)
+  #   3- remove pkeys constraints
+  #   4- update relation pkeys with the new ids
+  #   5- rename the temporary column to id
+  #   6- re-create the constraints
+  def change do
+    # Old serial int ids are transformed to 128bits with extra padding.
+    # The application (in `Pleroma.FlakeId`) handles theses IDs properly as integers; to keep compatibility
+    # with previously issued ids.
+    #execute "update activities set external_id = CAST( LPAD( TO_HEX(id), 32, '0' ) AS uuid);"
+    #execute "update users set external_id = CAST( LPAD( TO_HEX(id), 32, '0' ) AS uuid);"
+
+    clippy = start_clippy_heartbeats()
+
+    # Lock both tables to avoid a running server to meddling with our transaction
+    execute "LOCK TABLE activities;"
+    execute "LOCK TABLE users;"
+
+    execute """
+      ALTER TABLE activities
+      DROP CONSTRAINT activities_pkey CASCADE,
+      ALTER COLUMN id DROP default,
+      ALTER COLUMN id SET DATA TYPE uuid USING CAST( LPAD( TO_HEX(id), 32, '0' ) AS uuid),
+      ADD PRIMARY KEY (id);
+    """
+
+    execute """
+    ALTER TABLE users
+    DROP CONSTRAINT users_pkey CASCADE,
+    ALTER COLUMN id DROP default,
+    ALTER COLUMN id SET DATA TYPE uuid USING CAST( LPAD( TO_HEX(id), 32, '0' ) AS uuid),
+    ADD PRIMARY KEY (id);
+    """
+
+    execute "UPDATE users SET info = jsonb_set(info, '{pinned_activities}', array_to_json(ARRAY(select jsonb_array_elements_text(info->'pinned_activities')))::jsonb);"
+
+    # Fkeys:
+    # Activities - Referenced by:
+    #   TABLE "notifications" CONSTRAINT "notifications_activity_id_fkey" FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE CASCADE
+    # Users - Referenced by:
+    #  TABLE "filters" CONSTRAINT "filters_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+    #  TABLE "lists" CONSTRAINT "lists_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+    #  TABLE "notifications" CONSTRAINT "notifications_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+    #  TABLE "oauth_authorizations" CONSTRAINT "oauth_authorizations_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)
+    #  TABLE "oauth_tokens" CONSTRAINT "oauth_tokens_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)
+    #  TABLE "password_reset_tokens" CONSTRAINT "password_reset_tokens_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)
+    #  TABLE "push_subscriptions" CONSTRAINT "push_subscriptions_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+    #  TABLE "websub_client_subscriptions" CONSTRAINT "websub_client_subscriptions_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)
+
+    execute """
+    ALTER TABLE notifications
+    ALTER COLUMN activity_id SET DATA TYPE uuid USING CAST( LPAD( TO_HEX(activity_id), 32, '0' ) AS uuid),
+    ADD CONSTRAINT notifications_activity_id_fkey FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE CASCADE;
+    """
+
+    for table <- ~w(notifications filters lists oauth_authorizations oauth_tokens password_reset_tokens push_subscriptions websub_client_subscriptions) do
+      execute """
+      ALTER TABLE #{table}
+      ALTER COLUMN user_id SET DATA TYPE uuid USING CAST( LPAD( TO_HEX(user_id), 32, '0' ) AS uuid),
+      ADD CONSTRAINT #{table}_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+      """
+    end
+
+    flush()
+
+    stop_clippy_heartbeats(clippy)
+  end
+
+  defp start_clippy_heartbeats() do
+    count = from(a in "activities", select: count(a.id)) |> Repo.one!
+
+    if count > 5000 do
+      heartbeat_interval = :timer.minutes(2) + :timer.seconds(30)
+      all_tips = Clippy.tips() ++ [
+        "The migration is still running, maybe it's time for another “tea”?",
+        "Happy rabbits practice a cute behavior known as a\n“binky:” they jump up in the air\nand twist\nand spin around!",
+        "Nothing and everything.\n\nI still work.",
+        "Pleroma runs on a Raspberry Pi!\n\n  … but this migration will take forever if you\nactually run on a raspberry pi",
+        "Status? Stati? Post? Note? Toot?\nRepeat? Reboost? Boost? Retweet? Retoot??\n\nI-I'm confused.",
+      ]
+
+      heartbeat = fn(heartbeat, runs, all_tips, tips) ->
+        tips = if Integer.is_even(runs) do
+          tips = if tips == [], do: all_tips, else: tips
+          [tip | tips] = Enum.shuffle(tips)
+          Clippy.puts(tip)
+          tips
+        else
+          IO.puts "\n -- #{DateTime.to_string(DateTime.utc_now())} Migration still running, please wait…\n"
+          tips
+        end
+        :timer.sleep(heartbeat_interval)
+        heartbeat.(heartbeat, runs + 1, all_tips, tips)
+      end
+
+      Clippy.puts [
+        [:red, :bright, "It looks like you are running an older instance!"],
+        [""],
+        [:bright, "This migration may take a long time", :reset, " -- so you probably should"],
+        ["go drink a cofe, or a tea, or a beer, a whiskey, a vodka,"],
+        ["while it runs to deal with your temporary fediverse pause!"]
+      ]
+      :timer.sleep(heartbeat_interval)
+      spawn_link(fn() -> heartbeat.(heartbeat, 1, all_tips, []) end)
+    end
+  end
+
+  defp stop_clippy_heartbeats(pid) do
+    if pid do
+      Process.unlink(pid)
+      Process.exit(pid, :kill)
+      Clippy.puts [[:green, :bright, "Hurray!!", "", "", "Migration completed!"]]
+    end
+  end
+
+end
diff --git a/priv/repo/migrations/20190124131141_update_activity_visibility_again.exs b/priv/repo/migrations/20190124131141_update_activity_visibility_again.exs
new file mode 100644
index 000000000..0519a5143
--- /dev/null
+++ b/priv/repo/migrations/20190124131141_update_activity_visibility_again.exs
@@ -0,0 +1,37 @@
+defmodule Pleroma.Repo.Migrations.UpdateActivityVisibilityAgain do
+  use Ecto.Migration
+  @disable_ddl_transaction true
+
+  def up do
+    definition = """
+    create or replace function activity_visibility(actor varchar, recipients varchar[], data jsonb) returns varchar as $$
+    DECLARE
+      fa varchar;
+      public varchar := 'https://www.w3.org/ns/activitystreams#Public';
+    BEGIN
+      SELECT COALESCE(users.follower_address, '') into fa from public.users where users.ap_id = actor;
+
+      IF data->'to' ? public THEN
+        RETURN 'public';
+      ELSIF data->'cc' ? public THEN
+        RETURN 'unlisted';
+      ELSIF ARRAY[fa] && recipients THEN
+        RETURN 'private';
+      ELSIF not(ARRAY[fa, public] && recipients) THEN
+        RETURN 'direct';
+      ELSE
+        RETURN 'unknown';
+      END IF;
+    END;
+    $$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE SECURITY DEFINER;
+    """
+
+    execute(definition)
+
+  end
+
+  def down do
+
+  end
+
+end
diff --git a/priv/repo/migrations/20190126160540_change_push_subscriptions_varchar.exs b/priv/repo/migrations/20190126160540_change_push_subscriptions_varchar.exs
new file mode 100644
index 000000000..337fed156
--- /dev/null
+++ b/priv/repo/migrations/20190126160540_change_push_subscriptions_varchar.exs
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.ChangePushSubscriptionsVarchar do
+  use Ecto.Migration
+
+  def change do
+    alter table(:push_subscriptions) do
+      modify(:endpoint, :varchar)
+    end
+  end
+end
diff --git a/priv/repo/migrations/20190127151220_add_activities_likes_index.exs b/priv/repo/migrations/20190127151220_add_activities_likes_index.exs
new file mode 100644
index 000000000..b1822d265
--- /dev/null
+++ b/priv/repo/migrations/20190127151220_add_activities_likes_index.exs
@@ -0,0 +1,8 @@
+defmodule Pleroma.Repo.Migrations.AddActivitiesLikesIndex do
+  use Ecto.Migration
+  @disable_ddl_transaction true
+
+  def change do
+    create index(:activities, ["((data #> '{\"object\",\"likes\"}'))"], concurrently: true, name: :activities_likes, using: :gin)
+  end
+end
diff --git a/priv/static/index.html b/priv/static/index.html
index 74792bf82..4a1a35282 100644
--- a/priv/static/index.html
+++ b/priv/static/index.html
@@ -1 +1 @@
-<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.3d3e30a9afb8c41739656f496e8c79e6.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.e58590e04ca06ebbea1e.js></script><script type=text/javascript src=/static/js/vendor.61fac267296f19262d14.js></script><script type=text/javascript src=/static/js/app.76e23c93f1de5902c4d7.js></script></body></html>
\ No newline at end of file
+<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.3d3e30a9afb8c41739656f496e8c79e6.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.8dc8d7a1dc85bfdf2b14.js></script><script type=text/javascript src=/static/js/vendor.61fd03d8471aaadcf63c.js></script><script type=text/javascript src=/static/js/app.ddbd2a89e264d04e0d6d.js></script></body></html>
\ No newline at end of file
diff --git a/priv/static/static/config.json b/priv/static/static/config.json
index cc72900a8..24e26696f 100644
--- a/priv/static/static/config.json
+++ b/priv/static/static/config.json
@@ -18,5 +18,6 @@
   "hideUserStats": false,
   "loginMethod": "password",
   "webPushNotifications": false,
-  "noAttachmentLinks": false
+  "noAttachmentLinks": false,
+  "nsfwCensorImage": ""
 }
diff --git a/priv/static/static/js/app.76e23c93f1de5902c4d7.js b/priv/static/static/js/app.76e23c93f1de5902c4d7.js
deleted file mode 100644
index 4413aa4dc..000000000
Binary files a/priv/static/static/js/app.76e23c93f1de5902c4d7.js and /dev/null differ
diff --git a/priv/static/static/js/app.76e23c93f1de5902c4d7.js.map b/priv/static/static/js/app.76e23c93f1de5902c4d7.js.map
deleted file mode 100644
index 01b977adb..000000000
Binary files a/priv/static/static/js/app.76e23c93f1de5902c4d7.js.map and /dev/null differ
diff --git a/priv/static/static/js/app.ddbd2a89e264d04e0d6d.js b/priv/static/static/js/app.ddbd2a89e264d04e0d6d.js
new file mode 100644
index 000000000..95af860ad
Binary files /dev/null and b/priv/static/static/js/app.ddbd2a89e264d04e0d6d.js differ
diff --git a/priv/static/static/js/app.ddbd2a89e264d04e0d6d.js.map b/priv/static/static/js/app.ddbd2a89e264d04e0d6d.js.map
new file mode 100644
index 000000000..0fc435ffb
Binary files /dev/null and b/priv/static/static/js/app.ddbd2a89e264d04e0d6d.js.map differ
diff --git a/priv/static/static/js/manifest.8dc8d7a1dc85bfdf2b14.js b/priv/static/static/js/manifest.8dc8d7a1dc85bfdf2b14.js
new file mode 100644
index 000000000..712cb9ae8
Binary files /dev/null and b/priv/static/static/js/manifest.8dc8d7a1dc85bfdf2b14.js differ
diff --git a/priv/static/static/js/manifest.e58590e04ca06ebbea1e.js.map b/priv/static/static/js/manifest.8dc8d7a1dc85bfdf2b14.js.map
similarity index 92%
rename from priv/static/static/js/manifest.e58590e04ca06ebbea1e.js.map
rename to priv/static/static/js/manifest.8dc8d7a1dc85bfdf2b14.js.map
index dde57f245..e76faf6e8 100644
Binary files a/priv/static/static/js/manifest.e58590e04ca06ebbea1e.js.map and b/priv/static/static/js/manifest.8dc8d7a1dc85bfdf2b14.js.map differ
diff --git a/priv/static/static/js/manifest.e58590e04ca06ebbea1e.js b/priv/static/static/js/manifest.e58590e04ca06ebbea1e.js
deleted file mode 100644
index 6c72d1f6f..000000000
Binary files a/priv/static/static/js/manifest.e58590e04ca06ebbea1e.js and /dev/null differ
diff --git a/priv/static/static/js/vendor.61fac267296f19262d14.js.map b/priv/static/static/js/vendor.61fac267296f19262d14.js.map
deleted file mode 100644
index 8fa2f5cb5..000000000
Binary files a/priv/static/static/js/vendor.61fac267296f19262d14.js.map and /dev/null differ
diff --git a/priv/static/static/js/vendor.61fac267296f19262d14.js b/priv/static/static/js/vendor.61fd03d8471aaadcf63c.js
similarity index 51%
rename from priv/static/static/js/vendor.61fac267296f19262d14.js
rename to priv/static/static/js/vendor.61fd03d8471aaadcf63c.js
index 6ca06dfab..c0bd08259 100644
Binary files a/priv/static/static/js/vendor.61fac267296f19262d14.js and b/priv/static/static/js/vendor.61fd03d8471aaadcf63c.js differ
diff --git a/priv/static/static/js/vendor.61fd03d8471aaadcf63c.js.map b/priv/static/static/js/vendor.61fd03d8471aaadcf63c.js.map
new file mode 100644
index 000000000..b827959a7
Binary files /dev/null and b/priv/static/static/js/vendor.61fd03d8471aaadcf63c.js.map differ
diff --git a/priv/static/sw.js b/priv/static/sw.js
index 73e42aa29..e3c265153 100644
Binary files a/priv/static/sw.js and b/priv/static/sw.js differ
diff --git a/priv/static/sw.js.map b/priv/static/sw.js.map
index dae63a1fd..9ac7414bf 100644
Binary files a/priv/static/sw.js.map and b/priv/static/sw.js.map differ
diff --git a/test/flake_id_test.exs b/test/flake_id_test.exs
new file mode 100644
index 000000000..ca2338041
--- /dev/null
+++ b/test/flake_id_test.exs
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.FlakeIdTest do
+  use Pleroma.DataCase
+  import Kernel, except: [to_string: 1]
+  import Pleroma.FlakeId
+
+  describe "fake flakes (compatibility with older serial integers)" do
+    test "from_string/1" do
+      fake_flake = <<0::integer-size(64), 42::integer-size(64)>>
+      assert from_string("42") == fake_flake
+      assert from_string(42) == fake_flake
+    end
+
+    test "zero or -1 is a null flake" do
+      fake_flake = <<0::integer-size(128)>>
+      assert from_string("0") == fake_flake
+      assert from_string("-1") == fake_flake
+    end
+
+    test "to_string/1" do
+      fake_flake = <<0::integer-size(64), 42::integer-size(64)>>
+      assert to_string(fake_flake) == "42"
+    end
+  end
+
+  test "ecto type behaviour" do
+    flake = <<0, 0, 1, 104, 80, 229, 2, 235, 140, 22, 69, 201, 53, 210, 0, 0>>
+    flake_s = "9eoozpwTul5mjSEDRI"
+
+    assert cast(flake) == {:ok, flake_s}
+    assert cast(flake_s) == {:ok, flake_s}
+
+    assert load(flake) == {:ok, flake_s}
+    assert load(flake_s) == {:ok, flake_s}
+
+    assert dump(flake_s) == {:ok, flake}
+    assert dump(flake) == {:ok, flake}
+  end
+end
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 3d6efd52c..bcdf2e006 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -653,6 +653,14 @@ def get("https://social.heldscal.la/user/23211", _, _, Accept: "application/acti
     {:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)}
   end
 
+  def get("http://example.com/ogp", _, _, _) do
+    {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}}
+  end
+
+  def get("http://example.com/empty", _, _, _) do
+    {:ok, %Tesla.Env{status: 200, body: "hello"}}
+  end
+
   def get("http://404.site" <> _, _, _, _) do
     {:ok,
      %Tesla.Env{
diff --git a/test/user_test.exs b/test/user_test.exs
index 092cfc5dc..a0657c7b6 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -672,12 +672,13 @@ test "get recipients from activity" do
         "status" => "hey @#{addressed.nickname} @#{addressed_remote.nickname}"
       })
 
-    assert [addressed] == User.get_recipients_from_activity(activity)
+    assert Enum.map([actor, addressed], & &1.ap_id) --
+             Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == []
 
     {:ok, user} = User.follow(user, actor)
     {:ok, _user_two} = User.follow(user_two, actor)
     recipients = User.get_recipients_from_activity(activity)
-    assert length(recipients) == 2
+    assert length(recipients) == 3
     assert user in recipients
     assert addressed in recipients
   end
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index d517c7aa7..75b0918a6 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -65,6 +65,34 @@ test "it returns a user" do
       assert user.info.ap_enabled
       assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
     end
+
+    test "it fetches the appropriate tag-restricted posts" do
+      user = insert(:user)
+
+      {:ok, status_one} = CommonAPI.post(user, %{"status" => ". #test"})
+      {:ok, status_two} = CommonAPI.post(user, %{"status" => ". #essais"})
+      {:ok, status_three} = CommonAPI.post(user, %{"status" => ". #test #reject"})
+
+      fetch_one = ActivityPub.fetch_activities([], %{"tag" => "test"})
+      fetch_two = ActivityPub.fetch_activities([], %{"tag" => ["test", "essais"]})
+
+      fetch_three =
+        ActivityPub.fetch_activities([], %{
+          "tag" => ["test", "essais"],
+          "tag_reject" => ["reject"]
+        })
+
+      fetch_four =
+        ActivityPub.fetch_activities([], %{
+          "tag" => ["test"],
+          "tag_all" => ["test", "reject"]
+        })
+
+      assert fetch_one == [status_one, status_three]
+      assert fetch_two == [status_one, status_two, status_three]
+      assert fetch_three == [status_one, status_two]
+      assert fetch_four == [status_three]
+    end
   end
 
   describe "insertion" do
@@ -86,6 +114,17 @@ test "drops activities beyond a certain limit" do
       assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data)
     end
 
+    test "doesn't drop activities with content being null" do
+      data = %{
+        "ok" => true,
+        "object" => %{
+          "content" => nil
+        }
+      }
+
+      assert {:ok, _} = ActivityPub.insert(data)
+    end
+
     test "returns the activity if one with the same id is already in" do
       activity = insert(:note_activity)
       {:ok, new_activity} = ActivityPub.insert(activity.data)
@@ -161,7 +200,7 @@ test "removes doubled 'to' recipients" do
 
       assert activity.data["to"] == ["user1", "user2"]
       assert activity.actor == user.ap_id
-      assert activity.recipients == ["user1", "user2"]
+      assert activity.recipients == ["user1", "user2", user.ap_id]
     end
   end
 
diff --git a/test/web/activity_pub/mrf/anti_followbot_policy_test.exs b/test/web/activity_pub/mrf/anti_followbot_policy_test.exs
new file mode 100644
index 000000000..2ea4f9d3f
--- /dev/null
+++ b/test/web/activity_pub/mrf/anti_followbot_policy_test.exs
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do
+  use Pleroma.DataCase
+  import Pleroma.Factory
+
+  alias Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy
+
+  describe "blocking based on attributes" do
+    test "matches followbots by nickname" do
+      actor = insert(:user, %{nickname: "followbot@example.com"})
+      target = insert(:user)
+
+      message = %{
+        "@context" => "https://www.w3.org/ns/activitystreams",
+        "type" => "Follow",
+        "actor" => actor.ap_id,
+        "object" => target.ap_id,
+        "id" => "https://example.com/activities/1234"
+      }
+
+      {:reject, nil} = AntiFollowbotPolicy.filter(message)
+    end
+
+    test "matches followbots by display name" do
+      actor = insert(:user, %{name: "Federation Bot"})
+      target = insert(:user)
+
+      message = %{
+        "@context" => "https://www.w3.org/ns/activitystreams",
+        "type" => "Follow",
+        "actor" => actor.ap_id,
+        "object" => target.ap_id,
+        "id" => "https://example.com/activities/1234"
+      }
+
+      {:reject, nil} = AntiFollowbotPolicy.filter(message)
+    end
+  end
+
+  test "it allows non-followbots" do
+    actor = insert(:user)
+    target = insert(:user)
+
+    message = %{
+      "@context" => "https://www.w3.org/ns/activitystreams",
+      "type" => "Follow",
+      "actor" => actor.ap_id,
+      "object" => target.ap_id,
+      "id" => "https://example.com/activities/1234"
+    }
+
+    {:ok, _} = AntiFollowbotPolicy.filter(message)
+  end
+end
diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs
index d53e11963..f8cd68173 100644
--- a/test/web/mastodon_api/account_view_test.exs
+++ b/test/web/mastodon_api/account_view_test.exs
@@ -61,7 +61,9 @@ test "Represent a user account" do
       },
       pleroma: %{
         confirmation_pending: false,
-        tags: []
+        tags: [],
+        is_admin: false,
+        is_moderator: false
       }
     }
 
@@ -102,7 +104,9 @@ test "Represent a Service(bot) account" do
       },
       pleroma: %{
         confirmation_pending: false,
-        tags: []
+        tags: [],
+        is_admin: false,
+        is_moderator: false
       }
     }
 
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 8443dc856..b8f901e6c 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -148,7 +148,7 @@ test "posting a direct status", %{conn: conn} do
 
     assert %{"id" => id, "visibility" => "direct"} = json_response(conn, 200)
     assert activity = Repo.get(Activity, id)
-    assert activity.recipients == [user2.ap_id]
+    assert activity.recipients == [user2.ap_id, user1.ap_id]
     assert activity.data["to"] == [user2.ap_id]
     assert activity.data["cc"] == []
   end
@@ -182,6 +182,16 @@ test "direct timeline", %{conn: conn} do
     assert %{"visibility" => "direct"} = status
     assert status["url"] != direct.data["id"]
 
+    # User should be able to see his own direct message
+    res_conn =
+      build_conn()
+      |> assign(:user, user_one)
+      |> get("api/v1/timelines/direct")
+
+    [status] = json_response(res_conn, 200)
+
+    assert %{"visibility" => "direct"} = status
+
     # Both should be visible here
     res_conn =
       conn
@@ -1034,6 +1044,34 @@ test "hashtag timeline", %{conn: conn} do
     end)
   end
 
+  test "multi-hashtag timeline", %{conn: conn} do
+    user = insert(:user)
+
+    {:ok, activity_test} = CommonAPI.post(user, %{"status" => "#test"})
+    {:ok, activity_test1} = CommonAPI.post(user, %{"status" => "#test #test1"})
+    {:ok, activity_none} = CommonAPI.post(user, %{"status" => "#test #none"})
+
+    any_test =
+      conn
+      |> get("/api/v1/timelines/tag/test", %{"any" => ["test1"]})
+
+    [status_none, status_test1, status_test] = json_response(any_test, 200)
+
+    assert to_string(activity_test.id) == status_test["id"]
+    assert to_string(activity_test1.id) == status_test1["id"]
+    assert to_string(activity_none.id) == status_none["id"]
+
+    restricted_test =
+      conn
+      |> get("/api/v1/timelines/tag/test", %{"all" => ["test1"], "none" => ["none"]})
+
+    assert [status_test1] == json_response(restricted_test, 200)
+
+    all_test = conn |> get("/api/v1/timelines/tag/test", %{"all" => ["none"]})
+
+    assert [status_none] == json_response(all_test, 200)
+  end
+
   test "getting followers", %{conn: conn} do
     user = insert(:user)
     other_user = insert(:user)
@@ -1613,5 +1651,22 @@ test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
                |> post("/api/v1/statuses/#{activity_two.id}/pin")
                |> json_response(400)
     end
+
+    test "Status rich-media Card", %{conn: conn, user: user} do
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "http://example.com/ogp"})
+
+      response =
+        conn
+        |> get("/api/v1/statuses/#{activity.id}/card")
+        |> json_response(200)
+
+      assert response == %{
+               "image" => "http://ia.media-imdb.com/images/rock.jpg",
+               "provider_name" => "www.imdb.com",
+               "title" => "The Rock",
+               "type" => "link",
+               "url" => "http://www.imdb.com/title/tt0117500/"
+             }
+    end
   end
 end
diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs
index e33479368..ebf6273e8 100644
--- a/test/web/mastodon_api/status_view_test.exs
+++ b/test/web/mastodon_api/status_view_test.exs
@@ -149,7 +149,10 @@ test "contains mentions" do
 
     status = StatusView.render("status.json", %{activity: activity})
 
-    assert status.mentions == [AccountView.render("mention.json", %{user: user})]
+    actor = Repo.get_by(User, ap_id: activity.actor)
+
+    assert status.mentions ==
+             Enum.map([user, actor], fn u -> AccountView.render("mention.json", %{user: u}) end)
   end
 
   test "attachments" do
diff --git a/test/web/metadata/opengraph_test.exs b/test/web/metadata/opengraph_test.exs
new file mode 100644
index 000000000..4283f72cd
--- /dev/null
+++ b/test/web/metadata/opengraph_test.exs
@@ -0,0 +1,94 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do
+  use Pleroma.DataCase
+  import Pleroma.Factory
+  alias Pleroma.Web.Metadata.Providers.OpenGraph
+
+  test "it renders all supported types of attachments and skips unknown types" do
+    user = insert(:user)
+
+    note =
+      insert(:note, %{
+        data: %{
+          "actor" => user.ap_id,
+          "tag" => [],
+          "id" => "https://pleroma.gov/objects/whatever",
+          "content" => "pleroma in a nutshell",
+          "attachment" => [
+            %{
+              "url" => [
+                %{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"}
+              ]
+            },
+            %{
+              "url" => [
+                %{
+                  "mediaType" => "application/octet-stream",
+                  "href" => "https://pleroma.gov/fqa/badapple.sfc"
+                }
+              ]
+            },
+            %{
+              "url" => [
+                %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"}
+              ]
+            },
+            %{
+              "url" => [
+                %{
+                  "mediaType" => "audio/basic",
+                  "href" => "http://www.gnu.org/music/free-software-song.au"
+                }
+              ]
+            }
+          ]
+        }
+      })
+
+    result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user})
+
+    assert Enum.all?(
+             [
+               {:meta, [property: "og:image", content: "https://pleroma.gov/tenshi.png"], []},
+               {:meta,
+                [property: "og:audio", content: "http://www.gnu.org/music/free-software-song.au"],
+                []},
+               {:meta, [property: "og:video", content: "https://pleroma.gov/about/juche.webm"],
+                []}
+             ],
+             fn element -> element in result end
+           )
+  end
+
+  test "it does not render attachments if post is nsfw" do
+    Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false)
+    user = insert(:user, avatar: %{"url" => [%{"href" => "https://pleroma.gov/tenshi.png"}]})
+
+    note =
+      insert(:note, %{
+        data: %{
+          "actor" => user.ap_id,
+          "id" => "https://pleroma.gov/objects/whatever",
+          "content" => "#cuteposting #nsfw #hambaga",
+          "tag" => ["cuteposting", "nsfw", "hambaga"],
+          "sensitive" => true,
+          "attachment" => [
+            %{
+              "url" => [
+                %{"mediaType" => "image/png", "href" => "https://misskey.microsoft/corndog.png"}
+              ]
+            }
+          ]
+        }
+      })
+
+    result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user})
+
+    assert {:meta, [property: "og:image", content: "https://pleroma.gov/tenshi.png"], []} in result
+
+    refute {:meta, [property: "og:image", content: "https://misskey.microsoft/corndog.png"], []} in result
+  end
+end
diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index ccd552258..e0d3cb55f 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -34,6 +34,31 @@ test "redirects with oauth authorization" do
     assert Repo.get_by(Authorization, token: code)
   end
 
+  test "correctly handles wrong credentials", %{conn: conn} do
+    user = insert(:user)
+    app = insert(:oauth_app)
+
+    result =
+      conn
+      |> post("/oauth/authorize", %{
+        "authorization" => %{
+          "name" => user.nickname,
+          "password" => "wrong",
+          "client_id" => app.client_id,
+          "redirect_uri" => app.redirect_uris,
+          "state" => "statepassed"
+        }
+      })
+      |> html_response(:unauthorized)
+
+    # Keep the details
+    assert result =~ app.client_id
+    assert result =~ app.redirect_uris
+
+    # Error message
+    assert result =~ "Invalid"
+  end
+
   test "issues a token for an all-body request" do
     user = insert(:user)
     app = insert(:oauth_app)
diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs
index ad9bc418a..cba12b3f7 100644
--- a/test/web/ostatus/ostatus_controller_test.exs
+++ b/test/web/ostatus/ostatus_controller_test.exs
@@ -108,6 +108,7 @@ test "gets an object", %{conn: conn} do
 
     conn =
       conn
+      |> put_req_header("accept", "application/xml")
       |> get(url)
 
     expected =
@@ -134,31 +135,34 @@ test "404s on nonexisting objects", %{conn: conn} do
     |> response(404)
   end
 
+  test "gets an activity in xml format", %{conn: conn} do
+    note_activity = insert(:note_activity)
+    [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
+
+    conn
+    |> put_req_header("accept", "application/xml")
+    |> get("/activities/#{uuid}")
+    |> response(200)
+  end
+
   test "404s on deleted objects", %{conn: conn} do
     note_activity = insert(:note_activity)
     [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["object"]["id"]))
     object = Object.get_by_ap_id(note_activity.data["object"]["id"])
 
     conn
+    |> put_req_header("accept", "application/xml")
     |> get("/objects/#{uuid}")
     |> response(200)
 
     Object.delete(object)
 
     conn
+    |> put_req_header("accept", "application/xml")
     |> get("/objects/#{uuid}")
     |> response(404)
   end
 
-  test "gets an activity", %{conn: conn} do
-    note_activity = insert(:note_activity)
-    [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
-
-    conn
-    |> get("/activities/#{uuid}")
-    |> response(200)
-  end
-
   test "404s on private activities", %{conn: conn} do
     note_activity = insert(:direct_note_activity)
     [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
@@ -174,7 +178,7 @@ test "404s on nonexistent activities", %{conn: conn} do
     |> response(404)
   end
 
-  test "gets a notice", %{conn: conn} do
+  test "gets a notice in xml format", %{conn: conn} do
     note_activity = insert(:note_activity)
 
     conn
diff --git a/test/web/rich_media/controllers/rich_media_controller_test.exs b/test/web/rich_media/controllers/rich_media_controller_test.exs
index 37c82631f..fef126513 100644
--- a/test/web/rich_media/controllers/rich_media_controller_test.exs
+++ b/test/web/rich_media/controllers/rich_media_controller_test.exs
@@ -1,19 +1,14 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
 defmodule Pleroma.Web.RichMedia.RichMediaControllerTest do
   use Pleroma.Web.ConnCase
   import Pleroma.Factory
+  import Tesla.Mock
 
   setup do
-    Tesla.Mock.mock(fn
-      %{
-        method: :get,
-        url: "http://example.com/ogp"
-      } ->
-        %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}
-
-      %{method: :get, url: "http://example.com/empty"} ->
-        %Tesla.Env{status: 200, body: "hello"}
-    end)
-
+    mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
     :ok
   end
 
diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs
index f22cdd870..863abd10f 100644
--- a/test/web/twitter_api/twitter_api_controller_test.exs
+++ b/test/web/twitter_api/twitter_api_controller_test.exs
@@ -797,7 +797,7 @@ test "with credentials, invalid activity", %{conn: conn, user: current_user} do
         |> with_credentials(current_user.nickname, "test")
         |> post("/api/favorites/create/1.json")
 
-      assert json_response(conn, 500)
+      assert json_response(conn, 400)
     end
   end
 
@@ -1621,7 +1621,7 @@ test "it approves a friend request" do
       conn =
         build_conn()
         |> assign(:user, user)
-        |> post("/api/pleroma/friendships/approve", %{"user_id" => to_string(other_user.id)})
+        |> post("/api/pleroma/friendships/approve", %{"user_id" => other_user.id})
 
       assert relationship = json_response(conn, 200)
       assert other_user.id == relationship["id"]
@@ -1644,7 +1644,7 @@ test "it denies a friend request" do
       conn =
         build_conn()
         |> assign(:user, user)
-        |> post("/api/pleroma/friendships/deny", %{"user_id" => to_string(other_user.id)})
+        |> post("/api/pleroma/friendships/deny", %{"user_id" => other_user.id})
 
       assert relationship = json_response(conn, 200)
       assert other_user.id == relationship["id"]
diff --git a/test/web/twitter_api/views/user_view_test.exs b/test/web/twitter_api/views/user_view_test.exs
index 5f7481eb6..daf18c1c5 100644
--- a/test/web/twitter_api/views/user_view_test.exs
+++ b/test/web/twitter_api/views/user_view_test.exs
@@ -100,6 +100,7 @@ test "A user" do
       "locked" => false,
       "default_scope" => "public",
       "no_rich_text" => false,
+      "hide_network" => false,
       "fields" => [],
       "pleroma" => %{
         "confirmation_pending" => false,
@@ -146,6 +147,7 @@ test "A user for a given other follower", %{user: user} do
       "locked" => false,
       "default_scope" => "public",
       "no_rich_text" => false,
+      "hide_network" => false,
       "fields" => [],
       "pleroma" => %{
         "confirmation_pending" => false,
@@ -193,6 +195,7 @@ test "A user that follows you", %{user: user} do
       "locked" => false,
       "default_scope" => "public",
       "no_rich_text" => false,
+      "hide_network" => false,
       "fields" => [],
       "pleroma" => %{
         "confirmation_pending" => false,
@@ -254,6 +257,7 @@ test "A blocked user for the blocker" do
       "locked" => false,
       "default_scope" => "public",
       "no_rich_text" => false,
+      "hide_network" => false,
       "fields" => [],
       "pleroma" => %{
         "confirmation_pending" => false,