diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52e6c33f81ffa9e6e29cfdb39722b2bd982a4cd4..2f5d8f6125389fa20bab3e90ac343739b970daa4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,11 +12,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ### Added
 - NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
+- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
 - Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
 - New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
 <details>
   <summary>API Changes</summary>
 - Mastodon API: Support for `include_types` in `/api/v1/notifications`.
+- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
 </details>
 
 ## [2.0.0] - 2019-03-08
diff --git a/COPYING b/COPYING
index 0aede0fbaf27be6332059f9c50f4a0e436f796ac..3140c8038e6401ca9e11a972621672554c87c03f 100644
--- a/COPYING
+++ b/COPYING
@@ -1,4 +1,4 @@
-Unless otherwise stated this repository is copyright © 2017-2019
+Unless otherwise stated this repository is copyright © 2017-2020
 Pleroma Authors <https://pleroma.social/>, and is distributed under
 The GNU Affero General Public License Version 3, you should have received a
 copy of the license file as AGPL-3.
@@ -23,7 +23,7 @@ priv/static/images/pleroma-fox-tan-shy.png
 
 ---
 
-The following files are copyright © 2017-2019 Pleroma Authors
+The following files are copyright © 2017-2020 Pleroma Authors
 <https://pleroma.social/>, and are distributed under the Creative Commons
 Attribution-ShareAlike 4.0 International license, you should have received
 a copy of the license file as CC-BY-SA-4.0.
diff --git a/coveralls.json b/coveralls.json
new file mode 100644
index 0000000000000000000000000000000000000000..75e845adefba2db0a0c0cc054c8612b8742a6a6b
--- /dev/null
+++ b/coveralls.json
@@ -0,0 +1,6 @@
+{
+  "skip_files": [
+    "test/support",
+    "lib/mix/tasks/pleroma/benchmark.ex"
+  ]
+}
\ No newline at end of file
diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index 58d7023472d0202aa023b7e40790423346d48761..57fb6bc6a9c7a273ab2f741dad0ddfe66ccc20ed 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -392,6 +392,19 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
   - `email`
   - `name`, optional
 
+- Response:
+  - On success: `204`, empty response
+  - On failure:
+    - 400 Bad Request, JSON:
+
+    ```json
+      [
+        {
+          "error": "Appropriate error message here"
+        }
+      ]
+    ```
+
 ## `GET /api/pleroma/admin/users/:nickname/password_reset`
 
 ### Get a password reset token for a given nickname
diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md
index dc8f54d2a3498ea59e935b053739c232946b0a1f..1059155cfccba73ec292a048f7fd5c13fbd3f9ed 100644
--- a/docs/API/differences_in_mastoapi_responses.md
+++ b/docs/API/differences_in_mastoapi_responses.md
@@ -164,6 +164,7 @@ Additional parameters can be added to the JSON body/Form data:
 - `actor_type` - the type of this account.
 
 ### Pleroma Settings Store
+
 Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.
 
 The parameter should have a form of `{frontend_name: {...}}`, with `frontend_name` identifying your type of client, e.g. `pleroma_fe`. It will overwrite everything under this property, but will not overwrite other frontend's settings.
@@ -172,17 +173,20 @@ This information is returned in the `verify_credentials` endpoint.
 
 ## Authentication
 
-*Pleroma supports refreshing tokens.
+*Pleroma supports refreshing tokens.*
 
 `POST /oauth/token`
-Post here request with grant_type=refresh_token to obtain new access token. Returns an access token.
+
+Post here request with `grant_type=refresh_token` to obtain new access token. Returns an access token.
 
 ## Account Registration
+
 `POST /api/v1/accounts`
 
 Has theses additional parameters (which are the same as in Pleroma-API):
-    * `fullname`: optional
-    * `bio`: optional
-    * `captcha_solution`: optional, contains provider-specific captcha solution,
-    * `captcha_token`: optional, contains provider-specific captcha token
-    * `token`: invite token required when the registerations aren't public.
+
+- `fullname`: optional
+- `bio`: optional
+- `captcha_solution`: optional, contains provider-specific captcha solution,
+- `captcha_token`: optional, contains provider-specific captcha token
+- `token`: invite token required when the registrations aren't public.
diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md
index 12e63ef9f75e8c96bdf38789b18a601a41c31d2d..90c43c356be09d2706ca00377535846feffcb5a6 100644
--- a/docs/API/pleroma_api.md
+++ b/docs/API/pleroma_api.md
@@ -431,7 +431,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
 
 # Emoji Reactions
 
-Emoji reactions work a lot like favourites do. They make it possible to react to a post with a single emoji character.
+Emoji reactions work a lot like favourites do. They make it possible to react to a post with a single emoji character. To detect the presence of this feature, you can check `pleroma_emoji_reactions` entry in the features list of nodeinfo.
 
 ## `PUT /api/v1/pleroma/statuses/:id/reactions/:emoji`
 ### React to a post with a unicode emoji
diff --git a/docs/administration/CLI_tasks/emoji.md b/docs/administration/CLI_tasks/emoji.md
index efec8222cb532475caca9d77a1f0b644e37f0848..3d524a52b190b16e1f26ec8f4c139a9b506b462b 100644
--- a/docs/administration/CLI_tasks/emoji.md
+++ b/docs/administration/CLI_tasks/emoji.md
@@ -39,8 +39,8 @@ mix pleroma.emoji get-packs [option ...] <pack ...>
 mix pleroma.emoji gen-pack PACK-URL
 ```
 
-Currently, only .zip archives are recognized as remote pack files and packs are therefore assumed to be zip archives. This command is intended to run interactively and will first ask you some basic questions about the pack, then download the remote file and generate an SHA256 checksum for it, then generate an emoji file list for you. 
+Currently, only .zip archives are recognized as remote pack files and packs are therefore assumed to be zip archives. This command is intended to run interactively and will first ask you some basic questions about the pack, then download the remote file and generate an SHA256 checksum for it, then generate an emoji file list for you.
 
-  The manifest entry will either be written to a newly created `index.json` file or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously.
+  The manifest entry will either be written to a newly created `pack_name.json` file (pack name is asked in questions) or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously.
 
   The file list will be written to the file specified previously, *replacing* that file. You _should_ check that the file list doesn't contain anything you don't need in the pack, that is, anything that is not an emoji (the whole pack is downloaded, but only emoji files are extracted).
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
index 429d763c7b386ac0d45201262c998da6dce0d55f..cdffa88b28c827af1863e1d8b5e245b7bac629c7 100644
--- a/lib/mix/tasks/pleroma/emoji.ex
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -14,8 +14,8 @@ def run(["ls-packs" | args]) do
 
     {options, [], []} = parse_global_opts(args)
 
-    manifest =
-      fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest())
+    url_or_path = options[:manifest] || default_manifest()
+    manifest = fetch_manifest(url_or_path)
 
     Enum.each(manifest, fn {name, info} ->
       to_print = [
@@ -40,9 +40,9 @@ def run(["get-packs" | args]) do
 
     {options, pack_names, []} = parse_global_opts(args)
 
-    manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest()
+    url_or_path = options[:manifest] || default_manifest()
 
-    manifest = fetch_manifest(manifest_url)
+    manifest = fetch_manifest(url_or_path)
 
     for pack_name <- pack_names do
       if Map.has_key?(manifest, pack_name) do
@@ -75,7 +75,10 @@ def run(["get-packs" | args]) do
         end
 
         # The url specified in files should be in the same directory
-        files_url = Path.join(Path.dirname(manifest_url), pack["files"])
+        files_url =
+          url_or_path
+          |> Path.dirname()
+          |> Path.join(pack["files"])
 
         IO.puts(
           IO.ANSI.format([
@@ -133,38 +136,51 @@ def run(["get-packs" | args]) do
     end
   end
 
-  def run(["gen-pack", src]) do
+  def run(["gen-pack" | args]) do
     start_pleroma()
 
-    proposed_name = Path.basename(src) |> Path.rootname()
-    name = String.trim(IO.gets("Pack name [#{proposed_name}]: "))
-    # If there's no name, use the default one
-    name = if String.length(name) > 0, do: name, else: proposed_name
+    {opts, [src], []} =
+      OptionParser.parse(
+        args,
+        strict: [
+          name: :string,
+          license: :string,
+          homepage: :string,
+          description: :string,
+          files: :string,
+          extensions: :string
+        ]
+      )
 
-    license = String.trim(IO.gets("License: "))
-    homepage = String.trim(IO.gets("Homepage: "))
-    description = String.trim(IO.gets("Description: "))
+    proposed_name = Path.basename(src) |> Path.rootname()
+    name = get_option(opts, :name, "Pack name:", proposed_name)
+    license = get_option(opts, :license, "License:")
+    homepage = get_option(opts, :homepage, "Homepage:")
+    description = get_option(opts, :description, "Description:")
 
-    proposed_files_name = "#{name}.json"
-    files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: "))
-    files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name
+    proposed_files_name = "#{name}_files.json"
+    files_name = get_option(opts, :files, "Save file list to:", proposed_files_name)
 
     default_exts = [".png", ".gif"]
-    default_exts_str = Enum.join(default_exts, " ")
 
-    exts =
-      String.trim(
-        IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ")
+    custom_exts =
+      get_option(
+        opts,
+        :extensions,
+        "Emoji file extensions (separated with spaces):",
+        Enum.join(default_exts, " ")
       )
+      |> String.split(" ", trim: true)
 
     exts =
-      if String.length(exts) > 0 do
-        String.split(exts, " ")
-        |> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end)
-      else
+      if MapSet.equal?(MapSet.new(default_exts), MapSet.new(custom_exts)) do
         default_exts
+      else
+        custom_exts
       end
 
+    IO.puts("Using #{Enum.join(exts, " ")} extensions")
+
     IO.puts("Downloading the pack and generating SHA256")
 
     binary_archive = Tesla.get!(client(), src).body
@@ -194,14 +210,16 @@ def run(["gen-pack", src]) do
     IO.puts("""
 
     #{files_name} has been created and contains the list of all found emojis in the pack.
-    Please review the files in the remove those not needed.
+    Please review the files in the pack and remove those not needed.
     """)
 
-    if File.exists?("index.json") do
-      existing_data = File.read!("index.json") |> Jason.decode!()
+    pack_file = "#{name}.json"
+
+    if File.exists?(pack_file) do
+      existing_data = File.read!(pack_file) |> Jason.decode!()
 
       File.write!(
-        "index.json",
+        pack_file,
         Jason.encode!(
           Map.merge(
             existing_data,
@@ -211,11 +229,11 @@ def run(["gen-pack", src]) do
         )
       )
 
-      IO.puts("index.json file has been update with the #{name} pack")
+      IO.puts("#{pack_file} has been updated with the #{name} pack")
     else
-      File.write!("index.json", Jason.encode!(pack_json, pretty: true))
+      File.write!(pack_file, Jason.encode!(pack_json, pretty: true))
 
-      IO.puts("index.json has been created with the #{name} pack")
+      IO.puts("#{pack_file} has been created with the #{name} pack")
     end
   end
 
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index e2a658cb3c8ee7a76a47473929746f2ed51a2169..c44e7fc8bbe90b27c6b77a2bfad7f6c209377ada 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -35,9 +35,19 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
         nickname_text = get_nickname_text(nickname, opts)
 
         link =
-          ~s(<span class="h-card"><a data-user="#{id}" class="u-url mention" href="#{ap_id}" rel="ugc">@<span>#{
-            nickname_text
-          }</span></a></span>)
+          Phoenix.HTML.Tag.content_tag(
+            :span,
+            Phoenix.HTML.Tag.content_tag(
+              :a,
+              ["@", Phoenix.HTML.Tag.content_tag(:span, nickname_text)],
+              "data-user": id,
+              class: "u-url mention",
+              href: ap_id,
+              rel: "ugc"
+            ),
+            class: "h-card"
+          )
+          |> Phoenix.HTML.safe_to_string()
 
         {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
 
@@ -49,7 +59,15 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
   def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
     tag = String.downcase(tag)
     url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
-    link = ~s(<a class="hashtag" data-tag="#{tag}" href="#{url}" rel="tag ugc">#{tag_text}</a>)
+
+    link =
+      Phoenix.HTML.Tag.content_tag(:a, tag_text,
+        class: "hashtag",
+        "data-tag": tag,
+        href: url,
+        rel: "tag ugc"
+      )
+      |> Phoenix.HTML.safe_to_string()
 
     {link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
   end
diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex
index 20823a7658daa7c926887eda807966737ce20a04..cd25a2e746d1889f9b263daff6a3200917177780 100644
--- a/lib/pleroma/gun/conn.ex
+++ b/lib/pleroma/gun/conn.ex
@@ -49,8 +49,10 @@ def open(%URI{} = uri, name, opts) do
 
     key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
 
+    max_connections = pool_opts[:max_connections] || 250
+
     conn_pid =
-      if Connections.count(name) < opts[:max_connection] do
+      if Connections.count(name) < max_connections do
         do_open(uri, opts)
       else
         close_least_used_and_do_open(name, uri, opts)
diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex
index 9ae6a5600ca1cfc09253244a6a6bb1378c0f33d5..99608b8a5540c68e367158e89590bc0c123ffc1b 100644
--- a/lib/pleroma/object/containment.ex
+++ b/lib/pleroma/object/containment.ex
@@ -32,6 +32,18 @@ def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor)
     get_actor(%{"actor" => actor})
   end
 
+  def get_object(%{"object" => id}) when is_binary(id) do
+    id
+  end
+
+  def get_object(%{"object" => %{"id" => id}}) when is_binary(id) do
+    id
+  end
+
+  def get_object(_) do
+    nil
+  end
+
   # TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus
   # objects being present in the test suite environment.  Once these objects are
   # removed, please also remove this.
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index ff828aa17f33478cd3759ea1f6e9d48d7093397e..71c8c3a4efe8e1975ff11c2469c782a30e4b274f 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -16,6 +16,7 @@ defmodule Pleroma.User do
   alias Pleroma.Conversation.Participation
   alias Pleroma.Delivery
   alias Pleroma.FollowingRelationship
+  alias Pleroma.Formatter
   alias Pleroma.HTML
   alias Pleroma.Keys
   alias Pleroma.Notification
@@ -452,7 +453,7 @@ defp put_fields(changeset) do
 
       fields =
         raw_fields
-        |> Enum.map(fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
+        |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
 
       changeset
       |> put_change(:raw_fields, raw_fields)
@@ -462,6 +463,12 @@ defp put_fields(changeset) do
     end
   end
 
+  defp parse_fields(value) do
+    value
+    |> Formatter.linkify(mentions_format: :full)
+    |> elem(0)
+  end
+
   defp put_change_if_present(changeset, map_field, value_function) do
     if value = get_change(changeset, map_field) do
       with {:ok, new_value} <- value_function.(value) do
@@ -1979,17 +1986,6 @@ def fields(%{fields: nil}), do: []
 
   def fields(%{fields: fields}), do: fields
 
-  def sanitized_fields(%User{} = user) do
-    user
-    |> User.fields()
-    |> Enum.map(fn %{"name" => name, "value" => value} ->
-      %{
-        "name" => name,
-        "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
-      }
-    end)
-  end
-
   def validate_fields(changeset, remote? \\ false) do
     limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
     limit = Pleroma.Config.get([:instance, limit_name], 0)
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 53b6ad654b692d652ebf64ce81e59d30de653ad4..19286fd01a29f9f75dfb8ed78e9a2d94694368d3 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -125,6 +125,21 @@ def increase_poll_votes_if_vote(%{
 
   def increase_poll_votes_if_vote(_create_data), do: :noop
 
+  @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
+  def persist(object, meta) do
+    with local <- Keyword.fetch!(meta, :local),
+         {recipients, _, _} <- get_recipients(object),
+         {:ok, activity} <-
+           Repo.insert(%Activity{
+             data: object,
+             local: local,
+             recipients: recipients,
+             actor: object["actor"]
+           }) do
+      {:ok, activity, meta}
+    end
+  end
+
   @spec insert(map(), boolean(), boolean(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
   def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
     with nil <- Activity.normalize(map),
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
new file mode 100644
index 0000000000000000000000000000000000000000..429a510b8118df4f136f614de281131fd5dd78b8
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -0,0 +1,43 @@
+defmodule Pleroma.Web.ActivityPub.Builder do
+  @moduledoc """
+  This module builds the objects. Meant to be used for creating local objects.
+
+  This module encodes our addressing policies and general shape of our objects.
+  """
+
+  alias Pleroma.Object
+  alias Pleroma.User
+  alias Pleroma.Web.ActivityPub.Utils
+  alias Pleroma.Web.ActivityPub.Visibility
+
+  @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
+  def like(actor, object) do
+    object_actor = User.get_cached_by_ap_id(object.data["actor"])
+
+    # Address the actor of the object, and our actor's follower collection if the post is public.
+    to =
+      if Visibility.is_public?(object) do
+        [actor.follower_address, object.data["actor"]]
+      else
+        [object.data["actor"]]
+      end
+
+    # CC everyone who's been addressed in the object, except ourself and the object actor's
+    # follower collection
+    cc =
+      (object.data["to"] ++ (object.data["cc"] || []))
+      |> List.delete(actor.ap_id)
+      |> List.delete(object_actor.follower_address)
+
+    {:ok,
+     %{
+       "id" => Utils.generate_activity_id(),
+       "actor" => actor.ap_id,
+       "type" => "Like",
+       "object" => object.data["id"],
+       "to" => to,
+       "cc" => cc,
+       "context" => object.data["context"]
+     }, []}
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
new file mode 100644
index 0000000000000000000000000000000000000000..dc4bce0595a12c409475206b2aed0e7222b7ce18
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -0,0 +1,37 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidator do
+  @moduledoc """
+  This module is responsible for validating an object (which can be an activity)
+  and checking if it is both well formed and also compatible with our view of
+  the system.
+  """
+
+  alias Pleroma.Object
+  alias Pleroma.User
+  alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+
+  @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
+  def validate(object, meta)
+
+  def validate(%{"type" => "Like"} = object, meta) do
+    with {:ok, object} <-
+           object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
+      object = stringify_keys(object |> Map.from_struct())
+      {:ok, object, meta}
+    end
+  end
+
+  def stringify_keys(object) do
+    object
+    |> Map.new(fn {key, val} -> {to_string(key), val} end)
+  end
+
+  def fetch_actor_and_object(object) do
+    User.get_or_fetch_by_ap_id(object["actor"])
+    Object.normalize(object["object"])
+    :ok
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
new file mode 100644
index 0000000000000000000000000000000000000000..b479c391837f1ccf20dfabd5134b562054e4dbb8
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
@@ -0,0 +1,32 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
+  import Ecto.Changeset
+
+  alias Pleroma.Object
+  alias Pleroma.User
+
+  def validate_actor_presence(cng, field_name \\ :actor) do
+    cng
+    |> validate_change(field_name, fn field_name, actor ->
+      if User.get_cached_by_ap_id(actor) do
+        []
+      else
+        [{field_name, "can't find user"}]
+      end
+    end)
+  end
+
+  def validate_object_presence(cng, field_name \\ :object) do
+    cng
+    |> validate_change(field_name, fn field_name, object ->
+      if Object.get_cached_by_ap_id(object) do
+        []
+      else
+        [{field_name, "can't find object"}]
+      end
+    end)
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_validator.ex
new file mode 100644
index 0000000000000000000000000000000000000000..926804ce74c18311fbf0516e9ea8ae7a21245b75
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/create_validator.ex
@@ -0,0 +1,30 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateNoteValidator do
+  use Ecto.Schema
+
+  alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+  import Ecto.Changeset
+
+  @primary_key false
+
+  embedded_schema do
+    field(:id, Types.ObjectID, primary_key: true)
+    field(:actor, Types.ObjectID)
+    field(:type, :string)
+    field(:to, {:array, :string})
+    field(:cc, {:array, :string})
+    field(:bto, {:array, :string}, default: [])
+    field(:bcc, {:array, :string}, default: [])
+
+    embeds_one(:object, NoteValidator)
+  end
+
+  def cast_data(data) do
+    cast(%__MODULE__{}, data, __schema__(:fields))
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
new file mode 100644
index 0000000000000000000000000000000000000000..49546ceaaa22c33d331e5c5f9cb9818fa147da1c
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
+  use Ecto.Schema
+
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+  alias Pleroma.Web.ActivityPub.Utils
+
+  import Ecto.Changeset
+  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+  @primary_key false
+
+  embedded_schema do
+    field(:id, Types.ObjectID, primary_key: true)
+    field(:type, :string)
+    field(:object, Types.ObjectID)
+    field(:actor, Types.ObjectID)
+    field(:context, :string)
+    field(:to, {:array, :string})
+    field(:cc, {:array, :string})
+  end
+
+  def cast_and_validate(data) do
+    data
+    |> cast_data()
+    |> validate_data()
+  end
+
+  def cast_data(data) do
+    %__MODULE__{}
+    |> cast(data, [:id, :type, :object, :actor, :context, :to, :cc])
+  end
+
+  def validate_data(data_cng) do
+    data_cng
+    |> validate_inclusion(:type, ["Like"])
+    |> validate_required([:id, :type, :object, :actor, :context, :to, :cc])
+    |> validate_actor_presence()
+    |> validate_object_presence()
+    |> validate_existing_like()
+  end
+
+  def validate_existing_like(%{changes: %{actor: actor, object: object}} = cng) do
+    if Utils.get_existing_like(actor, %{data: %{"id" => object}}) do
+      cng
+      |> add_error(:actor, "already liked this object")
+      |> add_error(:object, "already liked by this actor")
+    else
+      cng
+    end
+  end
+
+  def validate_existing_like(cng), do: cng
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex
new file mode 100644
index 0000000000000000000000000000000000000000..c95b622e48e77f6b1045171d51e63cee4c10d64a
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex
@@ -0,0 +1,63 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
+  use Ecto.Schema
+
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+  import Ecto.Changeset
+
+  @primary_key false
+
+  embedded_schema do
+    field(:id, Types.ObjectID, primary_key: true)
+    field(:to, {:array, :string}, default: [])
+    field(:cc, {:array, :string}, default: [])
+    field(:bto, {:array, :string}, default: [])
+    field(:bcc, {:array, :string}, default: [])
+    # TODO: Write type
+    field(:tag, {:array, :map}, default: [])
+    field(:type, :string)
+    field(:content, :string)
+    field(:context, :string)
+    field(:actor, Types.ObjectID)
+    field(:attributedTo, Types.ObjectID)
+    field(:summary, :string)
+    field(:published, Types.DateTime)
+    # TODO: Write type
+    field(:emoji, :map, default: %{})
+    field(:sensitive, :boolean, default: false)
+    # TODO: Write type
+    field(:attachment, {:array, :map}, default: [])
+    field(:replies_count, :integer, default: 0)
+    field(:like_count, :integer, default: 0)
+    field(:announcement_count, :integer, default: 0)
+    field(:inRepyTo, :string)
+
+    field(:likes, {:array, :string}, default: [])
+    field(:announcements, {:array, :string}, default: [])
+
+    # see if needed
+    field(:conversation, :string)
+    field(:context_id, :string)
+  end
+
+  def cast_and_validate(data) do
+    data
+    |> cast_data()
+    |> validate_data()
+  end
+
+  def cast_data(data) do
+    %__MODULE__{}
+    |> cast(data, __schema__(:fields))
+  end
+
+  def validate_data(data_cng) do
+    data_cng
+    |> validate_inclusion(:type, ["Note"])
+    |> validate_required([:id, :actor, :to, :cc, :type, :content, :context])
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex b/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex
new file mode 100644
index 0000000000000000000000000000000000000000..4f412fcde5ab0643f7b953382bf13551537ce27b
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex
@@ -0,0 +1,34 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTime do
+  @moduledoc """
+  The AP standard defines the date fields in AP as xsd:DateTime. Elixir's
+  DateTime can't parse this, but it can parse the related iso8601. This
+  module punches the date until it looks like iso8601 and normalizes to
+  it.
+
+  DateTimes without a timezone offset are treated as UTC.
+
+  Reference: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-published
+  """
+  use Ecto.Type
+
+  def type, do: :string
+
+  def cast(datetime) when is_binary(datetime) do
+    with {:ok, datetime, _} <- DateTime.from_iso8601(datetime) do
+      {:ok, DateTime.to_iso8601(datetime)}
+    else
+      {:error, :missing_offset} -> cast("#{datetime}Z")
+      _e -> :error
+    end
+  end
+
+  def cast(_), do: :error
+
+  def dump(data) do
+    {:ok, data}
+  end
+
+  def load(data) do
+    {:ok, data}
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex b/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex
new file mode 100644
index 0000000000000000000000000000000000000000..f6e749b337249db0c71e0d1b5b0ebc7dcbababd5
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex
@@ -0,0 +1,29 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do
+  use Ecto.Type
+
+  def type, do: :string
+
+  def cast(object) when is_binary(object) do
+    # Host has to be present and scheme has to be an http scheme (for now)
+    case URI.parse(object) do
+      %URI{host: nil} -> :error
+      %URI{host: ""} -> :error
+      %URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, object}
+      _ -> :error
+    end
+  end
+
+  def cast(%{"id" => object}), do: cast(object)
+
+  def cast(_) do
+    :error
+  end
+
+  def dump(data) do
+    {:ok, data}
+  end
+
+  def load(data) do
+    {:ok, data}
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex
new file mode 100644
index 0000000000000000000000000000000000000000..7ccee54c9d829d14204bde726a2958c6e41c54c5
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.Pipeline do
+  alias Pleroma.Activity
+  alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.ActivityPub.MRF
+  alias Pleroma.Web.ActivityPub.ObjectValidator
+  alias Pleroma.Web.ActivityPub.SideEffects
+  alias Pleroma.Web.Federator
+
+  @spec common_pipeline(map(), keyword()) :: {:ok, Activity.t(), keyword()} | {:error, any()}
+  def common_pipeline(object, meta) do
+    with {_, {:ok, validated_object, meta}} <-
+           {:validate_object, ObjectValidator.validate(object, meta)},
+         {_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)},
+         {_, {:ok, %Activity{} = activity, meta}} <-
+           {:persist_object, ActivityPub.persist(mrfd_object, meta)},
+         {_, {:ok, %Activity{} = activity, meta}} <-
+           {:execute_side_effects, SideEffects.handle(activity, meta)},
+         {_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
+      {:ok, activity, meta}
+    else
+      {:mrf_object, {:reject, _}} -> {:ok, nil, meta}
+      e -> {:error, e}
+    end
+  end
+
+  defp maybe_federate(activity, meta) do
+    with {:ok, local} <- Keyword.fetch(meta, :local) do
+      if local do
+        Federator.publish(activity)
+        {:ok, :federated}
+      else
+        {:ok, :not_federated}
+      end
+    else
+      _e -> {:error, :badarg}
+    end
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
new file mode 100644
index 0000000000000000000000000000000000000000..666a4e310a6a116c1f68e0dafc6fd30686f0bd2f
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -0,0 +1,28 @@
+defmodule Pleroma.Web.ActivityPub.SideEffects do
+  @moduledoc """
+  This module looks at an inserted object and executes the side effects that it
+  implies. For example, a `Like` activity will increase the like count on the
+  liked object, a `Follow` activity will add the user to the follower
+  collection, and so on.
+  """
+  alias Pleroma.Notification
+  alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.Utils
+
+  def handle(object, meta \\ [])
+
+  # Tasks this handles:
+  # - Add like to object
+  # - Set up notification
+  def handle(%{data: %{"type" => "Like"}} = object, meta) do
+    liked_object = Object.get_by_ap_id(object.data["object"])
+    Utils.add_like_to_object(object, liked_object)
+    Notification.create_notifications(object)
+    {:ok, object, meta}
+  end
+
+  # Nothing to do
+  def handle(object, meta) do
+    {:ok, object, meta}
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 09bd9a44290679477778ffa6548e290632daa43a..0a8ad62ad28c2ceb04398b707fa34d65b63e6c10 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -13,6 +13,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.ActivityPub.ObjectValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+  alias Pleroma.Web.ActivityPub.Pipeline
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.ActivityPub.Visibility
   alias Pleroma.Web.Federator
@@ -609,17 +612,20 @@ def handle_incoming(
     |> handle_incoming(options)
   end
 
-  def handle_incoming(
-        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
-        _options
-      ) do
-    with actor <- Containment.get_actor(data),
-         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
-         {:ok, object} <- get_obj_helper(object_id),
-         {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
+  def handle_incoming(%{"type" => "Like"} = data, _options) do
+    with {_, {:ok, cast_data_sym}} <-
+           {:casting_data,
+            data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
+         cast_data = ObjectValidator.stringify_keys(Map.from_struct(cast_data_sym)),
+         :ok <- ObjectValidator.fetch_actor_and_object(cast_data),
+         {_, {:ok, cast_data}} <- {:ensure_context_presence, ensure_context_presence(cast_data)},
+         {_, {:ok, cast_data}} <-
+           {:ensure_recipients_presence, ensure_recipients_presence(cast_data)},
+         {_, {:ok, activity, _meta}} <-
+           {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
       {:ok, activity}
     else
-      _e -> :error
+      e -> {:error, e}
     end
   end
 
@@ -1243,4 +1249,45 @@ def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
   def maybe_fix_user_url(data), do: data
 
   def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
+
+  defp ensure_context_presence(%{"context" => context} = data) when is_binary(context),
+    do: {:ok, data}
+
+  defp ensure_context_presence(%{"object" => object} = data) when is_binary(object) do
+    with %{data: %{"context" => context}} when is_binary(context) <- Object.normalize(object) do
+      {:ok, Map.put(data, "context", context)}
+    else
+      _ ->
+        {:error, :no_context}
+    end
+  end
+
+  defp ensure_context_presence(_) do
+    {:error, :no_context}
+  end
+
+  defp ensure_recipients_presence(%{"to" => [_ | _], "cc" => [_ | _]} = data),
+    do: {:ok, data}
+
+  defp ensure_recipients_presence(%{"object" => object} = data) do
+    case Object.normalize(object) do
+      %{data: %{"actor" => actor}} ->
+        data =
+          data
+          |> Map.put("to", [actor])
+          |> Map.put("cc", data["cc"] || [])
+
+        {:ok, data}
+
+      nil ->
+        {:error, :no_object}
+
+      _ ->
+        {:error, :no_actor}
+    end
+  end
+
+  defp ensure_recipients_presence(_) do
+    {:error, :no_object}
+  end
 end
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index ca54399204c0357141788433ad004ab1b0e72322..fdbd24acb61f43806c986733b4e086436adfe4d5 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -576,9 +576,8 @@ def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target})
 
   @doc "Sends registration invite via email"
   def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
-    with true <-
-           Config.get([:instance, :invites_enabled]) &&
-             !Config.get([:instance, :registrations_open]),
+    with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
+         {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
          {:ok, invite_token} <- UserInviteToken.create_invite(),
          email <-
            Pleroma.Emails.UserEmail.user_invitation_email(
@@ -589,6 +588,18 @@ def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params)
            ),
          {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
       json_response(conn, :no_content, "")
+    else
+      {:registrations_open, _} ->
+        errors(
+          conn,
+          {:error, "To send invites you need to set the `registrations_open` option to false."}
+        )
+
+      {:invites_enabled, _} ->
+        errors(
+          conn,
+          {:error, "To send invites you need to set the `invites_enabled` option to true."}
+        )
     end
   end
 
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
new file mode 100644
index 0000000000000000000000000000000000000000..41e48a0850a5bcd48cdd77e0aba92fd64e68d146
--- /dev/null
+++ b/lib/pleroma/web/api_spec.ex
@@ -0,0 +1,44 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec do
+  alias OpenApiSpex.OpenApi
+  alias Pleroma.Web.Endpoint
+  alias Pleroma.Web.Router
+
+  @behaviour OpenApi
+
+  @impl OpenApi
+  def spec do
+    %OpenApi{
+      servers: [
+        # Populate the Server info from a phoenix endpoint
+        OpenApiSpex.Server.from_endpoint(Endpoint)
+      ],
+      info: %OpenApiSpex.Info{
+        title: "Pleroma",
+        description: Application.spec(:pleroma, :description) |> to_string(),
+        version: Application.spec(:pleroma, :vsn) |> to_string()
+      },
+      # populate the paths from a phoenix router
+      paths: OpenApiSpex.Paths.from_router(Router),
+      components: %OpenApiSpex.Components{
+        securitySchemes: %{
+          "oAuth" => %OpenApiSpex.SecurityScheme{
+            type: "oauth2",
+            flows: %OpenApiSpex.OAuthFlows{
+              password: %OpenApiSpex.OAuthFlow{
+                authorizationUrl: "/oauth/authorize",
+                tokenUrl: "/oauth/token",
+                scopes: %{"read" => "read"}
+              }
+            }
+          }
+        }
+      }
+    }
+    # discover request/response schemas from path specs
+    |> OpenApiSpex.resolve_schema_modules()
+  end
+end
diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex
new file mode 100644
index 0000000000000000000000000000000000000000..35cf4c0d8313e7c9ff0712f23eef462e593dd4d2
--- /dev/null
+++ b/lib/pleroma/web/api_spec/helpers.ex
@@ -0,0 +1,27 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Helpers do
+  def request_body(description, schema_ref, opts \\ []) do
+    media_types = ["application/json", "multipart/form-data"]
+
+    content =
+      media_types
+      |> Enum.map(fn type ->
+        {type,
+         %OpenApiSpex.MediaType{
+           schema: schema_ref,
+           example: opts[:example],
+           examples: opts[:examples]
+         }}
+      end)
+      |> Enum.into(%{})
+
+    %OpenApiSpex.RequestBody{
+      description: description,
+      content: content,
+      required: opts[:required] || false
+    }
+  end
+end
diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex
new file mode 100644
index 0000000000000000000000000000000000000000..26d8dbd421a7331367ad73fca00587971ee44923
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/app_operation.ex
@@ -0,0 +1,96 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.AppOperation do
+  alias OpenApiSpex.Operation
+  alias OpenApiSpex.Schema
+  alias Pleroma.Web.ApiSpec.Helpers
+  alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
+  alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
+
+  @spec open_api_operation(atom) :: Operation.t()
+  def open_api_operation(action) do
+    operation = String.to_existing_atom("#{action}_operation")
+    apply(__MODULE__, operation, [])
+  end
+
+  @spec create_operation() :: Operation.t()
+  def create_operation do
+    %Operation{
+      tags: ["apps"],
+      summary: "Create an application",
+      description: "Create a new application to obtain OAuth2 credentials",
+      operationId: "AppController.create",
+      requestBody: Helpers.request_body("Parameters", AppCreateRequest, required: true),
+      responses: %{
+        200 => Operation.response("App", "application/json", AppCreateResponse),
+        422 =>
+          Operation.response(
+            "Unprocessable Entity",
+            "application/json",
+            %Schema{
+              type: :object,
+              description:
+                "If a required parameter is missing or improperly formatted, the request will fail.",
+              properties: %{
+                error: %Schema{type: :string}
+              },
+              example: %{
+                "error" => "Validation failed: Redirect URI must be an absolute URI."
+              }
+            }
+          )
+      }
+    }
+  end
+
+  def verify_credentials_operation do
+    %Operation{
+      tags: ["apps"],
+      summary: "Verify your app works",
+      description: "Confirm that the app's OAuth2 credentials work.",
+      operationId: "AppController.verify_credentials",
+      security: [
+        %{
+          "oAuth" => ["read"]
+        }
+      ],
+      responses: %{
+        200 =>
+          Operation.response("App", "application/json", %Schema{
+            type: :object,
+            description:
+              "If the Authorization header was provided with a valid token, you should see your app returned as an Application entity.",
+            properties: %{
+              name: %Schema{type: :string},
+              vapid_key: %Schema{type: :string},
+              website: %Schema{type: :string, nullable: true}
+            },
+            example: %{
+              "name" => "My App",
+              "vapid_key" =>
+                "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
+              "website" => "https://myapp.com/"
+            }
+          }),
+        422 =>
+          Operation.response(
+            "Unauthorized",
+            "application/json",
+            %Schema{
+              type: :object,
+              description:
+                "If the Authorization header contains an invalid token, is malformed, or is not present, an error will be returned indicating an authorization failure.",
+              properties: %{
+                error: %Schema{type: :string}
+              },
+              example: %{
+                "error" => "The access token is invalid."
+              }
+            }
+          )
+      }
+    }
+  end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/app_create_request.ex b/lib/pleroma/web/api_spec/schemas/app_create_request.ex
new file mode 100644
index 0000000000000000000000000000000000000000..8a83abef3eb1f4e5771c01235eec32eb14cbffac
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/app_create_request.ex
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateRequest do
+  alias OpenApiSpex.Schema
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "AppCreateRequest",
+    description: "POST body for creating an app",
+    type: :object,
+    properties: %{
+      client_name: %Schema{type: :string, description: "A name for your application."},
+      redirect_uris: %Schema{
+        type: :string,
+        description:
+          "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
+      },
+      scopes: %Schema{
+        type: :string,
+        description: "Space separated list of scopes. If none is provided, defaults to `read`."
+      },
+      website: %Schema{type: :string, description: "A URL to the homepage of your app"}
+    },
+    required: [:client_name, :redirect_uris],
+    example: %{
+      "client_name" => "My App",
+      "redirect_uris" => "https://myapp.com/auth/callback",
+      "website" => "https://myapp.com/"
+    }
+  })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/app_create_response.ex b/lib/pleroma/web/api_spec/schemas/app_create_response.ex
new file mode 100644
index 0000000000000000000000000000000000000000..f290fb031792e3cc0c5b9b13d66fddacc93be9e3
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/app_create_response.ex
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateResponse do
+  alias OpenApiSpex.Schema
+
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "AppCreateResponse",
+    description: "Response schema for an app",
+    type: :object,
+    properties: %{
+      id: %Schema{type: :string},
+      name: %Schema{type: :string},
+      client_id: %Schema{type: :string},
+      client_secret: %Schema{type: :string},
+      redirect_uri: %Schema{type: :string},
+      vapid_key: %Schema{type: :string},
+      website: %Schema{type: :string, nullable: true}
+    },
+    example: %{
+      "id" => "123",
+      "name" => "My App",
+      "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
+      "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
+      "vapid_key" =>
+        "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
+      "website" => "https://myapp.com/"
+    }
+  })
+end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 2646b9f7b8887c31cc685e54c4c3a68be15da0c6..636cf3301e14c5f8a7e5a3f06d2da83f2c9dea57 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -12,6 +12,8 @@ defmodule Pleroma.Web.CommonAPI do
   alias Pleroma.User
   alias Pleroma.UserRelationship
   alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.ActivityPub.Builder
+  alias Pleroma.Web.ActivityPub.Pipeline
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.ActivityPub.Visibility
 
@@ -19,6 +21,7 @@ defmodule Pleroma.Web.CommonAPI do
   import Pleroma.Web.CommonAPI.Utils
 
   require Pleroma.Constants
+  require Logger
 
   def follow(follower, followed) do
     timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
@@ -109,18 +112,51 @@ def unrepeat(id_or_ap_id, user) do
     end
   end
 
-  def favorite(id_or_ap_id, user) do
-    with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
-         object <- Object.normalize(activity),
-         like_activity <- Utils.get_existing_like(user.ap_id, object) do
-      if like_activity do
-        {:ok, like_activity, object}
-      else
-        ActivityPub.like(user, object)
-      end
+  @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
+  def favorite(%User{} = user, id) do
+    case favorite_helper(user, id) do
+      {:ok, _} = res ->
+        res
+
+      {:error, :not_found} = res ->
+        res
+
+      {:error, e} ->
+        Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
+        {:error, dgettext("errors", "Could not favorite")}
+    end
+  end
+
+  def favorite_helper(user, id) do
+    with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
+         {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
+         {_, {:ok, %Activity{} = activity, _meta}} <-
+           {:common_pipeline,
+            Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
+      {:ok, activity}
     else
-      {:find_activity, _} -> {:error, :not_found}
-      _ -> {:error, dgettext("errors", "Could not favorite")}
+      {:find_object, _} ->
+        {:error, :not_found}
+
+      {:common_pipeline,
+       {
+         :error,
+         {
+           :validate_object,
+           {
+             :error,
+             changeset
+           }
+         }
+       }} = e ->
+        if {:object, {"already liked by this actor", []}} in changeset.errors do
+          {:ok, :already_liked}
+        else
+          {:error, e}
+        end
+
+      e ->
+        {:error, e}
     end
   end
 
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
index 5e2871f185ea7fb2d0248141dee4ef10e01cf811..005c604447e3999cf756ec2dd1b842930d46df5e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -14,17 +14,20 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
   plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
+  plug(OpenApiSpex.Plug.CastAndValidate)
 
   @local_mastodon_name "Mastodon-Local"
 
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AppOperation
+
   @doc "POST /api/v1/apps"
-  def create(conn, params) do
+  def create(%{body_params: params} = conn, _params) do
     scopes = Scopes.fetch_scopes(params, ["read"])
 
     app_attrs =
       params
-      |> Map.drop(["scope", "scopes"])
-      |> Map.put("scopes", scopes)
+      |> Map.take([:client_name, :redirect_uris, :website])
+      |> Map.put(:scopes, scopes)
 
     with cs <- App.register_changeset(%App{}, app_attrs),
          false <- cs.changes[:client_name] == @local_mastodon_name,
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index 0c9218454454768fee8f9335e1cd10c5e9f5d57a..a6b4096ec6f594b1d0f92800308293230f14e463 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -66,7 +66,8 @@ def clear(%{assigns: %{user: user}} = conn, _params) do
     json(conn, %{})
   end
 
-  # POST /api/v1/notifications/dismiss
+  # POST /api/v1/notifications/:id/dismiss
+  # POST /api/v1/notifications/dismiss (deprecated)
   def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
     with {:ok, _notif} <- Notification.dismiss(user, id) do
       json(conn, %{})
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index 37afe6949f29f1e116beb5f6c1d88fdf3c19852f..ec8f0d8a067fa16668ab05c128932c6e0fcbde29 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -207,9 +207,9 @@ def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
   end
 
   @doc "POST /api/v1/statuses/:id/favourite"
-  def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
-    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
-         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+  def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
+    with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
+         %Activity{} = activity <- Activity.get_by_id(activity_id) do
       try_render(conn, "show.json", activity: activity, for: user, as: :activity)
     end
   end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index c482bba6498d4c806e37a9b540663c84eb8506b6..99e62f580c4f613a1ae32e3198363285fc93810f 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -13,16 +13,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
   alias Pleroma.Web.MediaProxy
 
   def render("index.json", %{users: users} = opts) do
+    reading_user = opts[:for]
+
     relationships_opt =
       cond do
         Map.has_key?(opts, :relationships) ->
           opts[:relationships]
 
-        is_nil(opts[:for]) ->
+        is_nil(reading_user) ->
           UserRelationship.view_relationships_option(nil, [])
 
         true ->
-          UserRelationship.view_relationships_option(opts[:for], users)
+          UserRelationship.view_relationships_option(reading_user, users)
       end
 
     opts = Map.put(opts, :relationships, relationships_opt)
@@ -143,7 +145,7 @@ def render("relationships.json", %{user: user, targets: targets} = opts) do
         Map.has_key?(opts, :relationships) ->
           opts[:relationships]
 
-        is_nil(opts[:for]) ->
+        is_nil(user) ->
           UserRelationship.view_relationships_option(nil, [])
 
         true ->
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index 89f5734ffaee84d618dd260fc18f5b5798b3ba12..ae87d47016cd031dab3fd73d7fc36d9058dcc38b 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -36,7 +36,7 @@ def render("index.json", %{notifications: notifications, for: reading_user} = op
         Map.has_key?(opts, :relationships) ->
           opts[:relationships]
 
-        is_nil(opts[:for]) ->
+        is_nil(reading_user) ->
           UserRelationship.view_relationships_option(nil, [])
 
         true ->
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 82326986ced11047de3a18206d5673af45605ff1..cea76e735b0d3040ac33b63d49f6c3a5804c7f1c 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -72,6 +72,8 @@ defp reblogged?(activity, user) do
   end
 
   def render("index.json", opts) do
+    reading_user = opts[:for]
+
     # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
     activities = Enum.filter(opts.activities, & &1)
     replied_to_activities = get_replied_to_activities(activities)
@@ -82,8 +84,8 @@ def render("index.json", opts) do
       |> Enum.map(&Object.normalize(&1).data["id"])
       |> Activity.create_by_object_ap_id()
       |> Activity.with_preloaded_object(:left)
-      |> Activity.with_preloaded_bookmark(opts[:for])
-      |> Activity.with_set_thread_muted_field(opts[:for])
+      |> Activity.with_preloaded_bookmark(reading_user)
+      |> Activity.with_set_thread_muted_field(reading_user)
       |> Repo.all()
 
     relationships_opt =
@@ -91,13 +93,13 @@ def render("index.json", opts) do
         Map.has_key?(opts, :relationships) ->
           opts[:relationships]
 
-        is_nil(opts[:for]) ->
+        is_nil(reading_user) ->
           UserRelationship.view_relationships_option(nil, [])
 
         true ->
           actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"]))
 
-          UserRelationship.view_relationships_option(opts[:for], actors)
+          UserRelationship.view_relationships_option(reading_user, actors)
       end
 
     opts =
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index 30838b1eb76d467845144f1437c49a42eb38a6ea..f9a5ddcc00e79814e5931289bb5ac8615acca3ac 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -75,7 +75,8 @@ def raw_nodeinfo do
         end,
         if Config.get([:instance, :safe_dm_mentions]) do
           "safe_dm_mentions"
-        end
+        end,
+        "pleroma_emoji_reactions"
       ]
       |> Enum.filter(& &1)
 
diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex
index 8ecf901f3085112f008a8c3142c7d61637f241e4..1023f16d4911cb0fc3c4b8334d1b6cf9cd742300 100644
--- a/lib/pleroma/web/oauth/scopes.ex
+++ b/lib/pleroma/web/oauth/scopes.ex
@@ -15,7 +15,12 @@ defmodule Pleroma.Web.OAuth.Scopes do
   Note: `scopes` is used by Mastodon — supporting it but sticking to
   OAuth's standard `scope` wherever we control it
   """
-  @spec fetch_scopes(map(), list()) :: list()
+  @spec fetch_scopes(map() | struct(), list()) :: list()
+
+  def fetch_scopes(%Pleroma.Web.ApiSpec.Schemas.AppCreateRequest{scopes: scopes}, default) do
+    parse_scopes(scopes, default)
+  end
+
   def fetch_scopes(params, default) do
     parse_scopes(params["scope"] || params["scopes"], default)
   end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 5a09027391f6ab01b62553e192c8b1a1dbf627b8..5f5ec1c81c3673b5074fd93fe086341004e9fe1b 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -29,6 +29,7 @@ defmodule Pleroma.Web.Router do
     plug(Pleroma.Plugs.SetUserSessionIdPlug)
     plug(Pleroma.Plugs.EnsureUserKeyPlug)
     plug(Pleroma.Plugs.IdempotencyPlug)
+    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
   pipeline :authenticated_api do
@@ -44,6 +45,7 @@ defmodule Pleroma.Web.Router do
     plug(Pleroma.Plugs.SetUserSessionIdPlug)
     plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
     plug(Pleroma.Plugs.IdempotencyPlug)
+    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
   pipeline :admin_api do
@@ -61,6 +63,7 @@ defmodule Pleroma.Web.Router do
     plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
     plug(Pleroma.Plugs.UserIsAdminPlug)
     plug(Pleroma.Plugs.IdempotencyPlug)
+    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
   pipeline :mastodon_html do
@@ -94,10 +97,12 @@ defmodule Pleroma.Web.Router do
 
   pipeline :config do
     plug(:accepts, ["json", "xml"])
+    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
   pipeline :pleroma_api do
     plug(:accepts, ["html", "json"])
+    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
   pipeline :mailbox_preview do
@@ -347,9 +352,11 @@ defmodule Pleroma.Web.Router do
 
     get("/notifications", NotificationController, :index)
     get("/notifications/:id", NotificationController, :show)
+    post("/notifications/:id/dismiss", NotificationController, :dismiss)
     post("/notifications/clear", NotificationController, :clear)
-    post("/notifications/dismiss", NotificationController, :dismiss)
     delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
+    # Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
+    post("/notifications/dismiss", NotificationController, :dismiss)
 
     get("/scheduled_statuses", ScheduledActivityController, :index)
     get("/scheduled_statuses/:id", ScheduledActivityController, :show)
@@ -500,6 +507,12 @@ defmodule Pleroma.Web.Router do
     )
   end
 
+  scope "/api" do
+    pipe_through(:api)
+
+    get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
+  end
+
   scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
     pipe_through(:authenticated_api)
 
diff --git a/mix.exs b/mix.exs
index 375bc67c107b74759df9e464814d9bc7a6c8b12b..c781995e0dc271f41ed153d43fd2efa07b039ac7 100644
--- a/mix.exs
+++ b/mix.exs
@@ -37,12 +37,21 @@ def project do
         pleroma: [
           include_executables_for: [:unix],
           applications: [ex_syslogger: :load, syslog: :load],
-          steps: [:assemble, &copy_files/1, &copy_nginx_config/1]
+          steps: [:assemble, &put_otp_version/1, &copy_files/1, &copy_nginx_config/1]
         ]
       ]
     ]
   end
 
+  def put_otp_version(%{path: target_path} = release) do
+    File.write!(
+      Path.join([target_path, "OTP_VERSION"]),
+      Pleroma.OTPVersion.version()
+    )
+
+    release
+  end
+
   def copy_files(%{path: target_path} = release) do
     File.cp_r!("./rel/files", target_path)
     release
@@ -179,7 +188,8 @@ defp deps do
        git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
        ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
       {:mox, "~> 0.5", only: :test},
-      {:restarter, path: "./restarter"}
+      {:restarter, path: "./restarter"},
+      {:open_api_spex, "~> 3.6"}
     ] ++ oauth_deps()
   end
 
diff --git a/mix.lock b/mix.lock
index 50be45a4d8f5866cab5f10c240617744faa4bc8b..ba4e3ac4422923362e7ece777c9a570ff185380d 100644
--- a/mix.lock
+++ b/mix.lock
@@ -74,6 +74,7 @@
   "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
   "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
   "oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"},
+  "open_api_spex": {:hex, :open_api_spex, "3.6.0", "64205aba9f2607f71b08fd43e3351b9c5e9898ec5ef49fc0ae35890da502ade9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "126ba3473966277132079cb1d5bf1e3df9e36fe2acd00166e75fd125cecb59c5"},
   "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
   "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm", "595d09db74cb093b1903381c9de423276a931a2480a46a1a5dc7f932a2a6375b"},
   "phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"},
diff --git a/test/fixtures/emoji/packs/blank.png.zip b/test/fixtures/emoji/packs/blank.png.zip
new file mode 100644
index 0000000000000000000000000000000000000000..651daf1271fb95ca1404142441360eed4fbdd45d
Binary files /dev/null and b/test/fixtures/emoji/packs/blank.png.zip differ
diff --git a/test/fixtures/emoji/packs/default-manifest.json b/test/fixtures/emoji/packs/default-manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..c8433808d0fd43080989e06d33ee5dc8cb91b867
--- /dev/null
+++ b/test/fixtures/emoji/packs/default-manifest.json
@@ -0,0 +1,10 @@
+{
+  "finmoji": {
+    "license": "CC BY-NC-ND 4.0",
+    "homepage": "https://finland.fi/emoji/",
+    "description": "Finland is the first country in the world to publish its own set of country themed emojis. The Finland emoji collection contains 56 tongue-in-cheek emotions, which were created to explain some hard-to-describe Finnish emotions, Finnish words and customs.",
+    "src": "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip",
+    "src_sha256": "384025A1AC6314473863A11AC7AB38A12C01B851A3F82359B89B4D4211D3291D",
+    "files": "finmoji.json"
+  }
+}
\ No newline at end of file
diff --git a/test/fixtures/emoji/packs/finmoji.json b/test/fixtures/emoji/packs/finmoji.json
new file mode 100644
index 0000000000000000000000000000000000000000..27977099870f164c95b54cc2693135dab0ace02a
--- /dev/null
+++ b/test/fixtures/emoji/packs/finmoji.json
@@ -0,0 +1,3 @@
+{
+  "blank": "blank.png"
+}
\ No newline at end of file
diff --git a/test/fixtures/emoji/packs/manifest.json b/test/fixtures/emoji/packs/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..2d51a459b694886ef8e72d1154392db8029dc4e7
--- /dev/null
+++ b/test/fixtures/emoji/packs/manifest.json
@@ -0,0 +1,10 @@
+{
+  "blobs.gg": {
+    "src_sha256": "3a12f3a181678d5b3584a62095411b0d60a335118135910d879920f8ade5a57f",
+    "src": "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip",
+    "license": "Apache 2.0",
+    "homepage": "https://blobs.gg",
+    "files": "blobs_gg.json",
+    "description": "Blob Emoji from blobs.gg repacked as apng"
+  }
+}
\ No newline at end of file
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index cf8441cf68906e7919ab4f8d5eea8adb6d05f6f2..93fd8eab78eefdbb57e00b324941b56db4b1f458 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -150,13 +150,13 @@ test "gives a replacement for user links, using local nicknames in user links te
       assert length(mentions) == 3
 
       expected_text =
-        ~s(<span class="h-card"><a data-user="#{gsimg.id}" class="u-url mention" href="#{
+        ~s(<span class="h-card"><a class="u-url mention" data-user="#{gsimg.id}" href="#{
           gsimg.ap_id
-        }" rel="ugc">@<span>gsimg</span></a></span> According to <span class="h-card"><a data-user="#{
+        }" rel="ugc">@<span>gsimg</span></a></span> According to <span class="h-card"><a class="u-url mention" data-user="#{
           archaeme.id
-        }" class="u-url mention" href="#{"https://archeme/@archa_eme_"}" rel="ugc">@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class="h-card"><a data-user="#{
+        }" href="#{"https://archeme/@archa_eme_"}" rel="ugc">@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class="h-card"><a class="u-url mention" data-user="#{
           archaeme_remote.id
-        }" class="u-url mention" href="#{archaeme_remote.ap_id}" rel="ugc">@<span>archaeme</span></a></span>)
+        }" href="#{archaeme_remote.ap_id}" rel="ugc">@<span>archaeme</span></a></span>)
 
       assert expected_text == text
     end
@@ -171,7 +171,7 @@ test "gives a replacement for user links when the user is using Osada" do
       assert length(mentions) == 1
 
       expected_text =
-        ~s(<span class="h-card"><a data-user="#{mike.id}" class="u-url mention" href="#{
+        ~s(<span class="h-card"><a class="u-url mention" data-user="#{mike.id}" href="#{
           mike.ap_id
         }" rel="ugc">@<span>mike</span></a></span> test)
 
@@ -187,7 +187,7 @@ test "gives a replacement for single-character local nicknames" do
       assert length(mentions) == 1
 
       expected_text =
-        ~s(<span class="h-card"><a data-user="#{o.id}" class="u-url mention" href="#{o.ap_id}" rel="ugc">@<span>o</span></a></span> hi)
+        ~s(<span class="h-card"><a class="u-url mention" data-user="#{o.id}" href="#{o.ap_id}" rel="ugc">@<span>o</span></a></span> hi)
 
       assert expected_text == text
     end
@@ -209,17 +209,13 @@ test "given the 'safe_mention' option, it will only mention people in the beginn
       assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}]
 
       assert expected_text ==
-               ~s(<span class="h-card"><a data-user="#{user.id}" class="u-url mention" href="#{
+               ~s(<span class="h-card"><a class="u-url mention" data-user="#{user.id}" href="#{
                  user.ap_id
-               }" rel="ugc">@<span>#{user.nickname}</span></a></span> <span class="h-card"><a data-user="#{
+               }" rel="ugc">@<span>#{user.nickname}</span></a></span> <span class="h-card"><a class="u-url mention" data-user="#{
                  other_user.id
-               }" class="u-url mention" href="#{other_user.ap_id}" rel="ugc">@<span>#{
-                 other_user.nickname
-               }</span></a></span> hey dudes i hate <span class="h-card"><a data-user="#{
+               }" href="#{other_user.ap_id}" rel="ugc">@<span>#{other_user.nickname}</span></a></span> hey dudes i hate <span class="h-card"><a class="u-url mention" data-user="#{
                  third_user.id
-               }" class="u-url mention" href="#{third_user.ap_id}" rel="ugc">@<span>#{
-                 third_user.nickname
-               }</span></a></span>)
+               }" href="#{third_user.ap_id}" rel="ugc">@<span>#{third_user.nickname}</span></a></span>)
     end
 
     test "given the 'safe_mention' option, it will still work without any mention" do
diff --git a/test/notification_test.exs b/test/notification_test.exs
index 7cfa40c5178d5e4798b4628ac25ebb83493b4de6..837a9dacd17a9ea854878580817ebd4a39515430 100644
--- a/test/notification_test.exs
+++ b/test/notification_test.exs
@@ -537,7 +537,7 @@ test "it does not send notification to mentioned users in likes" do
           "status" => "hey @#{other_user.nickname}!"
         })
 
-      {:ok, activity_two, _} = CommonAPI.favorite(activity_one.id, third_user)
+      {:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id)
 
       {enabled_receivers, _disabled_receivers} =
         Notification.get_notified_from_activity(activity_two)
@@ -620,7 +620,7 @@ test "liking an activity results in 1 notification, then 0 if the activity is de
 
       assert Enum.empty?(Notification.for_user(user))
 
-      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+      {:ok, _} = CommonAPI.favorite(other_user, activity.id)
 
       assert length(Notification.for_user(user)) == 1
 
@@ -637,7 +637,7 @@ test "liking an activity results in 1 notification, then 0 if the activity is un
 
       assert Enum.empty?(Notification.for_user(user))
 
-      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+      {:ok, _} = CommonAPI.favorite(other_user, activity.id)
 
       assert length(Notification.for_user(user)) == 1
 
@@ -692,7 +692,7 @@ test "liking an activity which is already deleted does not generate a notificati
 
       assert Enum.empty?(Notification.for_user(user))
 
-      {:error, _} = CommonAPI.favorite(activity.id, other_user)
+      {:error, :not_found} = CommonAPI.favorite(other_user, activity.id)
 
       assert Enum.empty?(Notification.for_user(user))
     end
diff --git a/test/object_test.exs b/test/object_test.exs
index fe583decd7ed1d591cc8975af6d95619b46d015e..198d3b1cf1ac969016e0ca5effc2f0000ee9210b 100644
--- a/test/object_test.exs
+++ b/test/object_test.exs
@@ -380,7 +380,8 @@ test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
 
       user = insert(:user)
       activity = Activity.get_create_by_object_ap_id(object.data["id"])
-      {:ok, _activity, object} = CommonAPI.favorite(activity.id, user)
+      {:ok, activity} = CommonAPI.favorite(user, activity.id)
+      object = Object.get_by_ap_id(activity.data["object"])
 
       assert object.data["like_count"] == 1
 
diff --git a/test/stat_test.exs b/test/stat_test.exs
index 33b77e7e72559454f89dbf019f1121aa64a48486..bccc1c8d07a435bb5d2df6c07d4507a2deb80f5c 100644
--- a/test/stat_test.exs
+++ b/test/stat_test.exs
@@ -60,7 +60,7 @@ test "doesn't count unrelated activities" do
       other_user = insert(:user)
       {:ok, activity} = CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"})
       _ = CommonAPI.follow(user, other_user)
-      CommonAPI.favorite(activity.id, other_user)
+      CommonAPI.favorite(other_user, activity.id)
       CommonAPI.repeat(activity.id, other_user)
 
       assert %{direct: 0, private: 0, public: 1, unlisted: 0} =
diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs
index ed1c31d9c47b9678463c8532624c8ee47d084ca3..7b05993d318ff19be76206ef89263b60e778cdf2 100644
--- a/test/tasks/database_test.exs
+++ b/test/tasks/database_test.exs
@@ -102,7 +102,7 @@ test "it turns OrderedCollection likes into empty arrays" do
       {:ok, %{id: id, object: object}} = CommonAPI.post(user, %{"status" => "test"})
       {:ok, %{object: object2}} = CommonAPI.post(user, %{"status" => "test test"})
 
-      CommonAPI.favorite(id, user2)
+      CommonAPI.favorite(user2, id)
 
       likes = %{
         "first" =>
diff --git a/test/tasks/emoji_test.exs b/test/tasks/emoji_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..f5de3ef0e1d4302a40503f0c4b1a28c8fcfe4a8c
--- /dev/null
+++ b/test/tasks/emoji_test.exs
@@ -0,0 +1,226 @@
+defmodule Mix.Tasks.Pleroma.EmojiTest do
+  use ExUnit.Case, async: true
+
+  import ExUnit.CaptureIO
+  import Tesla.Mock
+
+  alias Mix.Tasks.Pleroma.Emoji
+
+  describe "ls-packs" do
+    test "with default manifest as url" do
+      mock(fn
+        %{
+          method: :get,
+          url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"
+        } ->
+          %Tesla.Env{
+            status: 200,
+            body: File.read!("test/fixtures/emoji/packs/default-manifest.json")
+          }
+      end)
+
+      capture_io(fn -> Emoji.run(["ls-packs"]) end) =~
+        "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip"
+    end
+
+    test "with passed manifest as file" do
+      capture_io(fn ->
+        Emoji.run(["ls-packs", "-m", "test/fixtures/emoji/packs/manifest.json"])
+      end) =~ "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip"
+    end
+  end
+
+  describe "get-packs" do
+    test "download pack from default manifest" do
+      mock(fn
+        %{
+          method: :get,
+          url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"
+        } ->
+          %Tesla.Env{
+            status: 200,
+            body: File.read!("test/fixtures/emoji/packs/default-manifest.json")
+          }
+
+        %{
+          method: :get,
+          url: "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip"
+        } ->
+          %Tesla.Env{
+            status: 200,
+            body: File.read!("test/fixtures/emoji/packs/blank.png.zip")
+          }
+
+        %{
+          method: :get,
+          url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/finmoji.json"
+        } ->
+          %Tesla.Env{
+            status: 200,
+            body: File.read!("test/fixtures/emoji/packs/finmoji.json")
+          }
+      end)
+
+      assert capture_io(fn -> Emoji.run(["get-packs", "finmoji"]) end) =~ "Writing pack.json for"
+
+      emoji_path =
+        Path.join(
+          Pleroma.Config.get!([:instance, :static_dir]),
+          "emoji"
+        )
+
+      assert File.exists?(Path.join([emoji_path, "finmoji", "pack.json"]))
+      on_exit(fn -> File.rm_rf!("test/instance_static/emoji/finmoji") end)
+    end
+
+    test "pack not found" do
+      mock(fn
+        %{
+          method: :get,
+          url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"
+        } ->
+          %Tesla.Env{
+            status: 200,
+            body: File.read!("test/fixtures/emoji/packs/default-manifest.json")
+          }
+      end)
+
+      assert capture_io(fn -> Emoji.run(["get-packs", "not_found"]) end) =~
+               "No pack named \"not_found\" found"
+    end
+
+    test "raise on bad sha256" do
+      mock(fn
+        %{
+          method: :get,
+          url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip"
+        } ->
+          %Tesla.Env{
+            status: 200,
+            body: File.read!("test/fixtures/emoji/packs/blank.png.zip")
+          }
+      end)
+
+      assert_raise RuntimeError, ~r/^Bad SHA256 for blobs.gg/, fn ->
+        capture_io(fn ->
+          Emoji.run(["get-packs", "blobs.gg", "-m", "test/fixtures/emoji/packs/manifest.json"])
+        end)
+      end
+    end
+  end
+
+  describe "gen-pack" do
+    setup do
+      url = "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip"
+
+      mock(fn %{
+                method: :get,
+                url: ^url
+              } ->
+        %Tesla.Env{status: 200, body: File.read!("test/fixtures/emoji/packs/blank.png.zip")}
+      end)
+
+      {:ok, url: url}
+    end
+
+    test "with default extensions", %{url: url} do
+      name = "pack1"
+      pack_json = "#{name}.json"
+      files_json = "#{name}_file.json"
+      refute File.exists?(pack_json)
+      refute File.exists?(files_json)
+
+      captured =
+        capture_io(fn ->
+          Emoji.run([
+            "gen-pack",
+            url,
+            "--name",
+            name,
+            "--license",
+            "license",
+            "--homepage",
+            "homepage",
+            "--description",
+            "description",
+            "--files",
+            files_json,
+            "--extensions",
+            ".png .gif"
+          ])
+        end)
+
+      assert captured =~ "#{pack_json} has been created with the pack1 pack"
+      assert captured =~ "Using .png .gif extensions"
+
+      assert File.exists?(pack_json)
+      assert File.exists?(files_json)
+
+      on_exit(fn ->
+        File.rm!(pack_json)
+        File.rm!(files_json)
+      end)
+    end
+
+    test "with custom extensions and update existing files", %{url: url} do
+      name = "pack2"
+      pack_json = "#{name}.json"
+      files_json = "#{name}_file.json"
+      refute File.exists?(pack_json)
+      refute File.exists?(files_json)
+
+      captured =
+        capture_io(fn ->
+          Emoji.run([
+            "gen-pack",
+            url,
+            "--name",
+            name,
+            "--license",
+            "license",
+            "--homepage",
+            "homepage",
+            "--description",
+            "description",
+            "--files",
+            files_json,
+            "--extensions",
+            " .png   .gif    .jpeg "
+          ])
+        end)
+
+      assert captured =~ "#{pack_json} has been created with the pack2 pack"
+      assert captured =~ "Using .png .gif .jpeg extensions"
+
+      assert File.exists?(pack_json)
+      assert File.exists?(files_json)
+
+      captured =
+        capture_io(fn ->
+          Emoji.run([
+            "gen-pack",
+            url,
+            "--name",
+            name,
+            "--license",
+            "license",
+            "--homepage",
+            "homepage",
+            "--description",
+            "description",
+            "--files",
+            files_json,
+            "--extensions",
+            " .png   .gif    .jpeg "
+          ])
+        end)
+
+      assert captured =~ "#{pack_json} has been updated with the pack2 pack"
+
+      on_exit(fn ->
+        File.rm!(pack_json)
+        File.rm!(files_json)
+      end)
+    end
+  end
+end
diff --git a/test/user_test.exs b/test/user_test.exs
index 8055ebd08ebb24edffd582f64802b043cd2a5fda..d39787f35d5d00480b786e6619652295f610ee6e 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1141,8 +1141,8 @@ test "it deletes a user, all follow relationships and all activities", %{user: u
       object_two = insert(:note, user: follower)
       activity_two = insert(:note_activity, user: follower, note: object_two)
 
-      {:ok, like, _} = CommonAPI.favorite(activity_two.id, user)
-      {:ok, like_two, _} = CommonAPI.favorite(activity.id, follower)
+      {:ok, like} = CommonAPI.favorite(user, activity_two.id)
+      {:ok, like_two} = CommonAPI.favorite(follower, activity.id)
       {:ok, repeat, _} = CommonAPI.repeat(activity_two.id, user)
 
       {:ok, job} = User.delete(user)
@@ -1404,7 +1404,7 @@ test "preserves hosts in user links text" do
       bio = "A.k.a. @nick@domain.com"
 
       expected_text =
-        ~s(A.k.a. <span class="h-card"><a data-user="#{remote_user.id}" class="u-url mention" href="#{
+        ~s(A.k.a. <span class="h-card"><a class="u-url mention" data-user="#{remote_user.id}" href="#{
           remote_user.ap_id
         }" rel="ugc">@<span>nick@domain.com</span></a></span>)
 
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index 049b14498ec6bd69c40e50160451bc96b9aaf249..17e7b97deffcbfe93c7eeeabd7550e4dea6c1e2b 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -1900,14 +1900,14 @@ test "returns a favourite activities sorted by adds to favorite" do
       {:ok, a4} = CommonAPI.post(user2, %{"status" => "Agent Smith "})
       {:ok, a5} = CommonAPI.post(user1, %{"status" => "Red or Blue "})
 
-      {:ok, _, _} = CommonAPI.favorite(a4.id, user)
-      {:ok, _, _} = CommonAPI.favorite(a3.id, other_user)
-      {:ok, _, _} = CommonAPI.favorite(a3.id, user)
-      {:ok, _, _} = CommonAPI.favorite(a5.id, other_user)
-      {:ok, _, _} = CommonAPI.favorite(a5.id, user)
-      {:ok, _, _} = CommonAPI.favorite(a4.id, other_user)
-      {:ok, _, _} = CommonAPI.favorite(a1.id, user)
-      {:ok, _, _} = CommonAPI.favorite(a1.id, other_user)
+      {:ok, _} = CommonAPI.favorite(user, a4.id)
+      {:ok, _} = CommonAPI.favorite(other_user, a3.id)
+      {:ok, _} = CommonAPI.favorite(user, a3.id)
+      {:ok, _} = CommonAPI.favorite(other_user, a5.id)
+      {:ok, _} = CommonAPI.favorite(user, a5.id)
+      {:ok, _} = CommonAPI.favorite(other_user, a4.id)
+      {:ok, _} = CommonAPI.favorite(user, a1.id)
+      {:ok, _} = CommonAPI.favorite(other_user, a1.id)
       result = ActivityPub.fetch_favourites(user)
 
       assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id]
diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..3c5c3696e299c14e08febf8d52caf83f26ca58b6
--- /dev/null
+++ b/test/web/activity_pub/object_validator_test.exs
@@ -0,0 +1,83 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Web.ActivityPub.ObjectValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+  alias Pleroma.Web.ActivityPub.Utils
+  alias Pleroma.Web.CommonAPI
+
+  import Pleroma.Factory
+
+  describe "likes" do
+    setup do
+      user = insert(:user)
+      {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
+
+      valid_like = %{
+        "to" => [user.ap_id],
+        "cc" => [],
+        "type" => "Like",
+        "id" => Utils.generate_activity_id(),
+        "object" => post_activity.data["object"],
+        "actor" => user.ap_id,
+        "context" => "a context"
+      }
+
+      %{valid_like: valid_like, user: user, post_activity: post_activity}
+    end
+
+    test "returns ok when called in the ObjectValidator", %{valid_like: valid_like} do
+      {:ok, object, _meta} = ObjectValidator.validate(valid_like, [])
+
+      assert "id" in Map.keys(object)
+    end
+
+    test "is valid for a valid object", %{valid_like: valid_like} do
+      assert LikeValidator.cast_and_validate(valid_like).valid?
+    end
+
+    test "it errors when the actor is missing or not known", %{valid_like: valid_like} do
+      without_actor = Map.delete(valid_like, "actor")
+
+      refute LikeValidator.cast_and_validate(without_actor).valid?
+
+      with_invalid_actor = Map.put(valid_like, "actor", "invalidactor")
+
+      refute LikeValidator.cast_and_validate(with_invalid_actor).valid?
+    end
+
+    test "it errors when the object is missing or not known", %{valid_like: valid_like} do
+      without_object = Map.delete(valid_like, "object")
+
+      refute LikeValidator.cast_and_validate(without_object).valid?
+
+      with_invalid_object = Map.put(valid_like, "object", "invalidobject")
+
+      refute LikeValidator.cast_and_validate(with_invalid_object).valid?
+    end
+
+    test "it errors when the actor has already like the object", %{
+      valid_like: valid_like,
+      user: user,
+      post_activity: post_activity
+    } do
+      _like = CommonAPI.favorite(user, post_activity.id)
+
+      refute LikeValidator.cast_and_validate(valid_like).valid?
+    end
+
+    test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do
+      wrapped_like =
+        valid_like
+        |> Map.put("actor", %{"id" => valid_like["actor"]})
+        |> Map.put("object", %{"id" => valid_like["object"]})
+
+      validated = LikeValidator.cast_and_validate(wrapped_like)
+
+      assert validated.valid?
+
+      assert {:actor, valid_like["actor"]} in validated.changes
+      assert {:object, valid_like["object"]} in validated.changes
+    end
+  end
+end
diff --git a/test/web/activity_pub/object_validators/note_validator_test.exs b/test/web/activity_pub/object_validators/note_validator_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..30c481ffbc8329fd74cafedf634ffd345ad4dcdf
--- /dev/null
+++ b/test/web/activity_pub/object_validators/note_validator_test.exs
@@ -0,0 +1,35 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidatorTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator
+  alias Pleroma.Web.ActivityPub.Utils
+
+  import Pleroma.Factory
+
+  describe "Notes" do
+    setup do
+      user = insert(:user)
+
+      note = %{
+        "id" => Utils.generate_activity_id(),
+        "type" => "Note",
+        "actor" => user.ap_id,
+        "to" => [user.follower_address],
+        "cc" => [],
+        "content" => "Hellow this is content.",
+        "context" => "xxx",
+        "summary" => "a post"
+      }
+
+      %{user: user, note: note}
+    end
+
+    test "a basic note validates", %{note: note} do
+      %{valid?: true} = NoteValidator.cast_and_validate(note)
+    end
+  end
+end
diff --git a/test/web/activity_pub/object_validators/types/date_time_test.exs b/test/web/activity_pub/object_validators/types/date_time_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..3e17a94974310fcfe791db716163686aa836dd74
--- /dev/null
+++ b/test/web/activity_pub/object_validators/types/date_time_test.exs
@@ -0,0 +1,32 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTimeTest do
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTime
+  use Pleroma.DataCase
+
+  test "it validates an xsd:Datetime" do
+    valid_strings = [
+      "2004-04-12T13:20:00",
+      "2004-04-12T13:20:15.5",
+      "2004-04-12T13:20:00-05:00",
+      "2004-04-12T13:20:00Z"
+    ]
+
+    invalid_strings = [
+      "2004-04-12T13:00",
+      "2004-04-1213:20:00",
+      "99-04-12T13:00",
+      "2004-04-12"
+    ]
+
+    assert {:ok, "2004-04-01T12:00:00Z"} == DateTime.cast("2004-04-01T12:00:00Z")
+
+    Enum.each(valid_strings, fn date_time ->
+      result = DateTime.cast(date_time)
+      assert {:ok, _} = result
+    end)
+
+    Enum.each(invalid_strings, fn date_time ->
+      result = DateTime.cast(date_time)
+      assert :error == result
+    end)
+  end
+end
diff --git a/test/web/activity_pub/object_validators/types/object_id_test.exs b/test/web/activity_pub/object_validators/types/object_id_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..8342131828b8c76441ba05c44ea0801d64e22b99
--- /dev/null
+++ b/test/web/activity_pub/object_validators/types/object_id_test.exs
@@ -0,0 +1,37 @@
+defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
+  use Pleroma.DataCase
+
+  @uris [
+    "http://lain.com/users/lain",
+    "http://lain.com",
+    "https://lain.com/object/1"
+  ]
+
+  @non_uris [
+    "https://",
+    "rin",
+    1,
+    :x,
+    %{"1" => 2}
+  ]
+
+  test "it accepts http uris" do
+    Enum.each(@uris, fn uri ->
+      assert {:ok, uri} == ObjectID.cast(uri)
+    end)
+  end
+
+  test "it accepts an object with a nested uri id" do
+    Enum.each(@uris, fn uri ->
+      assert {:ok, uri} == ObjectID.cast(%{"id" => uri})
+    end)
+  end
+
+  test "it rejects non-uri strings" do
+    Enum.each(@non_uris, fn non_uri ->
+      assert :error == ObjectID.cast(non_uri)
+      assert :error == ObjectID.cast(%{"id" => non_uri})
+    end)
+  end
+end
diff --git a/test/web/activity_pub/pipeline_test.exs b/test/web/activity_pub/pipeline_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..f3c43749889928b41cf515b45d8960248d0e8b50
--- /dev/null
+++ b/test/web/activity_pub/pipeline_test.exs
@@ -0,0 +1,87 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.PipelineTest do
+  use Pleroma.DataCase
+
+  import Mock
+  import Pleroma.Factory
+
+  describe "common_pipeline/2" do
+    test "it goes through validation, filtering, persisting, side effects and federation for local activities" do
+      activity = insert(:note_activity)
+      meta = [local: true]
+
+      with_mocks([
+        {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]},
+        {
+          Pleroma.Web.ActivityPub.MRF,
+          [],
+          [filter: fn o -> {:ok, o} end]
+        },
+        {
+          Pleroma.Web.ActivityPub.ActivityPub,
+          [],
+          [persist: fn o, m -> {:ok, o, m} end]
+        },
+        {
+          Pleroma.Web.ActivityPub.SideEffects,
+          [],
+          [handle: fn o, m -> {:ok, o, m} end]
+        },
+        {
+          Pleroma.Web.Federator,
+          [],
+          [publish: fn _o -> :ok end]
+        }
+      ]) do
+        assert {:ok, ^activity, ^meta} =
+                 Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
+
+        assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
+        assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity))
+        assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
+        assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
+        assert_called(Pleroma.Web.Federator.publish(activity))
+      end
+    end
+
+    test "it goes through validation, filtering, persisting, side effects without federation for remote activities" do
+      activity = insert(:note_activity)
+      meta = [local: false]
+
+      with_mocks([
+        {Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]},
+        {
+          Pleroma.Web.ActivityPub.MRF,
+          [],
+          [filter: fn o -> {:ok, o} end]
+        },
+        {
+          Pleroma.Web.ActivityPub.ActivityPub,
+          [],
+          [persist: fn o, m -> {:ok, o, m} end]
+        },
+        {
+          Pleroma.Web.ActivityPub.SideEffects,
+          [],
+          [handle: fn o, m -> {:ok, o, m} end]
+        },
+        {
+          Pleroma.Web.Federator,
+          [],
+          []
+        }
+      ]) do
+        assert {:ok, ^activity, ^meta} =
+                 Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
+
+        assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
+        assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity))
+        assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
+        assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
+      end
+    end
+  end
+end
diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..b67bd14b36e08f07c57629aefa378a7991acdcb7
--- /dev/null
+++ b/test/web/activity_pub/side_effects_test.exs
@@ -0,0 +1,34 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.ActivityPub.Builder
+  alias Pleroma.Web.ActivityPub.SideEffects
+  alias Pleroma.Web.CommonAPI
+
+  import Pleroma.Factory
+
+  describe "like objects" do
+    setup do
+      user = insert(:user)
+      {:ok, post} = CommonAPI.post(user, %{"status" => "hey"})
+
+      {:ok, like_data, _meta} = Builder.like(user, post.object)
+      {:ok, like, _meta} = ActivityPub.persist(like_data, local: true)
+
+      %{like: like, user: user}
+    end
+
+    test "add the like to the original object", %{like: like, user: user} do
+      {:ok, like, _} = SideEffects.handle(like)
+      object = Object.get_by_ap_id(like.data["object"])
+      assert object.data["like_count"] == 1
+      assert user.ap_id in object.data["likes"]
+    end
+  end
+end
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index b2cabbd300745ff5b1dce17f980c7624d4213947..6dfd823f757389f325aa25fec9cef029691f5b4e 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -334,7 +334,9 @@ test "it works for incoming likes" do
         |> Poison.decode!()
         |> Map.put("object", activity.data["object"])
 
-      {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+      {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
+
+      refute Enum.empty?(activity.recipients)
 
       assert data["actor"] == "http://mastodon.example.org/users/admin"
       assert data["type"] == "Like"
diff --git a/test/web/activity_pub/views/object_view_test.exs b/test/web/activity_pub/views/object_view_test.exs
index de5ffc5b3bae5831cde19f897bf7602f253a879b..6c006206bbad7094869ffb13878690f5381b5657 100644
--- a/test/web/activity_pub/views/object_view_test.exs
+++ b/test/web/activity_pub/views/object_view_test.exs
@@ -59,7 +59,7 @@ test "renders a like activity" do
     object = Object.normalize(note)
     user = insert(:user)
 
-    {:ok, like_activity, _} = CommonAPI.favorite(note.id, user)
+    {:ok, like_activity} = CommonAPI.favorite(user, note.id)
 
     result = ObjectView.render("object.json", %{object: like_activity})
 
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index fe8a086d8c8449a20eda629d257210bd618b3f35..f02f6ae7afedeeb5244d02b3708f0191a591913e 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -625,6 +625,39 @@ test "it returns 403 if requested by a non-admin" do
 
       assert json_response(conn, :forbidden)
     end
+
+    test "email with +", %{conn: conn, admin: admin} do
+      recipient_email = "foo+bar@baz.com"
+
+      conn
+      |> put_req_header("content-type", "application/json;charset=utf-8")
+      |> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email})
+      |> json_response(:no_content)
+
+      token_record =
+        Pleroma.UserInviteToken
+        |> Repo.all()
+        |> List.last()
+
+      assert token_record
+      refute token_record.used
+
+      notify_email = Config.get([:instance, :notify_email])
+      instance_name = Config.get([:instance, :name])
+
+      email =
+        Pleroma.Emails.UserEmail.user_invitation_email(
+          admin,
+          token_record,
+          recipient_email
+        )
+
+      Swoosh.TestAssertions.assert_email_sent(
+        from: {instance_name, notify_email},
+        to: recipient_email,
+        html_body: email.html_body
+      )
+    end
   end
 
   describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do
@@ -637,7 +670,8 @@ test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do
 
       conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
 
-      assert json_response(conn, :internal_server_error)
+      assert json_response(conn, :bad_request) ==
+               "To send invites you need to set the `invites_enabled` option to true."
     end
 
     test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do
@@ -646,7 +680,8 @@ test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do
 
       conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
 
-      assert json_response(conn, :internal_server_error)
+      assert json_response(conn, :bad_request) ==
+               "To send invites you need to set the `registrations_open` option to false."
     end
   end
 
diff --git a/test/web/api_spec/app_operation_test.exs b/test/web/api_spec/app_operation_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..5b96abb4452da4fb658edd38aba89e58470d7856
--- /dev/null
+++ b/test/web/api_spec/app_operation_test.exs
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.AppOperationTest do
+  use Pleroma.Web.ConnCase, async: true
+
+  alias Pleroma.Web.ApiSpec
+  alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
+  alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
+
+  import OpenApiSpex.TestAssertions
+  import Pleroma.Factory
+
+  test "AppCreateRequest example matches schema" do
+    api_spec = ApiSpec.spec()
+    schema = AppCreateRequest.schema()
+    assert_schema(schema.example, "AppCreateRequest", api_spec)
+  end
+
+  test "AppCreateResponse example matches schema" do
+    api_spec = ApiSpec.spec()
+    schema = AppCreateResponse.schema()
+    assert_schema(schema.example, "AppCreateResponse", api_spec)
+  end
+
+  test "AppController produces a AppCreateResponse", %{conn: conn} do
+    api_spec = ApiSpec.spec()
+    app_attrs = build(:oauth_app)
+
+    json =
+      conn
+      |> put_req_header("content-type", "application/json")
+      |> post(
+        "/api/v1/apps",
+        Jason.encode!(%{
+          client_name: app_attrs.client_name,
+          redirect_uris: app_attrs.redirect_uris
+        })
+      )
+      |> json_response(200)
+
+    assert_schema(json, "AppCreateResponse", api_spec)
+  end
+end
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index 0da0bd2e2903fc83d2133a9f378ae9eb707aaa3b..f46ad027271f79b252dd23544e03a31caab27978 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -284,9 +284,12 @@ test "favoriting a status" do
       user = insert(:user)
       other_user = insert(:user)
 
-      {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
+      {:ok, post_activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
 
-      {:ok, %Activity{}, _} = CommonAPI.favorite(activity.id, user)
+      {:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id)
+      assert data["type"] == "Like"
+      assert data["actor"] == user.ap_id
+      assert data["object"] == post_activity.data["object"]
     end
 
     test "retweeting a status twice returns the status" do
@@ -298,13 +301,13 @@ test "retweeting a status twice returns the status" do
       {:ok, ^activity, ^object} = CommonAPI.repeat(activity.id, user)
     end
 
-    test "favoriting a status twice returns the status" do
+    test "favoriting a status twice returns ok, but without the like activity" do
       user = insert(:user)
       other_user = insert(:user)
 
       {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
-      {:ok, %Activity{} = activity, object} = CommonAPI.favorite(activity.id, user)
-      {:ok, ^activity, ^object} = CommonAPI.favorite(activity.id, user)
+      {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
+      assert {:ok, :already_liked} = CommonAPI.favorite(user, activity.id)
     end
   end
 
diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs
index d383d1714e172a9ef5125dd320a6d28330134137..98cf02d495b59090454525bd9e02e45288f5516c 100644
--- a/test/web/common_api/common_api_utils_test.exs
+++ b/test/web/common_api/common_api_utils_test.exs
@@ -159,11 +159,11 @@ test "works for text/markdown with mentions" do
       {output, _, _} = Utils.format_input(text, "text/markdown")
 
       assert output ==
-               ~s(<p><strong>hello world</strong></p><p><em>another <span class="h-card"><a data-user="#{
+               ~s(<p><strong>hello world</strong></p><p><em>another <span class="h-card"><a class="u-url mention" data-user="#{
                  user.id
-               }" class="u-url mention" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> and <span class="h-card"><a data-user="#{
+               }" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> and <span class="h-card"><a class="u-url mention" data-user="#{
                  user.id
-               }" class="u-url mention" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> <a href="http://google.com" rel="ugc">google.com</a> paragraph</em></p>)
+               }" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> <a href="http://google.com" rel="ugc">google.com</a> paragraph</em></p>)
     end
   end
 
diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
index b693c1a47603401240fee96c0d7a89191c13aaf8..2d256f63c1f3e5b2284cad219a7f0c181da87f03 100644
--- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
+++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
@@ -82,9 +82,9 @@ test "updates the user's bio", %{conn: conn} do
       assert user_data = json_response(conn, 200)
 
       assert user_data["note"] ==
-               ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a data-user="#{
+               ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a class="u-url mention" data-user="#{
                  user2.id
-               }" class="u-url mention" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span><br/><br/>suya..)
+               }" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span><br/><br/>suya..)
     end
 
     test "updates the user's locking status", %{conn: conn} do
@@ -273,7 +273,7 @@ test "updates profile emojos", %{user: user, conn: conn} do
     test "update fields", %{conn: conn} do
       fields = [
         %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "<script>bar</script>"},
-        %{"name" => "link", "value" => "cofe.io"}
+        %{"name" => "link.io", "value" => "cofe.io"}
       ]
 
       account_data =
@@ -283,7 +283,10 @@ test "update fields", %{conn: conn} do
 
       assert account_data["fields"] == [
                %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"},
-               %{"name" => "link", "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)}
+               %{
+                 "name" => "link.io",
+                 "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)
+               }
              ]
 
       assert account_data["source"]["fields"] == [
@@ -291,14 +294,16 @@ test "update fields", %{conn: conn} do
                  "name" => "<a href=\"http://google.com\">foo</a>",
                  "value" => "<script>bar</script>"
                },
-               %{"name" => "link", "value" => "cofe.io"}
+               %{"name" => "link.io", "value" => "cofe.io"}
              ]
+    end
 
+    test "update fields via x-www-form-urlencoded", %{conn: conn} do
       fields =
         [
           "fields_attributes[1][name]=link",
-          "fields_attributes[1][value]=cofe.io",
-          "fields_attributes[0][name]=<a href=\"http://google.com\">foo</a>",
+          "fields_attributes[1][value]=http://cofe.io",
+          "fields_attributes[0][name]=foo",
           "fields_attributes[0][value]=bar"
         ]
         |> Enum.join("&")
@@ -310,32 +315,49 @@ test "update fields", %{conn: conn} do
         |> json_response(200)
 
       assert account["fields"] == [
-               %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"},
-               %{"name" => "link", "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)}
+               %{"name" => "foo", "value" => "bar"},
+               %{
+                 "name" => "link",
+                 "value" => ~S(<a href="http://cofe.io" rel="ugc">http://cofe.io</a>)
+               }
              ]
 
       assert account["source"]["fields"] == [
-               %{
-                 "name" => "<a href=\"http://google.com\">foo</a>",
-                 "value" => "bar"
-               },
-               %{"name" => "link", "value" => "cofe.io"}
+               %{"name" => "foo", "value" => "bar"},
+               %{"name" => "link", "value" => "http://cofe.io"}
              ]
+    end
 
+    test "update fields with empty name", %{conn: conn} do
+      fields = [
+        %{"name" => "foo", "value" => ""},
+        %{"name" => "", "value" => "bar"}
+      ]
+
+      account =
+        conn
+        |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+        |> json_response(200)
+
+      assert account["fields"] == [
+               %{"name" => "foo", "value" => ""}
+             ]
+    end
+
+    test "update fields when invalid request", %{conn: conn} do
       name_limit = Pleroma.Config.get([:instance, :account_field_name_length])
       value_limit = Pleroma.Config.get([:instance, :account_field_value_length])
 
+      long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join()
       long_value = Enum.map(0..value_limit, fn _ -> "x" end) |> Enum.join()
 
-      fields = [%{"name" => "<b>foo<b>", "value" => long_value}]
+      fields = [%{"name" => "foo", "value" => long_value}]
 
       assert %{"error" => "Invalid request"} ==
                conn
                |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
                |> json_response(403)
 
-      long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join()
-
       fields = [%{"name" => long_name, "value" => "bar"}]
 
       assert %{"error" => "Invalid request"} ==
@@ -346,7 +368,7 @@ test "update fields", %{conn: conn} do
       Pleroma.Config.put([:instance, :max_account_fields], 1)
 
       fields = [
-        %{"name" => "<b>foo<b>", "value" => "<i>bar</i>"},
+        %{"name" => "foo", "value" => "bar"},
         %{"name" => "link", "value" => "cofe.io"}
       ]
 
@@ -354,20 +376,6 @@ test "update fields", %{conn: conn} do
                conn
                |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
                |> json_response(403)
-
-      fields = [
-        %{"name" => "foo", "value" => ""},
-        %{"name" => "", "value" => "bar"}
-      ]
-
-      account =
-        conn
-        |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
-        |> json_response(200)
-
-      assert account["fields"] == [
-               %{"name" => "foo", "value" => ""}
-             ]
     end
   end
 end
diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs
index a9fa0ce48c40f1f6c127aa928676e35f0b862a3b..a450a732c8bcadf12116757318e0bb28196cf35d 100644
--- a/test/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/web/mastodon_api/controllers/account_controller_test.exs
@@ -794,7 +794,9 @@ test "blocking / unblocking a user" do
 
     test "Account registration via Application", %{conn: conn} do
       conn =
-        post(conn, "/api/v1/apps", %{
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/v1/apps", %{
           client_name: "client_name",
           redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
           scopes: "read, write, follow"
diff --git a/test/web/mastodon_api/controllers/app_controller_test.exs b/test/web/mastodon_api/controllers/app_controller_test.exs
index 77d234d67eea6675b3e7e9b90c501f175ccc734f..e7b11d14e1461fa910f72ddd73c8b5359a4f5b21 100644
--- a/test/web/mastodon_api/controllers/app_controller_test.exs
+++ b/test/web/mastodon_api/controllers/app_controller_test.exs
@@ -16,8 +16,7 @@ test "apps/verify_credentials", %{conn: conn} do
 
     conn =
       conn
-      |> assign(:user, token.user)
-      |> assign(:token, token)
+      |> put_req_header("authorization", "Bearer #{token.token}")
       |> get("/api/v1/apps/verify_credentials")
 
     app = Repo.preload(token, :app).app
@@ -37,6 +36,7 @@ test "creates an oauth app", %{conn: conn} do
 
     conn =
       conn
+      |> put_req_header("content-type", "application/json")
       |> assign(:user, user)
       |> post("/api/v1/apps", %{
         client_name: app_attrs.client_name,
diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs
index 23f94e3a609ada410da2636ec0fa51edf0ad2a90..1557937d84314f7abd73c2008e7bdb2900549d29 100644
--- a/test/web/mastodon_api/controllers/notification_controller_test.exs
+++ b/test/web/mastodon_api/controllers/notification_controller_test.exs
@@ -26,7 +26,7 @@ test "list of notifications" do
       |> get("/api/v1/notifications")
 
     expected_response =
-      "hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{
+      "hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{
         user.ap_id
       }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
 
@@ -45,7 +45,7 @@ test "getting a single notification" do
     conn = get(conn, "/api/v1/notifications/#{notification.id}")
 
     expected_response =
-      "hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{
+      "hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{
         user.ap_id
       }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
 
@@ -53,7 +53,7 @@ test "getting a single notification" do
     assert response == expected_response
   end
 
-  test "dismissing a single notification" do
+  test "dismissing a single notification (deprecated endpoint)" do
     %{user: user, conn: conn} = oauth_access(["write:notifications"])
     other_user = insert(:user)
 
@@ -69,6 +69,22 @@ test "dismissing a single notification" do
     assert %{} = json_response(conn, 200)
   end
 
+  test "dismissing a single notification" do
+    %{user: user, conn: conn} = oauth_access(["write:notifications"])
+    other_user = insert(:user)
+
+    {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
+
+    {:ok, [notification]} = Notification.create_notifications(activity)
+
+    conn =
+      conn
+      |> assign(:user, user)
+      |> post("/api/v1/notifications/#{notification.id}/dismiss")
+
+    assert %{} = json_response(conn, 200)
+  end
+
   test "clearing all notifications" do
     %{user: user, conn: conn} = oauth_access(["write:notifications", "read:notifications"])
     other_user = insert(:user)
@@ -194,10 +210,10 @@ test "filters notifications for Like activities" do
       {:ok, private_activity} =
         CommonAPI.post(other_user, %{"status" => ".", "visibility" => "private"})
 
-      {:ok, _, _} = CommonAPI.favorite(public_activity.id, user)
-      {:ok, _, _} = CommonAPI.favorite(direct_activity.id, user)
-      {:ok, _, _} = CommonAPI.favorite(unlisted_activity.id, user)
-      {:ok, _, _} = CommonAPI.favorite(private_activity.id, user)
+      {:ok, _} = CommonAPI.favorite(user, public_activity.id)
+      {:ok, _} = CommonAPI.favorite(user, direct_activity.id)
+      {:ok, _} = CommonAPI.favorite(user, unlisted_activity.id)
+      {:ok, _} = CommonAPI.favorite(user, private_activity.id)
 
       activity_ids =
         conn
@@ -274,7 +290,7 @@ test "filters notifications using exclude_types" do
 
     {:ok, mention_activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"})
     {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
-    {:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, other_user)
+    {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id)
     {:ok, reblog_activity, _} = CommonAPI.repeat(create_activity.id, other_user)
     {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
 
@@ -310,7 +326,7 @@ test "filters notifications using include_types" do
 
     {:ok, mention_activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"})
     {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
-    {:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, other_user)
+    {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id)
     {:ok, reblog_activity, _} = CommonAPI.repeat(create_activity.id, other_user)
     {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
 
diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs
index d59974d50bf479dc236ac2a0d954e917a36b6a86..cd9ca4973e6b97f8c85cec22bc56f8e3565bb2aa 100644
--- a/test/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/web/mastodon_api/controllers/status_controller_test.exs
@@ -775,7 +775,7 @@ test "reblogged status for another user" do
       user1 = insert(:user)
       user2 = insert(:user)
       user3 = insert(:user)
-      CommonAPI.favorite(activity.id, user2)
+      {:ok, _} = CommonAPI.favorite(user2, activity.id)
       {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
       {:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1)
       {:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
@@ -850,11 +850,15 @@ test "favoriting twice will just return 200", %{conn: conn} do
       activity = insert(:note_activity)
 
       post(conn, "/api/v1/statuses/#{activity.id}/favourite")
-      assert post(conn, "/api/v1/statuses/#{activity.id}/favourite") |> json_response(200)
+
+      assert post(conn, "/api/v1/statuses/#{activity.id}/favourite")
+             |> json_response(200)
     end
 
     test "returns 404 error for a wrong id", %{conn: conn} do
-      conn = post(conn, "/api/v1/statuses/1/favourite")
+      conn =
+        conn
+        |> post("/api/v1/statuses/1/favourite")
 
       assert json_response(conn, 404) == %{"error" => "Record not found"}
     end
@@ -866,7 +870,7 @@ test "returns 404 error for a wrong id", %{conn: conn} do
     test "unfavorites a status and returns it", %{user: user, conn: conn} do
       activity = insert(:note_activity)
 
-      {:ok, _, _} = CommonAPI.favorite(activity.id, user)
+      {:ok, _} = CommonAPI.favorite(user, activity.id)
 
       conn = post(conn, "/api/v1/statuses/#{activity.id}/unfavourite")
 
@@ -1176,7 +1180,7 @@ test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{c
 
     test "returns users who have favorited the status", %{conn: conn, activity: activity} do
       other_user = insert(:user)
-      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+      {:ok, _} = CommonAPI.favorite(other_user, activity.id)
 
       response =
         conn
@@ -1207,7 +1211,7 @@ test "does not return users who have favorited the status but are blocked", %{
       other_user = insert(:user)
       {:ok, _user_relationship} = User.block(user, other_user)
 
-      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+      {:ok, _} = CommonAPI.favorite(other_user, activity.id)
 
       response =
         conn
@@ -1219,7 +1223,7 @@ test "does not return users who have favorited the status but are blocked", %{
 
     test "does not fail on an unauthenticated request", %{activity: activity} do
       other_user = insert(:user)
-      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+      {:ok, _} = CommonAPI.favorite(other_user, activity.id)
 
       response =
         build_conn()
@@ -1239,7 +1243,7 @@ test "requires authentication for private posts", %{user: user} do
           "visibility" => "direct"
         })
 
-      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
+      {:ok, _} = CommonAPI.favorite(other_user, activity.id)
 
       favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
 
@@ -1399,7 +1403,7 @@ test "returns the favorites of a user" do
     {:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
     {:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
 
-    {:ok, _, _} = CommonAPI.favorite(activity.id, user)
+    {:ok, _} = CommonAPI.favorite(user, activity.id)
 
     first_conn = get(conn, "/api/v1/favourites")
 
@@ -1416,7 +1420,7 @@ test "returns the favorites of a user" do
           "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
       })
 
-    {:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
+    {:ok, _} = CommonAPI.favorite(user, second_activity.id)
 
     last_like = status["id"]
 
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index 8d00e3c21ba935aaf9b618425e38b0ed0bf1335e..4435f69ff6669faf6289651badcbcb4540ef31f2 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -209,6 +209,9 @@ defp test_relationship_rendering(user, other_user, expected_result) do
       relationships_opt = UserRelationship.view_relationships_option(user, [other_user])
       opts = Map.put(opts, :relationships, relationships_opt)
       assert expected_result == AccountView.render("relationship.json", opts)
+
+      assert [expected_result] ==
+               AccountView.render("relationships.json", %{user: user, targets: [other_user]})
     end
 
     @blank_response %{
diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs
index 81eefd7350d839078ba09793eb127061a3f041b2..c3ec9dfecbcf3f1dc44186748c30097fc2b03c28 100644
--- a/test/web/mastodon_api/views/notification_view_test.exs
+++ b/test/web/mastodon_api/views/notification_view_test.exs
@@ -54,7 +54,7 @@ test "Favourite notification" do
     user = insert(:user)
     another_user = insert(:user)
     {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"})
-    {:ok, favorite_activity, _object} = CommonAPI.favorite(create_activity.id, another_user)
+    {:ok, favorite_activity} = CommonAPI.favorite(another_user, create_activity.id)
     {:ok, [notification]} = Notification.create_notifications(favorite_activity)
     create_activity = Activity.get_by_id(create_activity.id)
 
diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs
index 43f32260616e9fcca5a481f765a3526e88369806..9bcc07b37c50f2a240b28b29bc5e47eb363ebbc4 100644
--- a/test/web/node_info_test.exs
+++ b/test/web/node_info_test.exs
@@ -7,6 +7,8 @@ defmodule Pleroma.Web.NodeInfoTest do
 
   import Pleroma.Factory
 
+  alias Pleroma.Config
+
   setup do: clear_config([:mrf_simple])
   setup do: clear_config(:instance)
 
@@ -47,7 +49,7 @@ test "nodeinfo shows restricted nicknames", %{conn: conn} do
 
     assert result = json_response(conn, 200)
 
-    assert Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) ==
+    assert Config.get([Pleroma.User, :restricted_nicknames]) ==
              result["metadata"]["restrictedNicknames"]
   end
 
@@ -65,10 +67,10 @@ test "returns software.repository field in nodeinfo 2.1", %{conn: conn} do
   end
 
   test "returns fieldsLimits field", %{conn: conn} do
-    Pleroma.Config.put([:instance, :max_account_fields], 10)
-    Pleroma.Config.put([:instance, :max_remote_account_fields], 15)
-    Pleroma.Config.put([:instance, :account_field_name_length], 255)
-    Pleroma.Config.put([:instance, :account_field_value_length], 2048)
+    Config.put([:instance, :max_account_fields], 10)
+    Config.put([:instance, :max_remote_account_fields], 15)
+    Config.put([:instance, :account_field_name_length], 255)
+    Config.put([:instance, :account_field_value_length], 2048)
 
     response =
       conn
@@ -82,8 +84,8 @@ test "returns fieldsLimits field", %{conn: conn} do
   end
 
   test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do
-    option = Pleroma.Config.get([:instance, :safe_dm_mentions])
-    Pleroma.Config.put([:instance, :safe_dm_mentions], true)
+    option = Config.get([:instance, :safe_dm_mentions])
+    Config.put([:instance, :safe_dm_mentions], true)
 
     response =
       conn
@@ -92,7 +94,7 @@ test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do
 
     assert "safe_dm_mentions" in response["metadata"]["features"]
 
-    Pleroma.Config.put([:instance, :safe_dm_mentions], false)
+    Config.put([:instance, :safe_dm_mentions], false)
 
     response =
       conn
@@ -101,14 +103,14 @@ test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do
 
     refute "safe_dm_mentions" in response["metadata"]["features"]
 
-    Pleroma.Config.put([:instance, :safe_dm_mentions], option)
+    Config.put([:instance, :safe_dm_mentions], option)
   end
 
   describe "`metadata/federation/enabled`" do
     setup do: clear_config([:instance, :federating])
 
     test "it shows if federation is enabled/disabled", %{conn: conn} do
-      Pleroma.Config.put([:instance, :federating], true)
+      Config.put([:instance, :federating], true)
 
       response =
         conn
@@ -117,7 +119,7 @@ test "it shows if federation is enabled/disabled", %{conn: conn} do
 
       assert response["metadata"]["federation"]["enabled"] == true
 
-      Pleroma.Config.put([:instance, :federating], false)
+      Config.put([:instance, :federating], false)
 
       response =
         conn
@@ -128,15 +130,39 @@ test "it shows if federation is enabled/disabled", %{conn: conn} do
     end
   end
 
+  test "it shows default features flags", %{conn: conn} do
+    response =
+      conn
+      |> get("/nodeinfo/2.1.json")
+      |> json_response(:ok)
+
+    default_features = [
+      "pleroma_api",
+      "mastodon_api",
+      "mastodon_api_streaming",
+      "polls",
+      "pleroma_explicit_addressing",
+      "shareable_emoji_packs",
+      "multifetch",
+      "pleroma_emoji_reactions",
+      "pleroma:api/v1/notifications:include_types_filter"
+    ]
+
+    assert MapSet.subset?(
+             MapSet.new(default_features),
+             MapSet.new(response["metadata"]["features"])
+           )
+  end
+
   test "it shows MRF transparency data if enabled", %{conn: conn} do
-    config = Pleroma.Config.get([:instance, :rewrite_policy])
-    Pleroma.Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
+    config = Config.get([:instance, :rewrite_policy])
+    Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
 
-    option = Pleroma.Config.get([:instance, :mrf_transparency])
-    Pleroma.Config.put([:instance, :mrf_transparency], true)
+    option = Config.get([:instance, :mrf_transparency])
+    Config.put([:instance, :mrf_transparency], true)
 
     simple_config = %{"reject" => ["example.com"]}
-    Pleroma.Config.put(:mrf_simple, simple_config)
+    Config.put(:mrf_simple, simple_config)
 
     response =
       conn
@@ -145,25 +171,25 @@ test "it shows MRF transparency data if enabled", %{conn: conn} do
 
     assert response["metadata"]["federation"]["mrf_simple"] == simple_config
 
-    Pleroma.Config.put([:instance, :rewrite_policy], config)
-    Pleroma.Config.put([:instance, :mrf_transparency], option)
-    Pleroma.Config.put(:mrf_simple, %{})
+    Config.put([:instance, :rewrite_policy], config)
+    Config.put([:instance, :mrf_transparency], option)
+    Config.put(:mrf_simple, %{})
   end
 
   test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do
-    config = Pleroma.Config.get([:instance, :rewrite_policy])
-    Pleroma.Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
+    config = Config.get([:instance, :rewrite_policy])
+    Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
 
-    option = Pleroma.Config.get([:instance, :mrf_transparency])
-    Pleroma.Config.put([:instance, :mrf_transparency], true)
+    option = Config.get([:instance, :mrf_transparency])
+    Config.put([:instance, :mrf_transparency], true)
 
-    exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
-    Pleroma.Config.put([:instance, :mrf_transparency_exclusions], ["other.site"])
+    exclusions = Config.get([:instance, :mrf_transparency_exclusions])
+    Config.put([:instance, :mrf_transparency_exclusions], ["other.site"])
 
     simple_config = %{"reject" => ["example.com", "other.site"]}
     expected_config = %{"reject" => ["example.com"]}
 
-    Pleroma.Config.put(:mrf_simple, simple_config)
+    Config.put(:mrf_simple, simple_config)
 
     response =
       conn
@@ -173,9 +199,9 @@ test "it performs exclusions from MRF transparency data if configured", %{conn:
     assert response["metadata"]["federation"]["mrf_simple"] == expected_config
     assert response["metadata"]["federation"]["exclusions"] == true
 
-    Pleroma.Config.put([:instance, :rewrite_policy], config)
-    Pleroma.Config.put([:instance, :mrf_transparency], option)
-    Pleroma.Config.put([:instance, :mrf_transparency_exclusions], exclusions)
-    Pleroma.Config.put(:mrf_simple, %{})
+    Config.put([:instance, :rewrite_policy], config)
+    Config.put([:instance, :mrf_transparency], option)
+    Config.put([:instance, :mrf_transparency_exclusions], exclusions)
+    Config.put(:mrf_simple, %{})
   end
 end
diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs
index 6787b414b12d5eda7f202fe42e501dfc897a070b..bb349cb1968216ed136d6f512cf21fd785f67abc 100644
--- a/test/web/ostatus/ostatus_controller_test.exs
+++ b/test/web/ostatus/ostatus_controller_test.exs
@@ -136,7 +136,7 @@ test "render html for redirect for html format", %{conn: conn} do
 
       user = insert(:user)
 
-      {:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user)
+      {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
 
       assert like_activity.data["type"] == "Like"
 
diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs
index 2aa87ac30599724cda63926b5da0de2bd3f11e65..ae5334015aa56e029203a922328236a6050b9d10 100644
--- a/test/web/pleroma_api/controllers/account_controller_test.exs
+++ b/test/web/pleroma_api/controllers/account_controller_test.exs
@@ -138,7 +138,7 @@ test "returns list of statuses favorited by specified user", %{
       user: user
     } do
       [activity | _] = insert_pair(:note_activity)
-      CommonAPI.favorite(activity.id, user)
+      CommonAPI.favorite(user, activity.id)
 
       response =
         conn
@@ -155,7 +155,7 @@ test "does not return favorites for specified user_id when user is not logged in
       user: user
     } do
       activity = insert(:note_activity)
-      CommonAPI.favorite(activity.id, user)
+      CommonAPI.favorite(user, activity.id)
 
       build_conn()
       |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
@@ -172,7 +172,7 @@ test "returns favorited DM only when user is logged in and he is one of recipien
           "visibility" => "direct"
         })
 
-      CommonAPI.favorite(direct.id, user)
+      CommonAPI.favorite(user, direct.id)
 
       for u <- [user, current_user] do
         response =
@@ -202,7 +202,7 @@ test "does not return others' favorited DM when user is not one of recipients",
           "visibility" => "direct"
         })
 
-      CommonAPI.favorite(direct.id, user)
+      CommonAPI.favorite(user, direct.id)
 
       response =
         conn
@@ -219,7 +219,7 @@ test "paginates favorites using since_id and max_id", %{
       activities = insert_list(10, :note_activity)
 
       Enum.each(activities, fn activity ->
-        CommonAPI.favorite(activity.id, user)
+        CommonAPI.favorite(user, activity.id)
       end)
 
       third_activity = Enum.at(activities, 2)
@@ -245,7 +245,7 @@ test "limits favorites using limit parameter", %{
       7
       |> insert_list(:note_activity)
       |> Enum.each(fn activity ->
-        CommonAPI.favorite(activity.id, user)
+        CommonAPI.favorite(user, activity.id)
       end)
 
       response =
@@ -277,7 +277,7 @@ test "returns 404 error when specified user is not exist", %{conn: conn} do
     test "returns 403 error when user has hidden own favorites", %{conn: conn} do
       user = insert(:user, hide_favorites: true)
       activity = insert(:note_activity)
-      CommonAPI.favorite(activity.id, user)
+      CommonAPI.favorite(user, activity.id)
 
       conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites")
 
@@ -287,7 +287,7 @@ test "returns 403 error when user has hidden own favorites", %{conn: conn} do
     test "hides favorites for new users by default", %{conn: conn} do
       user = insert(:user)
       activity = insert(:note_activity)
-      CommonAPI.favorite(activity.id, user)
+      CommonAPI.favorite(user, activity.id)
 
       assert user.hide_favorites
       conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites")
diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs
index 9f931c941e0cdfc0d9f2bcb902c57cc68eb2e7fe..9121d90e743eca90b157e9e450038597026f1cb3 100644
--- a/test/web/push/impl_test.exs
+++ b/test/web/push/impl_test.exs
@@ -170,7 +170,7 @@ test "renders title and body for like activity" do
           "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
       })
 
-    {:ok, activity, _} = CommonAPI.favorite(activity.id, user)
+    {:ok, activity} = CommonAPI.favorite(user, activity.id)
     object = Object.normalize(activity)
 
     assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has favorited your post"
diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs
index 5b928629b85dcebc6bd6ac84fd5fe265b51a6dcc..b3fe22920e62610913a2d431630d6f3d2f6d249e 100644
--- a/test/web/streamer/streamer_test.exs
+++ b/test/web/streamer/streamer_test.exs
@@ -64,9 +64,6 @@ test "it doesn't send notify to the 'user:notification' stream when a user is bl
       blocked = insert(:user)
       {:ok, _user_relationship} = User.block(user, blocked)
 
-      {:ok, activity} = CommonAPI.post(user, %{"status" => ":("})
-      {:ok, notif, _} = CommonAPI.favorite(activity.id, blocked)
-
       task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
 
       Streamer.add_socket(
@@ -74,6 +71,9 @@ test "it doesn't send notify to the 'user:notification' stream when a user is bl
         %{transport_pid: task.pid, assigns: %{user: user}}
       )
 
+      {:ok, activity} = CommonAPI.post(user, %{"status" => ":("})
+      {:ok, notif} = CommonAPI.favorite(blocked, activity.id)
+
       Streamer.stream("user:notification", notif)
       Task.await(task)
     end
@@ -83,10 +83,6 @@ test "it doesn't send notify to the 'user:notification' stream when a thread is
     } do
       user2 = insert(:user)
 
-      {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
-      {:ok, activity} = CommonAPI.add_mute(user, activity)
-      {:ok, notif, _} = CommonAPI.favorite(activity.id, user2)
-
       task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
 
       Streamer.add_socket(
@@ -94,6 +90,10 @@ test "it doesn't send notify to the 'user:notification' stream when a thread is
         %{transport_pid: task.pid, assigns: %{user: user}}
       )
 
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
+      {:ok, activity} = CommonAPI.add_mute(user, activity)
+      {:ok, notif} = CommonAPI.favorite(user2, activity.id)
+
       Streamer.stream("user:notification", notif)
       Task.await(task)
     end
@@ -103,10 +103,6 @@ test "it doesn't send notify to the 'user:notification' stream' when a domain is
     } do
       user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"})
 
-      {:ok, user} = User.block_domain(user, "hecking-lewd-place.com")
-      {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
-      {:ok, notif, _} = CommonAPI.favorite(activity.id, user2)
-
       task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end)
 
       Streamer.add_socket(
@@ -114,6 +110,10 @@ test "it doesn't send notify to the 'user:notification' stream' when a domain is
         %{transport_pid: task.pid, assigns: %{user: user}}
       )
 
+      {:ok, user} = User.block_domain(user, "hecking-lewd-place.com")
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
+      {:ok, notif} = CommonAPI.favorite(user2, activity.id)
+
       Streamer.stream("user:notification", notif)
       Task.await(task)
     end
@@ -476,7 +476,7 @@ test "it does send non-reblog notification for reblog-muted actors" do
     CommonAPI.hide_reblogs(user1, user2)
 
     {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"})
-    {:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, user2)
+    {:ok, favorite_activity} = CommonAPI.favorite(user2, create_activity.id)
 
     task =
       Task.async(fn ->
diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs
index 92f9aa0f515ce21fb5340a6ec65eb94e41770946..f6e13b66127030a0faf24f7a1d9a9619d3d242c3 100644
--- a/test/web/twitter_api/twitter_api_test.exs
+++ b/test/web/twitter_api/twitter_api_test.exs
@@ -109,7 +109,7 @@ test "it registers a new user and parses mentions in the bio" do
     {:ok, user2} = TwitterAPI.register_user(data2)
 
     expected_text =
-      ~s(<span class="h-card"><a data-user="#{user1.id}" class="u-url mention" href="#{
+      ~s(<span class="h-card"><a class="u-url mention" data-user="#{user1.id}" href="#{
         user1.ap_id
       }" rel="ugc">@<span>john</span></a></span> test)