Merge branch 'develop' into feature/database-compaction
authorrinpatch <rinpatch@sdf.org>
Wed, 17 Apr 2019 09:22:32 +0000 (12:22 +0300)
committerrinpatch <rinpatch@sdf.org>
Wed, 17 Apr 2019 09:22:32 +0000 (12:22 +0300)
28 files changed:
1  2 
lib/mix/tasks/compact_database.ex
lib/pleroma/activity.ex
lib/pleroma/gopher/server.ex
lib/pleroma/html.ex
lib/pleroma/object.ex
lib/pleroma/object/containment.ex
lib/pleroma/object/fetcher.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/activity_pub_controller.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/activity_pub/utils.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/common_api/utils.ex
lib/pleroma/web/federator/federator.ex
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
lib/pleroma/web/mastodon_api/views/status_view.ex
lib/pleroma/web/ostatus/activity_representer.ex
lib/pleroma/web/ostatus/handlers/note_handler.ex
lib/pleroma/web/twitter_api/controllers/util_controller.ex
lib/pleroma/web/twitter_api/views/activity_view.ex
test/object_test.exs
test/web/activity_pub/activity_pub_test.exs
test/web/activity_pub/transmogrifier_test.exs
test/web/common_api/common_api_test.exs
test/web/mastodon_api/status_view_test.exs
test/web/ostatus/ostatus_test.exs
test/web/twitter_api/twitter_api_test.exs
test/web/twitter_api/views/activity_view_test.exs

index 7de50812a5709bd134a786ef17eb1c06b276e9ae,0000000000000000000000000000000000000000..17b9721f72b5db5a2f6341bb55b3365ad762a48e
mode 100644,000000..100644
--- /dev/null
@@@ -1,57 -1,0 +1,57 @@@
-   import Mix.Ecto
 +defmodule Mix.Tasks.CompactDatabase do
 +  @moduledoc """
 +  Compact the database by flattening the object graph.
 +  """
 +
 +  require Logger
 +
 +  use Mix.Task
-   alias Pleroma.{Repo, Object, Activity}
 +  import Ecto.Query
-   def run(args) do
++  alias Pleroma.Activity
++  alias Pleroma.Repo
 +
 +  defp maybe_compact(%Activity{data: %{"object" => %{"id" => object_id}}} = activity) do
 +    data =
 +      activity.data
 +      |> Map.put("object", object_id)
 +
 +    {:ok, activity} =
 +      Activity.change(activity, %{data: data})
 +      |> Repo.update()
 +
 +    {:ok, activity}
 +  end
 +
 +  defp maybe_compact(%Activity{} = activity), do: {:ok, activity}
 +
 +  defp activity_query(min_id, max_id) do
 +    from(
 +      a in Activity,
 +      where: fragment("?->>'type' = 'Create'", a.data),
 +      where: a.id >= ^min_id,
 +      where: a.id < ^max_id
 +    )
 +  end
 +
++  def run(_args) do
 +    Application.ensure_all_started(:pleroma)
 +
 +    max = Repo.aggregate(Activity, :max, :id)
 +    Logger.info("Considering #{max} activities")
 +
 +    chunks = 0..round(max / 100)
 +
 +    Enum.each(chunks, fn i ->
 +      min = i * 100
 +      max = min + 100
 +
 +      activity_query(min, max)
 +      |> Repo.all()
 +      |> Enum.each(&maybe_compact/1)
 +
 +      IO.write(".")
 +    end)
 +
 +    Logger.info("Finished.")
 +  end
 +end
diff --combined lib/pleroma/activity.ex
index e3aa4eb976ae5b9ec2287dca86f216c9ffd3ef62,e6507e5ca011b342b8a656c6ce20e1476332eaaa..99cc9c077cb131472711e3e26a98ad880e0709ee
@@@ -1,18 -1,75 +1,76 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Activity do
    use Ecto.Schema
-   alias Pleroma.{Repo, Activity, Notification, Object}
-   import Ecto.{Query, Changeset}
+   alias Pleroma.Activity
+   alias Pleroma.Notification
+   alias Pleroma.Object
+   alias Pleroma.Repo
++  import Ecto.Changeset
+   import Ecto.Query
+   @type t :: %__MODULE__{}
+   @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
+   # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
+   @mastodon_notification_types %{
+     "Create" => "mention",
+     "Follow" => "follow",
+     "Announce" => "reblog",
+     "Like" => "favourite"
+   }
+   @mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
+                                          into: %{},
+                                          do: {v, k}
  
    schema "activities" do
      field(:data, :map)
      field(:local, :boolean, default: true)
      field(:actor, :string)
-     field(:recipients, {:array, :string})
+     field(:recipients, {:array, :string}, default: [])
      has_many(:notifications, Notification, on_delete: :delete_all)
  
+     # Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
+     # The foreign key is embedded in a jsonb field.
+     #
+     # To use it, you probably want to do an inner join and a preload:
+     #
+     # ```
+     # |> join(:inner, [activity], o in Object,
+     #      on: fragment("(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
+     #        o.data, activity.data, activity.data))
+     # |> preload([activity, object], [object: object])
+     # ```
+     #
+     # As a convenience, Activity.with_preloaded_object() sets up an inner join and preload for the
+     # typical case.
+     has_one(:object, Object, on_delete: :nothing, foreign_key: :id)
      timestamps()
    end
  
+   def with_preloaded_object(query) do
+     query
+     |> join(
+       :inner,
+       [activity],
+       o in Object,
+       on:
+         fragment(
+           "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
+           o.data,
+           activity.data,
+           activity.data
+         )
+     )
+     |> preload([activity, object], object: object)
+   end
    def get_by_ap_id(ap_id) do
      Repo.one(
        from(
      )
    end
  
-   # TODO:
-   # Go through these and fix them everywhere.
-   # Wrong name, only returns create activities
-   def all_by_object_ap_id_q(ap_id) do
 +  def change(struct, params \\ %{}) do
 +    struct
 +    |> cast(params, [:data])
 +    |> validate_required([:data])
 +    |> unique_constraint(:ap_id, name: :activities_unique_apid_index)
 +  end
 +
+   def get_by_ap_id_with_object(ap_id) do
+     Repo.one(
+       from(
+         activity in Activity,
+         where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id)),
+         left_join: o in Object,
+         on:
+           fragment(
+             "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
+             o.data,
+             activity.data,
+             activity.data
+           ),
+         preload: [object: o]
+       )
+     )
+   end
+   def get_by_id(id) do
+     Repo.get(Activity, id)
+   end
+   def get_by_id_with_object(id) do
+     from(activity in Activity,
+       where: activity.id == ^id,
+       inner_join: o in Object,
+       on:
+         fragment(
+           "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
+           o.data,
+           activity.data,
+           activity.data
+         ),
+       preload: [object: o]
+     )
+     |> Repo.one()
+   end
+   def by_object_ap_id(ap_id) do
      from(
        activity in Activity,
        where:
            activity.data,
            activity.data,
            ^to_string(ap_id)
+         )
+     )
+   end
+   def create_by_object_ap_id(ap_ids) when is_list(ap_ids) do
+     from(
+       activity in Activity,
+       where:
+         fragment(
+           "coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)",
+           activity.data,
+           activity.data,
+           ^ap_ids
          ),
        where: fragment("(?)->>'type' = 'Create'", activity.data)
      )
    end
  
-   # Wrong name, returns all.
-   def all_non_create_by_object_ap_id_q(ap_id) do
+   def create_by_object_ap_id(ap_id) when is_binary(ap_id) do
      from(
        activity in Activity,
        where:
            activity.data,
            activity.data,
            ^to_string(ap_id)
-         )
+         ),
+       where: fragment("(?)->>'type' = 'Create'", activity.data)
      )
    end
  
-   # Wrong name plz fix thx
-   def all_by_object_ap_id(ap_id) do
-     Repo.all(all_by_object_ap_id_q(ap_id))
+   def create_by_object_ap_id(_), do: nil
+   def get_all_create_by_object_ap_id(ap_id) do
+     Repo.all(create_by_object_ap_id(ap_id))
    end
  
-   def create_activity_by_object_id_query(ap_ids) do
+   def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
+     create_by_object_ap_id(ap_id)
+     |> Repo.one()
+   end
+   def get_create_by_object_ap_id(_), do: nil
+   def create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
      from(
        activity in Activity,
        where:
          fragment(
-           "coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)",
+           "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
            activity.data,
            activity.data,
-           ^ap_ids
+           ^to_string(ap_id)
          ),
-       where: fragment("(?)->>'type' = 'Create'", activity.data)
+       where: fragment("(?)->>'type' = 'Create'", activity.data),
+       inner_join: o in Object,
+       on:
+         fragment(
+           "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
+           o.data,
+           activity.data,
+           activity.data
+         ),
+       preload: [object: o]
      )
    end
  
-   def get_create_activity_by_object_ap_id(ap_id) when is_binary(ap_id) do
-     create_activity_by_object_id_query([ap_id])
+   def create_by_object_ap_id_with_object(_), do: nil
+   def get_create_by_object_ap_id_with_object(ap_id) do
+     ap_id
+     |> create_by_object_ap_id_with_object()
      |> Repo.one()
    end
  
-   def get_create_activity_by_object_ap_id(_), do: nil
-   def normalize(obj) when is_map(obj), do: normalize(obj["id"])
-   def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id(ap_id)
 -  def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
 -  def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
--  def normalize(_), do: nil
 +  defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
-     get_create_activity_by_object_ap_id(ap_id)
++    get_create_by_object_ap_id_with_object(ap_id)
 +  end
 +
 +  defp get_in_reply_to_activity_from_object(_), do: nil
  
 -  def get_in_reply_to_activity(%Activity{data: %{"object" => %{"inReplyTo" => ap_id}}}) do
 -    get_create_by_object_ap_id(ap_id)
 +  def get_in_reply_to_activity(%Activity{data: %{"object" => object}}) do
 +    get_in_reply_to_activity_from_object(Object.normalize(object))
    end
 -  def get_in_reply_to_activity(_), do: nil
++  def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
++  def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
++  def normalize(_), do: nil
+   def delete_by_ap_id(id) when is_binary(id) do
+     by_object_ap_id(id)
+     |> select([u], u)
+     |> Repo.delete_all()
+     |> elem(1)
+     |> Enum.find(fn
+       %{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
+       _ -> nil
+     end)
+   end
+   def delete_by_ap_id(_), do: nil
+   for {ap_type, type} <- @mastodon_notification_types do
+     def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
+       do: unquote(type)
+   end
+   def mastodon_notification_type(%Activity{}), do: nil
+   def from_mastodon_notification_type(type) do
+     Map.get(@mastodon_to_ap_notification_types, type)
+   end
+   def all_by_actor_and_id(actor, status_ids \\ [])
+   def all_by_actor_and_id(_actor, []), do: []
+   def all_by_actor_and_id(actor, status_ids) do
+     Activity
+     |> where([s], s.id in ^status_ids)
+     |> where([s], s.actor == ^actor)
+     |> Repo.all()
+   end
+   def increase_replies_count(nil), do: nil
+   def increase_replies_count(object_ap_id) do
+     from(a in create_by_object_ap_id(object_ap_id),
+       update: [
+         set: [
+           data:
+             fragment(
+               """
+               jsonb_set(?, '{object, repliesCount}',
+                 (coalesce((?->'object'->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
+               """,
+               a.data,
+               a.data
+             )
+         ]
+       ]
+     )
+     |> Repo.update_all([])
+     |> case do
+       {1, [activity]} -> activity
+       _ -> {:error, "Not found"}
+     end
+   end
+   def decrease_replies_count(nil), do: nil
+   def decrease_replies_count(object_ap_id) do
+     from(a in create_by_object_ap_id(object_ap_id),
+       update: [
+         set: [
+           data:
+             fragment(
+               """
+               jsonb_set(?, '{object, repliesCount}',
+                 (greatest(0, (?->'object'->>'repliesCount')::int - 1))::varchar::jsonb, true)
+               """,
+               a.data,
+               a.data
+             )
+         ]
+       ]
+     )
+     |> Repo.update_all([])
+     |> case do
+       {1, [activity]} -> activity
+       _ -> {:error, "Not found"}
+     end
+   end
  end
index 1ab15611c9669e5e704283c4d6db83d5c63e6dda,6a56a6f677c051debc6b8355b1fb442ef5e405b9..2ebc5d5f7a8ebc306d34904f2c97b4f2a2a95ff4
@@@ -1,8 -1,12 +1,12 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Gopher.Server do
    use GenServer
    require Logger
  
-   def start_link() do
+   def start_link do
      config = Pleroma.Config.get(:gopher, [])
      ip = Keyword.get(config, :ip, {0, 0, 0, 0})
      port = Keyword.get(config, :port, 1234)
@@@ -22,7 -26,7 +26,7 @@@
        :gopher,
        100,
        :ranch_tcp,
-       [port: port],
+       [ip: ip, port: port],
        __MODULE__.ProtocolHandler,
        []
      )
  end
  
  defmodule Pleroma.Gopher.Server.ProtocolHandler do
-   alias Pleroma.Web.ActivityPub.ActivityPub
-   alias Pleroma.User
    alias Pleroma.Activity
-   alias Pleroma.Repo
    alias Pleroma.HTML
 +  alias Pleroma.Object
+   alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ActivityPub
+   alias Pleroma.Web.ActivityPub.Visibility
  
    def start_link(ref, socket, transport, opts) do
      pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts])
      {:ok, pid}
    end
  
-   def init(ref, socket, transport, _Opts = []) do
+   def init(ref, socket, transport, [] = _Opts) do
      :ok = :ranch.accept_ack(ref)
      loop(socket, transport)
    end
@@@ -62,7 -65,8 +66,8 @@@
    def link(name, selector, type \\ 1) do
      address = Pleroma.Web.Endpoint.host()
      port = Pleroma.Config.get([:gopher, :port], 1234)
-     "#{type}#{name}\t#{selector}\t#{address}\t#{port}\r\n"
+     dstport = Pleroma.Config.get([:gopher, :dstport], port)
+     "#{type}#{name}\t#{selector}\t#{address}\t#{dstport}\r\n"
    end
  
    def render_activities(activities) do
      |> Enum.map(fn activity ->
        user = User.get_cached_by_ap_id(activity.data["actor"])
  
 -      object = activity.data["object"]
 +      object = Object.normalize(activity.data["object"])
        like_count = object["like_count"] || 0
        announcement_count = object["announcement_count"] || 0
  
        link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <>
          info("#{like_count} likes, #{announcement_count} repeats") <>
          "i\tfake\t(NULL)\t0\r\n" <>
 -        info(HTML.strip_tags(String.replace(activity.data["object"]["content"], "<br>", "\r")))
 +        info(HTML.strip_tags(String.replace(object["content"], "<br>", "\r")))
      end)
      |> Enum.join("i\tfake\t(NULL)\t0\r\n")
    end
    end
  
    def response("/notices/" <> id) do
-     with %Activity{} = activity <- Repo.get(Activity, id),
-          true <- ActivityPub.is_public?(activity) do
+     with %Activity{} = activity <- Activity.get_by_id(id),
+          true <- Visibility.is_public?(activity) do
        activities =
          ActivityPub.fetch_activities_for_context(activity.data["context"])
          |> render_activities
diff --combined lib/pleroma/html.ex
index 1b920d7fd5cf95781bbaa08d6d5f9107ad810658,7f1dbe28c81ce1e9a94cb43ae96bc0291d8cfb17..4b42d8c9b93a178de7d8f120a8a3a8f044672230
@@@ -1,3 -1,7 +1,7 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.HTML do
    alias HtmlSanitizeEx.Scrubber
  
@@@ -5,26 -9,82 +9,83 @@@
    defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers
    defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default]
  
-   def get_scrubbers() do
+   def get_scrubbers do
      Pleroma.Config.get([:markup, :scrub_policy])
      |> get_scrubbers
    end
  
    def filter_tags(html, nil) do
-     get_scrubbers()
-     |> Enum.reduce(html, fn scrubber, html ->
+     filter_tags(html, get_scrubbers())
+   end
+   def filter_tags(html, scrubbers) when is_list(scrubbers) do
+     Enum.reduce(scrubbers, html, fn scrubber, html ->
        filter_tags(html, scrubber)
      end)
    end
  
-   def filter_tags(html, scrubber) do
-     html |> Scrubber.scrub(scrubber)
+   def filter_tags(html, scrubber), do: Scrubber.scrub(html, scrubber)
+   def filter_tags(html), do: filter_tags(html, nil)
+   def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags)
+   def get_cached_scrubbed_html_for_activity(content, scrubbers, activity, key \\ "") do
+     key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"
+     Cachex.fetch!(:scrubber_cache, key, fn _key ->
 -      ensure_scrubbed_html(content, scrubbers, activity.data["object"]["fake"] || false)
++      object = Pleroma.Object.normalize(activity)
++      ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false)
+     end)
+   end
+   def get_cached_stripped_html_for_activity(content, activity, key) do
+     get_cached_scrubbed_html_for_activity(
+       content,
+       HtmlSanitizeEx.Scrubber.StripTags,
+       activity,
+       key
+     )
    end
  
-   def filter_tags(html), do: filter_tags(html, nil)
+   def ensure_scrubbed_html(
+         content,
+         scrubbers,
+         false = _fake
+       ) do
+     {:commit, filter_tags(content, scrubbers)}
+   end
+   def ensure_scrubbed_html(
+         content,
+         scrubbers,
+         true = _fake
+       ) do
+     {:ignore, filter_tags(content, scrubbers)}
+   end
+   defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do
+     generate_scrubber_signature([scrubber])
+   end
+   defp generate_scrubber_signature(scrubbers) do
+     Enum.reduce(scrubbers, "", fn scrubber, signature ->
+       "#{signature}#{to_string(scrubber)}"
+     end)
+   end
  
-   def strip_tags(html) do
-     html |> Scrubber.scrub(Scrubber.StripTags)
+   def extract_first_external_url(_, nil), do: {:error, "No content"}
+   def extract_first_external_url(object, content) do
+     key = "URL|#{object.id}"
+     Cachex.fetch!(:scrubber_cache, key, fn _key ->
+       result =
+         content
+         |> Floki.filter_out("a.mention")
+         |> Floki.attribute("a", "href")
+         |> Enum.at(0)
+       {:commit, {:ok, result}}
+     end)
    end
  end
  
@@@ -35,8 -95,7 +96,7 @@@ defmodule Pleroma.HTML.Scrubber.Twitter
    """
  
    @markup Application.get_env(:pleroma, :markup)
-   @uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
-   @valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
+   @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
  
    require HtmlSanitizeEx.Scrubber.Meta
    alias HtmlSanitizeEx.Scrubber.Meta
    Meta.strip_comments()
  
    # links
-   Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
-   Meta.allow_tag_with_these_attributes("a", ["name", "title"])
+   Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
+   Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
+   Meta.allow_tag_with_this_attribute_values("a", "rel", [
+     "tag",
+     "nofollow",
+     "noopener",
+     "noreferrer"
+   ])
  
    # paragraphs and linebreaks
    Meta.allow_tag_with_these_attributes("br", [])
    Meta.allow_tag_with_these_attributes("p", [])
  
    # microformats
-   Meta.allow_tag_with_these_attributes("span", [])
+   Meta.allow_tag_with_these_attributes("span", ["class"])
  
    # allow inline images for custom emoji
    @allow_inline_images Keyword.get(@markup, :allow_inline_images)
@@@ -78,16 -144,24 +145,24 @@@ defmodule Pleroma.HTML.Scrubber.Defaul
  
    require HtmlSanitizeEx.Scrubber.Meta
    alias HtmlSanitizeEx.Scrubber.Meta
+   # credo:disable-for-previous-line
+   # No idea how to fix this one…
  
    @markup Application.get_env(:pleroma, :markup)
-   @uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
-   @valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
+   @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
  
    Meta.remove_cdata_sections_before_scrub()
    Meta.strip_comments()
  
-   Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
-   Meta.allow_tag_with_these_attributes("a", ["name", "title"])
+   Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
+   Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
+   Meta.allow_tag_with_this_attribute_values("a", "rel", [
+     "tag",
+     "nofollow",
+     "noopener",
+     "noreferrer"
+   ])
  
    Meta.allow_tag_with_these_attributes("abbr", ["title"])
  
    Meta.allow_tag_with_these_attributes("ol", [])
    Meta.allow_tag_with_these_attributes("p", [])
    Meta.allow_tag_with_these_attributes("pre", [])
-   Meta.allow_tag_with_these_attributes("span", [])
+   Meta.allow_tag_with_these_attributes("span", ["class"])
    Meta.allow_tag_with_these_attributes("strong", [])
    Meta.allow_tag_with_these_attributes("u", [])
    Meta.allow_tag_with_these_attributes("ul", [])
@@@ -166,7 -240,7 +241,7 @@@ defmodule Pleroma.HTML.Transform.MediaP
      {"src", media_url}
    end
  
-   def scrub_attribute(tag, attribute), do: attribute
+   def scrub_attribute(_tag, attribute), do: attribute
  
    def scrub({"img", attributes, children}) do
      attributes =
      {"img", attributes, children}
    end
  
-   def scrub({:comment, children}), do: ""
+   def scrub({:comment, _children}), do: ""
  
    def scrub({tag, attributes, children}), do: {tag, attributes, children}
-   def scrub({tag, children}), do: children
+   def scrub({_tag, children}), do: children
    def scrub(text), do: text
  end
diff --combined lib/pleroma/object.ex
index 0e9aefb63c2cc1e79a20bf0fd1950b7c6ade90eb,013d6215710ec42858ab18ed3be69e6775e405ed..3f1d0fea1f28e67651195f337c09f9f16cf214c2
@@@ -1,8 -1,20 +1,21 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Object do
    use Ecto.Schema
-   alias Pleroma.{Repo, Object, Activity}
+   alias Pleroma.Activity
+   alias Pleroma.Object
 +  alias Pleroma.Object.Fetcher
-   import Ecto.{Query, Changeset}
+   alias Pleroma.ObjectTombstone
+   alias Pleroma.Repo
+   alias Pleroma.User
+   import Ecto.Query
+   import Ecto.Changeset
+   require Logger
  
    schema "objects" do
      field(:data, :map)
      Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
    end
  
 -  def normalize(%Activity{object: %Object{} = object}), do: object
 +  def normalize(_, fetch_remote \\ true)
+   # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
+   # Use this whenever possible, especially when walking graphs in an O(N) loop!
++  def normalize(%Activity{object: %Object{} = object}, _), do: object
  
-   def normalize(obj, fetch_remote) when is_map(obj), do: normalize(obj["id"], fetch_remote)
-   def normalize(ap_id, true) when is_binary(ap_id), do: Fetcher.fetch_object_from_id!(ap_id)
+   # A hack for fake activities
 -  def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}) do
++  def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do
+     %Object{id: "pleroma:fake_object_id", data: data}
+   end
+   # Catch and log Object.normalize() calls where the Activity's child object is not
+   # preloaded.
 -  def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}) do
++  def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote) do
+     Logger.debug(
+       "Object.normalize() called without preloaded object (#{ap_id}).  Consider preloading the object!"
+     )
+     Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
 -    normalize(ap_id)
++    normalize(ap_id, fetch_remote)
+   end
 -  def normalize(%Activity{data: %{"object" => ap_id}}) do
++  def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote) do
+     Logger.debug(
+       "Object.normalize() called without preloaded object (#{ap_id}).  Consider preloading the object!"
+     )
+     Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
 -    normalize(ap_id)
++    normalize(ap_id, fetch_remote)
+   end
+   # Old way, try fetching the object through cache.
 -  def normalize(%{"id" => ap_id}), do: normalize(ap_id)
 -  def normalize(ap_id) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
 -  def normalize(_), do: nil
++  def normalize(%{"id" => ap_id}, fetch_remote), do: normalize(ap_id, fetch_remote)
 +  def normalize(ap_id, false) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
-   def normalize(obj, _), do: nil
++  def normalize(ap_id, true) when is_binary(ap_id), do: Fetcher.fetch_object_from_id!(ap_id)
++  def normalize(_, _), do: nil
  
-   if Mix.env() == :test do
-     def get_cached_by_ap_id(ap_id) do
-       get_by_ap_id(ap_id)
-     end
-   else
-     def get_cached_by_ap_id(ap_id) do
-       key = "object:#{ap_id}"
-       Cachex.fetch!(:object_cache, key, fn _ ->
-         object = get_by_ap_id(ap_id)
-         if object do
-           {:commit, object}
-         else
-           {:ignore, object}
-         end
-       end)
-     end
+   # Owned objects can only be mutated by their owner
+   def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
+     do: actor == ap_id
+   # Legacy objects can be mutated by anybody
+   def authorize_mutation(%Object{}, %User{}), do: true
+   def get_cached_by_ap_id(ap_id) do
+     key = "object:#{ap_id}"
+     Cachex.fetch!(:object_cache, key, fn _ ->
+       object = get_by_ap_id(ap_id)
+       if object do
+         {:commit, object}
+       else
+         {:ignore, object}
+       end
+     end)
    end
  
    def context_mapping(context) do
      Object.change(%Object{}, %{data: %{"id" => context}})
    end
  
+   def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
+     %ObjectTombstone{
+       id: id,
+       formerType: type,
+       deleted: deleted
+     }
+     |> Map.from_struct()
+   end
+   def swap_object_with_tombstone(object) do
+     tombstone = make_tombstone(object)
+     object
+     |> Object.change(%{data: tombstone})
+     |> Repo.update()
+   end
    def delete(%Object{data: %{"id" => id}} = object) do
-     with Repo.delete(object),
-          Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
+     with {:ok, _obj} = swap_object_with_tombstone(object),
+          deleted_activity = Activity.delete_by_ap_id(id),
           {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
-       {:ok, object}
+       {:ok, object, deleted_activity}
+     end
+   end
+   def set_cache(%Object{data: %{"id" => ap_id}} = object) do
+     Cachex.put(:object_cache, "object:#{ap_id}", object)
+     {:ok, object}
+   end
+   def update_and_set_cache(changeset) do
+     with {:ok, object} <- Repo.update(changeset) do
+       set_cache(object)
+     else
+       e -> e
+     end
+   end
+   def increase_replies_count(ap_id) do
+     Object
+     |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
+     |> update([o],
+       set: [
+         data:
+           fragment(
+             """
+             jsonb_set(?, '{repliesCount}',
+               (coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
+             """,
+             o.data,
+             o.data
+           )
+       ]
+     )
+     |> Repo.update_all([])
+     |> case do
+       {1, [object]} -> set_cache(object)
+       _ -> {:error, "Not found"}
+     end
+   end
+   def decrease_replies_count(ap_id) do
+     Object
+     |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
+     |> update([o],
+       set: [
+         data:
+           fragment(
+             """
+             jsonb_set(?, '{repliesCount}',
+               (greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
+             """,
+             o.data,
+             o.data
+           )
+       ]
+     )
+     |> Repo.update_all([])
+     |> case do
+       {1, [object]} -> set_cache(object)
+       _ -> {:error, "Not found"}
      end
    end
  end
index 010b768bd02cc0c3a69ac36bb9ab26d1e2dcdf67,0000000000000000000000000000000000000000..27e89d87fb839045d08db024cd3801801b7528c1
mode 100644,000000..100644
--- /dev/null
@@@ -1,64 -1,0 +1,64 @@@
-   def contain_origin(id, %{"actor" => nil}), do: :error
 +defmodule Pleroma.Object.Containment do
 +  @moduledoc """
 +  # Object Containment
 +
 +  This module contains some useful functions for containing objects to specific
 +  origins and determining those origins.  They previously lived in the
 +  ActivityPub `Transmogrifier` module.
 +
 +  Object containment is an important step in validating remote objects to prevent
 +  spoofing, therefore removal of object containment functions is NOT recommended.
 +  """
 +
 +  require Logger
 +
 +  def get_actor(%{"actor" => actor}) when is_binary(actor) do
 +    actor
 +  end
 +
 +  def get_actor(%{"actor" => actor}) when is_list(actor) do
 +    if is_binary(Enum.at(actor, 0)) do
 +      Enum.at(actor, 0)
 +    else
 +      Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
 +      |> Map.get("id")
 +    end
 +  end
 +
 +  def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
 +    id
 +  end
 +
 +  def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
 +    get_actor(%{"actor" => actor})
 +  end
 +
 +  @doc """
 +  Checks that an imported AP object's actor matches the domain it came from.
 +  """
-   def contain_origin(id, %{"actor" => actor} = params) do
++  def contain_origin(_id, %{"actor" => nil}), do: :error
 +
-   def contain_origin_from_id(id, %{"id" => nil}), do: :error
++  def contain_origin(id, %{"actor" => _actor} = params) do
 +    id_uri = URI.parse(id)
 +    actor_uri = URI.parse(get_actor(params))
 +
 +    if id_uri.host == actor_uri.host do
 +      :ok
 +    else
 +      :error
 +    end
 +  end
 +
-   def contain_origin_from_id(id, %{"id" => other_id} = params) do
++  def contain_origin_from_id(_id, %{"id" => nil}), do: :error
 +
++  def contain_origin_from_id(id, %{"id" => other_id} = _params) do
 +    id_uri = URI.parse(id)
 +    other_uri = URI.parse(other_id)
 +
 +    if id_uri.host == other_uri.host do
 +      :ok
 +    else
 +      :error
 +    end
 +  end
 +end
index c98722f39c23bdc3b7267d15bae5e8673ecbccef,0000000000000000000000000000000000000000..19d9c51af2ee7646520d3d65580d24d507ca4de2
mode 100644,000000..100644
--- /dev/null
@@@ -1,78 -1,0 +1,78 @@@
-   alias Pleroma.{Object, Repo}
 +defmodule Pleroma.Object.Fetcher do
++  alias Pleroma.Object
 +  alias Pleroma.Object.Containment
 +  alias Pleroma.Web.ActivityPub.Transmogrifier
 +  alias Pleroma.Web.OStatus
 +
 +  require Logger
 +
 +  @httpoison Application.get_env(:pleroma, :httpoison)
 +
 +  # TODO:
 +  # This will create a Create activity, which we need internally at the moment.
 +  def fetch_object_from_id(id) do
 +    if object = Object.get_cached_by_ap_id(id) do
 +      {:ok, object}
 +    else
 +      Logger.info("Fetching #{id} via AP")
 +
 +      with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
 +           nil <- Object.normalize(data, false),
 +           params <- %{
 +             "type" => "Create",
 +             "to" => data["to"],
 +             "cc" => data["cc"],
 +             "actor" => data["actor"] || data["attributedTo"],
 +             "object" => data
 +           },
 +           :ok <- Containment.contain_origin(id, params),
 +           {:ok, activity} <- Transmogrifier.handle_incoming(params) do
 +        {:ok, Object.normalize(activity.data["object"], false)}
 +      else
 +        {:error, {:reject, nil}} ->
 +          {:reject, nil}
 +
 +        object = %Object{} ->
 +          {:ok, object}
 +
 +        _e ->
 +          Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
 +
 +          case OStatus.fetch_activity_from_url(id) do
 +            {:ok, [activity | _]} -> {:ok, Object.normalize(activity.data["object"], false)}
 +            e -> e
 +          end
 +      end
 +    end
 +  end
 +
 +  def fetch_object_from_id!(id) do
 +    with {:ok, object} <- fetch_object_from_id(id) do
 +      object
 +    else
 +      _e ->
 +        nil
 +    end
 +  end
 +
 +  def fetch_and_contain_remote_object_from_id(id) do
 +    Logger.info("Fetching #{id} via AP")
 +
 +    with true <- String.starts_with?(id, "http"),
 +         {:ok, %{body: body, status_code: code}} when code in 200..299 <-
 +           @httpoison.get(
 +             id,
 +             [Accept: "application/activity+json"],
 +             follow_redirect: true,
 +             timeout: 10000,
 +             recv_timeout: 20000
 +           ),
 +         {:ok, data} <- Jason.decode(body),
 +         :ok <- Containment.contain_origin_from_id(id, data) do
 +      {:ok, data}
 +    else
 +      e ->
 +        {:error, e}
 +    end
 +  end
 +end
index fefefc32064e346afb92c50a187f14185e4a8a1a,54dd4097c755ab99b798a8f11b41915d95d43228..1a3b47cb34fe401d707e6c5e4abbb41785f781bc
@@@ -1,12 -1,26 +1,26 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.ActivityPub.ActivityPub do
-   alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
+   alias Pleroma.Activity
+   alias Pleroma.Instances
+   alias Pleroma.Notification
+   alias Pleroma.Object
 +  alias Pleroma.Object.Fetcher
-   alias Pleroma.Web.ActivityPub.{Transmogrifier, MRF}
-   alias Pleroma.Web.WebFinger
+   alias Pleroma.Pagination
+   alias Pleroma.Repo
+   alias Pleroma.Upload
+   alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.MRF
+   alias Pleroma.Web.ActivityPub.Transmogrifier
    alias Pleroma.Web.Federator
--  alias Pleroma.Web.OStatus
+   alias Pleroma.Web.WebFinger
    import Ecto.Query
    import Pleroma.Web.ActivityPub.Utils
+   import Pleroma.Web.ActivityPub.Visibility
    require Logger
  
    @httpoison Application.get_env(:pleroma, :httpoison)
    defp get_recipients(%{"type" => "Announce"} = data) do
      to = data["to"] || []
      cc = data["cc"] || []
-     recipients = to ++ cc
      actor = User.get_cached_by_ap_id(data["actor"])
  
-     recipients
-     |> Enum.filter(fn recipient ->
-       case User.get_cached_by_ap_id(recipient) do
-         nil ->
-           true
+     recipients =
+       (to ++ cc)
+       |> Enum.filter(fn recipient ->
+         case User.get_cached_by_ap_id(recipient) do
+           nil ->
+             true
+           user ->
+             User.following?(user, actor)
+         end
+       end)
  
-         user ->
-           User.following?(user, actor)
-       end
-     end)
+     {recipients, to, cc}
+   end
  
+   defp get_recipients(%{"type" => "Create"} = data) do
+     to = data["to"] || []
+     cc = data["cc"] || []
+     actor = data["actor"] || []
+     recipients = (to ++ cc ++ [actor]) |> Enum.uniq()
      {recipients, to, cc}
    end
  
      end
    end
  
-   def insert(map, local \\ true) when is_map(map) do
+   defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
+     limit = Pleroma.Config.get([:instance, :remote_limit])
+     String.length(content) <= limit
+   end
+   defp check_remote_limit(_), do: true
+   def increase_note_count_if_public(actor, object) do
+     if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
+   end
+   def decrease_note_count_if_public(actor, object) do
+     if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
+   end
+   def increase_replies_count_if_reply(%{
+         "object" => %{"inReplyTo" => reply_ap_id} = object,
+         "type" => "Create"
+       }) do
+     if is_public?(object) do
+       Activity.increase_replies_count(reply_ap_id)
+       Object.increase_replies_count(reply_ap_id)
+     end
+   end
+   def increase_replies_count_if_reply(_create_data), do: :noop
+   def decrease_replies_count_if_reply(%Object{
+         data: %{"inReplyTo" => reply_ap_id} = object
+       }) do
+     if is_public?(object) do
+       Activity.decrease_replies_count(reply_ap_id)
+       Object.decrease_replies_count(reply_ap_id)
+     end
+   end
+   def decrease_replies_count_if_reply(_object), do: :noop
+   def insert(map, local \\ true, fake \\ false) when is_map(map) do
      with nil <- Activity.normalize(map),
-          map <- lazy_put_activity_defaults(map),
+          map <- lazy_put_activity_defaults(map, fake),
           :ok <- check_actor_is_active(map["actor"]),
+          {_, true} <- {:remote_limit_error, check_remote_limit(map)},
           {:ok, map} <- MRF.filter(map),
-          {:ok, map} <- insert_full_object(map) do
-       {recipients, _, _} = get_recipients(map)
+          {recipients, _, _} = get_recipients(map),
+          {:fake, false, map, recipients} <- {:fake, fake, map, recipients},
 -         {:ok, object} <- insert_full_object(map) do
++         {:ok, map, object} <- insert_full_object(map) do
        {:ok, activity} =
          Repo.insert(%Activity{
            data: map,
            recipients: recipients
          })
  
+       # Splice in the child object if we have one.
+       activity =
+         if !is_nil(object) do
+           Map.put(activity, :object, object)
+         else
+           activity
+         end
+       Task.start(fn ->
+         Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+       end)
        Notification.create_notifications(activity)
        stream_out(activity)
        {:ok, activity}
      else
-       %Activity{} = activity -> {:ok, activity}
-       error -> {:error, error}
+       %Activity{} = activity ->
+         {:ok, activity}
+       {:fake, true, map, recipients} ->
+         activity = %Activity{
+           data: map,
+           local: local,
+           actor: map["actor"],
+           recipients: recipients,
+           id: "pleroma:fakeid"
+         }
+         Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+         {:ok, activity}
+       error ->
+         {:error, error}
      end
    end
  
    def stream_out(activity) do
      public = "https://www.w3.org/ns/activitystreams#Public"
  
 -    if activity.data["type"] in ["Create", "Announce", "Delete"] do
 +    if activity.data["type"] in ["Create", "Announce"] do
 +      object = Object.normalize(activity.data["object"])
        Pleroma.Web.Streamer.stream("user", activity)
        Pleroma.Web.Streamer.stream("list", activity)
  
            Pleroma.Web.Streamer.stream("public:local", activity)
          end
  
-         object.data
-         |> Map.get("tag", [])
-         |> Enum.filter(fn tag -> is_bitstring(tag) end)
-         |> Enum.map(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
+         if activity.data["type"] in ["Create"] do
 -          activity.data["object"]
++          object.data
+           |> Map.get("tag", [])
+           |> Enum.filter(fn tag -> is_bitstring(tag) end)
+           |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
  
-         if object.data["attachment"] != [] do
-           Pleroma.Web.Streamer.stream("public:media", activity)
 -          if activity.data["object"]["attachment"] != [] do
++          if object.data["attachment"] != [] do
+             Pleroma.Web.Streamer.stream("public:media", activity)
  
-           if activity.local do
-             Pleroma.Web.Streamer.stream("public:local:media", activity)
+             if activity.local do
+               Pleroma.Web.Streamer.stream("public:local:media", activity)
+             end
            end
          end
        else
      end
    end
  
-   def create(%{to: to, actor: actor, context: context, object: object} = params) do
+   def create(%{to: to, actor: actor, context: context, object: object} = params, fake \\ false) do
      additional = params[:additional] || %{}
      # only accept false as false value
      local = !(params[:local] == false)
               %{to: to, actor: actor, published: published, context: context, object: object},
               additional
             ),
-          {:ok, activity} <- insert(create_data, local),
-          :ok <- maybe_federate(activity),
-          {:ok, _actor} <- User.increase_note_count(actor) do
+          {:ok, activity} <- insert(create_data, local, fake),
+          {:fake, false, activity} <- {:fake, fake, activity},
+          _ <- increase_replies_count_if_reply(create_data),
+          # Changing note count prior to enqueuing federation task in order to avoid
+          # race conditions on updating user.info
+          {:ok, _actor} <- increase_note_count_if_public(actor, activity),
+          :ok <- maybe_federate(activity) do
        {:ok, activity}
+     else
+       {:fake, true, activity} ->
+         {:ok, activity}
      end
    end
  
      # only accept false as false value
      local = !(params[:local] == false)
  
-     with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object},
+     with data <- %{"to" => to, "type" => "Accept", "actor" => actor.ap_id, "object" => object},
           {:ok, activity} <- insert(data, local),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
      # only accept false as false value
      local = !(params[:local] == false)
  
-     with data <- %{"to" => to, "type" => "Reject", "actor" => actor, "object" => object},
+     with data <- %{"to" => to, "type" => "Reject", "actor" => actor.ap_id, "object" => object},
           {:ok, activity} <- insert(data, local),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
          %User{ap_id: _} = user,
          %Object{data: %{"id" => _}} = object,
          activity_id \\ nil,
-         local \\ true
+         local \\ true,
+         public \\ true
        ) do
      with true <- is_public?(object),
-          announce_data <- make_announce_data(user, object, activity_id),
+          announce_data <- make_announce_data(user, object, activity_id, public),
           {:ok, activity} <- insert(announce_data, local),
           {:ok, object} <- add_announce_to_object(activity, object),
           :ok <- maybe_federate(activity) do
  
    def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
      user = User.get_cached_by_ap_id(actor)
+     to = (object.data["to"] || []) ++ (object.data["cc"] || [])
  
-     data = %{
-       "type" => "Delete",
-       "actor" => actor,
-       "object" => id,
-       "to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"]
-     }
-     with {:ok, _} <- Object.delete(object),
+     with {:ok, object, activity} <- Object.delete(object),
+          data <- %{
+            "type" => "Delete",
+            "actor" => actor,
+            "object" => id,
+            "to" => to,
+            "deleted_activity_id" => activity && activity.id
+          },
           {:ok, activity} <- insert(data, local),
-          :ok <- maybe_federate(activity),
-          {:ok, _actor} <- User.decrease_note_count(user) do
+          _ <- decrease_replies_count_if_reply(object),
+          # Changing note count prior to enqueuing federation task in order to avoid
+          # race conditions on updating user.info
+          {:ok, _actor} <- decrease_note_count_if_public(user, object),
+          :ok <- maybe_federate(activity) do
        {:ok, activity}
      end
    end
      end
    end
  
+   def flag(
+         %{
+           actor: actor,
+           context: context,
+           account: account,
+           statuses: statuses,
+           content: content
+         } = params
+       ) do
+     # only accept false as false value
+     local = !(params[:local] == false)
+     forward = !(params[:forward] == false)
+     additional = params[:additional] || %{}
+     params = %{
+       actor: actor,
+       context: context,
+       account: account,
+       statuses: statuses,
+       content: content
+     }
+     additional =
+       if forward do
+         Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
+       else
+         Map.merge(additional, %{"to" => [], "cc" => []})
+       end
+     with flag_data <- make_flag_data(params, additional),
+          {:ok, activity} <- insert(flag_data, local),
+          :ok <- maybe_federate(activity) do
+       Enum.each(User.all_superusers(), fn superuser ->
+         superuser
+         |> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
+         |> Pleroma.Emails.Mailer.deliver_async()
+       end)
+       {:ok, activity}
+     end
+   end
    def fetch_activities_for_context(context, opts \\ %{}) do
      public = ["https://www.w3.org/ns/activitystreams#Public"]
  
            ),
          order_by: [desc: :id]
        )
+       |> Activity.with_preloaded_object()
  
      Repo.all(query)
    end
  
      q
      |> restrict_unlisted()
-     |> Repo.all()
+     |> Pagination.fetch_paginated(opts)
      |> Enum.reverse()
    end
  
    @valid_visibilities ~w[direct unlisted public private]
  
-   defp restrict_visibility(query, %{visibility: "direct"}) do
-     public = "https://www.w3.org/ns/activitystreams#Public"
-     from(
-       activity in query,
-       join: sender in User,
-       on: sender.ap_id == activity.actor,
-       # Are non-direct statuses with no to/cc possible?
-       where:
-         fragment(
-           "not (? && ?)",
-           [^public, sender.follower_address],
-           activity.recipients
+   defp restrict_visibility(query, %{visibility: visibility})
+        when is_list(visibility) do
+     if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
+       query =
+         from(
+           a in query,
+           where:
+             fragment(
+               "activity_visibility(?, ?, ?) = ANY (?)",
+               a.actor,
+               a.recipients,
+               a.data,
+               ^visibility
+             )
          )
-     )
+       Ecto.Adapters.SQL.to_sql(:all, Repo, query)
+       query
+     else
+       Logger.error("Could not restrict visibility to #{visibility}")
+     end
+   end
+   defp restrict_visibility(query, %{visibility: visibility})
+        when visibility in @valid_visibilities do
+     query =
+       from(
+         a in query,
+         where:
+           fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
+       )
+     Ecto.Adapters.SQL.to_sql(:all, Repo, query)
+     query
    end
  
    defp restrict_visibility(_query, %{visibility: visibility})
        |> Map.put("type", ["Create", "Announce"])
        |> Map.put("actor_id", user.ap_id)
        |> Map.put("whole_db", true)
+       |> Map.put("pinned_activity_ids", user.info.pinned_activities)
  
      recipients =
        if reading_user do
      |> Enum.reverse()
    end
  
+   defp restrict_since(query, %{"since_id" => ""}), do: query
    defp restrict_since(query, %{"since_id" => since_id}) do
      from(activity in query, where: activity.id > ^since_id)
    end
  
    defp restrict_since(query, _), do: query
  
-   defp restrict_tag(query, %{"tag" => tag}) do
+   defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
+        when is_list(tag_reject) and tag_reject != [] do
+     from(
+       activity in query,
+       where: fragment(~s(\(not \(? #> '{"object","tag"}'\) \\?| ?\)), activity.data, ^tag_reject)
+     )
+   end
+   defp restrict_tag_reject(query, _), do: query
+   defp restrict_tag_all(query, %{"tag_all" => tag_all})
+        when is_list(tag_all) and tag_all != [] do
      from(
        activity in query,
-       where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data)
+       where: fragment(~s(\(? #> '{"object","tag"}'\) \\?& ?), activity.data, ^tag_all)
+     )
+   end
+   defp restrict_tag_all(query, _), do: query
+   defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
+     from(
+       activity in query,
+       where: fragment(~s(\(? #> '{"object","tag"}'\) \\?| ?), activity.data, ^tag)
+     )
+   end
+   defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
+     from(
+       activity in query,
+       where: fragment(~s(? <@ (? #> '{"object","tag"}'\)), ^tag, activity.data)
      )
    end
  
      )
    end
  
-   defp restrict_limit(query, %{"limit" => limit}) do
-     from(activity in query, limit: ^limit)
-   end
-   defp restrict_limit(query, _), do: query
    defp restrict_local(query, %{"local_only" => true}) do
      from(activity in query, where: activity.local == true)
    end
  
    defp restrict_local(query, _), do: query
  
-   defp restrict_max(query, %{"max_id" => max_id}) do
-     from(activity in query, where: activity.id < ^max_id)
-   end
-   defp restrict_max(query, _), do: query
    defp restrict_actor(query, %{"actor_id" => actor_id}) do
      from(activity in query, where: activity.actor == ^actor_id)
    end
    defp restrict_actor(query, _), do: query
  
    defp restrict_type(query, %{"type" => type}) when is_binary(type) do
-     restrict_type(query, %{"type" => [type]})
+     from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))
    end
  
    defp restrict_type(query, %{"type" => type}) do
    defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
      from(
        activity in query,
-       where: fragment("? <@ (? #> '{\"object\",\"likes\"}')", ^ap_id, activity.data)
+       where: fragment(~s(? <@ (? #> '{"object","likes"}'\)), ^ap_id, activity.data)
      )
    end
  
    defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
      from(
        activity in query,
-       where: fragment("not (? #> '{\"object\",\"attachment\"}' = ?)", activity.data, ^[])
+       where: fragment(~s(not (? #> '{"object","attachment"}' = ?\)), activity.data, ^[])
      )
    end
  
  
    defp restrict_replies(query, _), do: query
  
-   # Only search through last 100_000 activities by default
-   defp restrict_recent(query, %{"whole_db" => true}), do: query
+   defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do
+     from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
+   end
+   defp restrict_reblogs(query, _), do: query
+   defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
  
-   defp restrict_recent(query, _) do
-     since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_000
+   defp restrict_muted(query, %{"muting_user" => %User{info: info}}) do
+     mutes = info.mutes
  
-     from(activity in query, where: activity.id > ^since)
+     from(
+       activity in query,
+       where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
+       where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
+     )
    end
  
+   defp restrict_muted(query, _), do: query
    defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
      blocks = info.blocks || []
      domain_blocks = info.domain_blocks || []
      )
    end
  
+   defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do
+     from(activity in query, where: activity.id in ^ids)
+   end
+   defp restrict_pinned(query, _), do: query
+   defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
+     muted_reblogs = info.muted_reblogs || []
+     from(
+       activity in query,
+       where:
+         fragment(
+           "not ( ?->>'type' = 'Announce' and ? = ANY(?))",
+           activity.data,
+           activity.actor,
+           ^muted_reblogs
+         )
+     )
+   end
+   defp restrict_muted_reblogs(query, _), do: query
+   defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
+   defp maybe_preload_objects(query, _) do
+     query
+     |> Activity.with_preloaded_object()
+   end
    def fetch_activities_query(recipients, opts \\ %{}) do
-     base_query =
-       from(
-         activity in Activity,
-         limit: 20,
-         order_by: [fragment("? desc nulls last", activity.id)]
-       )
+     base_query = from(activity in Activity)
  
      base_query
+     |> maybe_preload_objects(opts)
      |> restrict_recipients(recipients, opts["user"])
      |> restrict_tag(opts)
+     |> restrict_tag_reject(opts)
+     |> restrict_tag_all(opts)
      |> restrict_since(opts)
      |> restrict_local(opts)
-     |> restrict_limit(opts)
-     |> restrict_max(opts)
      |> restrict_actor(opts)
      |> restrict_type(opts)
      |> restrict_favorited_by(opts)
-     |> restrict_recent(opts)
      |> restrict_blocked(opts)
+     |> restrict_muted(opts)
      |> restrict_media(opts)
      |> restrict_visibility(opts)
      |> restrict_replies(opts)
+     |> restrict_reblogs(opts)
+     |> restrict_pinned(opts)
+     |> restrict_muted_reblogs(opts)
    end
  
    def fetch_activities(recipients, opts \\ %{}) do
      fetch_activities_query(recipients, opts)
-     |> Repo.all()
+     |> Pagination.fetch_paginated(opts)
      |> Enum.reverse()
    end
  
    def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
      fetch_activities_query([], opts)
      |> restrict_to_cc(recipients_to, recipients_cc)
-     |> Repo.all()
+     |> Pagination.fetch_paginated(opts)
      |> Enum.reverse()
    end
  
    def upload(file, opts \\ []) do
      with {:ok, data} <- Upload.store(file, opts) do
-       Repo.insert(%Object{data: data})
+       obj_data =
+         if opts[:actor] do
+           Map.put(data, "actor", opts[:actor])
+         else
+           data
+         end
+       Repo.insert(%Object{data: obj_data})
      end
    end
  
    end
  
    def fetch_and_prepare_user_from_ap_id(ap_id) do
 -    with {:ok, data} <- fetch_and_contain_remote_object_from_id(ap_id) do
 +    with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do
        user_data_from_user_object(data)
      else
        e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
    end
  
    def publish(actor, activity) do
-     followers =
+     remote_followers =
        if actor.follower_address in activity.recipients do
          {:ok, followers} = User.get_followers(actor)
          followers |> Enum.filter(&(!&1.local))
  
      public = is_public?(activity)
  
-     remote_inboxes =
-       (Pleroma.Web.Salmon.remote_users(activity) ++ followers)
-       |> Enum.filter(fn user -> User.ap_enabled?(user) end)
-       |> Enum.map(fn %{info: %{source_data: data}} ->
-         (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
-       end)
-       |> Enum.uniq()
-       |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
      {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
      json = Jason.encode!(data)
  
-     Enum.each(remote_inboxes, fn inbox ->
-       Federator.enqueue(:publish_single_ap, %{
+     (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
+     |> Enum.filter(fn user -> User.ap_enabled?(user) end)
+     |> Enum.map(fn %{info: %{source_data: data}} ->
+       (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
+     end)
+     |> Enum.uniq()
+     |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
+     |> Instances.filter_reachable()
+     |> Enum.each(fn {inbox, unreachable_since} ->
+       Federator.publish_single_ap(%{
          inbox: inbox,
          json: json,
          actor: actor,
-         id: activity.data["id"]
+         id: activity.data["id"],
+         unreachable_since: unreachable_since
        })
      end)
    end
  
-   def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
+   def publish_one(%{inbox: inbox, json: json, actor: actor, id: id} = params) do
      Logger.info("Federating #{id} to #{inbox}")
      host = URI.parse(inbox).host
  
      digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
  
+     date =
+       NaiveDateTime.utc_now()
+       |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
      signature =
        Pleroma.Web.HTTPSignatures.sign(actor, %{
          host: host,
          "content-length": byte_size(json),
-         digest: digest
+         digest: digest,
+         date: date
        })
  
-     @httpoison.post(
-       inbox,
-       json,
-       [
-         {"Content-Type", "application/activity+json"},
-         {"signature", signature},
-         {"digest", digest}
-       ],
-       hackney: [pool: :default]
-     )
-   end
-   def is_public?(activity) do
-     "https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++
-                                                          (activity.data["cc"] || []))
-   end
-   def visible_for_user?(activity, nil) do
-     is_public?(activity)
-   end
-   def visible_for_user?(activity, user) do
-     x = [user.ap_id | user.following]
-     y = activity.data["to"] ++ (activity.data["cc"] || [])
-     visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
-   end
-   # guard
-   def entire_thread_visible_for_user?(nil, user), do: false
-   # child / root
-   def entire_thread_visible_for_user?(
-         %Activity{data: %{"object" => object_id}} = tail,
-         user
-       ) do
-     parent = Activity.get_in_reply_to_activity(tail)
-     cond do
-       !is_nil(parent) ->
-         visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
-       true ->
-         visible_for_user?(tail, user)
+     with {:ok, %{status: code}} when code in 200..299 <-
+            result =
+              @httpoison.post(
+                inbox,
+                json,
+                [
+                  {"Content-Type", "application/activity+json"},
+                  {"Date", date},
+                  {"signature", signature},
+                  {"digest", digest}
+                ]
+              ) do
+       if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
+         do: Instances.set_reachable(inbox)
+       result
+     else
+       {_post_result, response} ->
+         unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
+         {:error, response}
      end
    end
  
 -  # TODO:
 -  # This will create a Create activity, which we need internally at the moment.
 -  def fetch_object_from_id(id) do
 -    if object = Object.get_cached_by_ap_id(id) do
 -      {:ok, object}
 -    else
 -      with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
 -           nil <- Object.normalize(data),
 -           params <- %{
 -             "type" => "Create",
 -             "to" => data["to"],
 -             "cc" => data["cc"],
 -             "actor" => data["actor"] || data["attributedTo"],
 -             "object" => data
 -           },
 -           :ok <- Transmogrifier.contain_origin(id, params),
 -           {:ok, activity} <- Transmogrifier.handle_incoming(params) do
 -        {:ok, Object.normalize(activity)}
 -      else
 -        {:error, {:reject, nil}} ->
 -          {:reject, nil}
 -
 -        object = %Object{} ->
 -          {:ok, object}
 -
 -        _e ->
 -          Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
 -
 -          case OStatus.fetch_activity_from_url(id) do
 -            {:ok, [activity | _]} -> {:ok, Object.normalize(activity)}
 -            e -> e
 -          end
 -      end
 -    end
 -  end
 -
 -  def fetch_and_contain_remote_object_from_id(id) do
 -    Logger.info("Fetching object #{id} via AP")
 -
 -    with true <- String.starts_with?(id, "http"),
 -         {:ok, %{body: body, status: code}} when code in 200..299 <-
 -           @httpoison.get(
 -             id,
 -             [{:Accept, "application/activity+json"}]
 -           ),
 -         {:ok, data} <- Jason.decode(body),
 -         :ok <- Transmogrifier.contain_origin_from_id(id, data) do
 -      {:ok, data}
 -    else
 -      e ->
 -        {:error, e}
 -    end
 -  end
 -
    # filter out broken threads
    def contain_broken_threads(%Activity{} = activity, %User{} = user) do
      entire_thread_visible_for_user?(activity, user)
index 7b7c0e090da7d4194a23d69bc5d14cbc53c6b935,3331ebebd3b67406c5ec4e4a57797dd33a62eca5..0b80566bf5be5859ce25ccf54e00a8deb3188c48
@@@ -1,11 -1,20 +1,21 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.ActivityPub.ActivityPubController do
    use Pleroma.Web, :controller
-   alias Pleroma.{User, Object}
+   alias Pleroma.Activity
+   alias Pleroma.Object
 +  alias Pleroma.Object.Fetcher
-   alias Pleroma.Web.ActivityPub.{ObjectView, UserView}
+   alias Pleroma.User
    alias Pleroma.Web.ActivityPub.ActivityPub
+   alias Pleroma.Web.ActivityPub.ObjectView
    alias Pleroma.Web.ActivityPub.Relay
+   alias Pleroma.Web.ActivityPub.Transmogrifier
+   alias Pleroma.Web.ActivityPub.UserView
    alias Pleroma.Web.ActivityPub.Utils
+   alias Pleroma.Web.ActivityPub.Visibility
    alias Pleroma.Web.Federator
  
    require Logger
@@@ -13,6 -22,7 +23,7 @@@
    action_fallback(:errors)
  
    plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
+   plug(:set_requester_reachable when action in [:inbox])
    plug(:relay_active? when action in [:relay])
  
    def relay_active?(conn, _) do
@@@ -40,7 -50,7 +51,7 @@@
    def object(conn, %{"uuid" => uuid}) do
      with ap_id <- o_status_url(conn, :object, uuid),
           %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
-          {_, true} <- {:public?, ActivityPub.is_public?(object)} do
+          {_, true} <- {:public?, Visibility.is_public?(object)} do
        conn
        |> put_resp_header("content-type", "application/activity+json")
        |> json(ObjectView.render("object.json", %{object: object}))
      end
    end
  
+   def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
+     with ap_id <- o_status_url(conn, :object, uuid),
+          %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
+          {_, true} <- {:public?, Visibility.is_public?(object)},
+          likes <- Utils.get_object_likes(object) do
+       {page, _} = Integer.parse(page)
+       conn
+       |> put_resp_header("content-type", "application/activity+json")
+       |> json(ObjectView.render("likes.json", ap_id, likes, page))
+     else
+       {:public?, false} ->
+         {:error, :not_found}
+     end
+   end
+   def object_likes(conn, %{"uuid" => uuid}) do
+     with ap_id <- o_status_url(conn, :object, uuid),
+          %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
+          {_, true} <- {:public?, Visibility.is_public?(object)},
+          likes <- Utils.get_object_likes(object) do
+       conn
+       |> put_resp_header("content-type", "application/activity+json")
+       |> json(ObjectView.render("likes.json", ap_id, likes))
+     else
+       {:public?, false} ->
+         {:error, :not_found}
+     end
+   end
+   def activity(conn, %{"uuid" => uuid}) do
+     with ap_id <- o_status_url(conn, :activity, uuid),
+          %Activity{} = activity <- Activity.normalize(ap_id),
+          {_, true} <- {:public?, Visibility.is_public?(activity)} do
+       conn
+       |> put_resp_header("content-type", "application/activity+json")
+       |> json(ObjectView.render("object.json", %{object: activity}))
+     else
+       {:public?, false} ->
+         {:error, :not_found}
+     end
+   end
    def following(conn, %{"nickname" => nickname, "page" => page}) do
      with %User{} = user <- User.get_cached_by_nickname(nickname),
           {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
      end
    end
  
-   def outbox(conn, %{"nickname" => nickname, "max_id" => max_id}) do
+   def outbox(conn, %{"nickname" => nickname} = params) do
      with %User{} = user <- User.get_cached_by_nickname(nickname),
           {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
        conn
        |> put_resp_header("content-type", "application/activity+json")
-       |> json(UserView.render("outbox.json", %{user: user, max_id: max_id}))
+       |> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
      end
    end
  
-   def outbox(conn, %{"nickname" => nickname}) do
-     outbox(conn, %{"nickname" => nickname, "max_id" => nil})
-   end
    def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
-     with %User{} = user <- User.get_cached_by_nickname(nickname),
-          true <- Utils.recipient_in_message(user.ap_id, params),
-          params <- Utils.maybe_splice_recipient(user.ap_id, params) do
-       Federator.enqueue(:incoming_ap_doc, params)
+     with %User{} = recipient <- User.get_cached_by_nickname(nickname),
+          %User{} = actor <- User.get_or_fetch_by_ap_id(params["actor"]),
+          true <- Utils.recipient_in_message(recipient, actor, params),
+          params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
+       Federator.incoming_ap_doc(params)
        json(conn, "ok")
      end
    end
  
    def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
-     Federator.enqueue(:incoming_ap_doc, params)
+     Federator.incoming_ap_doc(params)
      json(conn, "ok")
    end
  
        "Signature missing or not from author, relayed Create message, fetching object from source"
      )
  
 -    ActivityPub.fetch_object_from_id(params["object"]["id"])
 +    Fetcher.fetch_object_from_id(params["object"]["id"])
  
      json(conn, "ok")
    end
      json(conn, "error")
    end
  
-   def relay(conn, params) do
+   def relay(conn, _params) do
      with %User{} = user <- Relay.get_actor(),
           {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
        conn
      end
    end
  
+   def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
+     conn
+     |> put_resp_header("content-type", "application/activity+json")
+     |> json(UserView.render("user.json", %{user: user}))
+   end
+   def whoami(_conn, _params), do: {:error, :not_found}
+   def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do
+     if nickname == user.nickname do
+       conn
+       |> put_resp_header("content-type", "application/activity+json")
+       |> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]}))
+     else
+       conn
+       |> put_status(:forbidden)
+       |> json("can't read inbox of #{nickname} as #{user.nickname}")
+     end
+   end
+   def handle_user_activity(user, %{"type" => "Create"} = params) do
+     object =
+       params["object"]
+       |> Map.merge(Map.take(params, ["to", "cc"]))
+       |> Map.put("attributedTo", user.ap_id())
+       |> Transmogrifier.fix_object()
+     ActivityPub.create(%{
+       to: params["to"],
+       actor: user,
+       context: object["context"],
+       object: object,
+       additional: Map.take(params, ["cc"])
+     })
+   end
+   def handle_user_activity(user, %{"type" => "Delete"} = params) do
+     with %Object{} = object <- Object.normalize(params["object"]),
+          true <- user.info.is_moderator || user.ap_id == object.data["actor"],
+          {:ok, delete} <- ActivityPub.delete(object) do
+       {:ok, delete}
+     else
+       _ -> {:error, "Can't delete object"}
+     end
+   end
+   def handle_user_activity(user, %{"type" => "Like"} = params) do
+     with %Object{} = object <- Object.normalize(params["object"]),
+          {:ok, activity, _object} <- ActivityPub.like(user, object) do
+       {:ok, activity}
+     else
+       _ -> {:error, "Can't like object"}
+     end
+   end
+   def handle_user_activity(_, _) do
+     {:error, "Unhandled activity type"}
+   end
+   def update_outbox(
+         %{assigns: %{user: user}} = conn,
+         %{"nickname" => nickname} = params
+       ) do
+     if nickname == user.nickname do
+       actor = user.ap_id()
+       params =
+         params
+         |> Map.drop(["id"])
+         |> Map.put("actor", actor)
+         |> Transmogrifier.fix_addressing()
+       with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
+         conn
+         |> put_status(:created)
+         |> put_resp_header("location", activity.data["id"])
+         |> json(activity.data)
+       else
+         {:error, message} ->
+           conn
+           |> put_status(:bad_request)
+           |> json(message)
+       end
+     else
+       conn
+       |> put_status(:forbidden)
+       |> json("can't update outbox of #{nickname} as #{user.nickname}")
+     end
+   end
    def errors(conn, {:error, :not_found}) do
      conn
      |> put_status(404)
      |> put_status(500)
      |> json("error")
    end
+   defp set_requester_reachable(%Plug.Conn{} = conn, _) do
+     with actor <- conn.params["actor"],
+          true <- is_binary(actor) do
+       Pleroma.Instances.set_reachable(actor)
+     end
+     conn
+   end
  end
index c4567193fee1475ae868716afd5a40ea7d2db084,39cd319212c415b265282dd12ae766907139c5f6..0637b18dc17e07089a2dfbf6cdd13a0e69a38f42
@@@ -1,27 -1,81 +1,34 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.ActivityPub.Transmogrifier do
    @moduledoc """
    A module to handle coding from internal to wire ActivityPub and back.
    """
-   alias Pleroma.Object.{Containment, Fetcher}
 +  alias Pleroma.User
 +  alias Pleroma.Object
++  alias Pleroma.Object.Containment
    alias Pleroma.Activity
+   alias Pleroma.Object
    alias Pleroma.Repo
+   alias Pleroma.User
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Utils
+   alias Pleroma.Web.ActivityPub.Visibility
  
    import Ecto.Query
  
    require Logger
  
 -  def get_actor(%{"actor" => actor}) when is_binary(actor) do
 -    actor
 -  end
 -
 -  def get_actor(%{"actor" => actor}) when is_list(actor) do
 -    if is_binary(Enum.at(actor, 0)) do
 -      Enum.at(actor, 0)
 -    else
 -      Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
 -      |> Map.get("id")
 -    end
 -  end
 -
 -  def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
 -    id
 -  end
 -
 -  def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
 -    get_actor(%{"actor" => actor})
 -  end
 -
 -  @doc """
 -  Checks that an imported AP object's actor matches the domain it came from.
 -  """
 -  def contain_origin(_id, %{"actor" => nil}), do: :error
 -
 -  def contain_origin(id, %{"actor" => _actor} = params) do
 -    id_uri = URI.parse(id)
 -    actor_uri = URI.parse(get_actor(params))
 -
 -    if id_uri.host == actor_uri.host do
 -      :ok
 -    else
 -      :error
 -    end
 -  end
 -
 -  def contain_origin_from_id(_id, %{"id" => nil}), do: :error
 -
 -  def contain_origin_from_id(id, %{"id" => other_id} = _params) do
 -    id_uri = URI.parse(id)
 -    other_uri = URI.parse(other_id)
 -
 -    if id_uri.host == other_uri.host do
 -      :ok
 -    else
 -      :error
 -    end
 -  end
 -
    @doc """
    Modifies an incoming AP object (mastodon format) to our internal format.
    """
    def fix_object(object) do
      object
      |> fix_actor
-     |> fix_attachments
      |> fix_url
+     |> fix_attachments
      |> fix_context
      |> fix_in_reply_to
      |> fix_emoji
      |> fix_content_map
      |> fix_likes
      |> fix_addressing
+     |> fix_summary
+   end
+   def fix_summary(%{"summary" => nil} = object) do
+     object
+     |> Map.put("summary", "")
+   end
+   def fix_summary(%{"summary" => _} = object) do
+     # summary is present, nothing to do
+     object
+   end
+   def fix_summary(object) do
+     object
+     |> Map.put("summary", "")
    end
  
    def fix_addressing_list(map, field) do
-     if is_binary(map[field]) do
-       map
-       |> Map.put(field, [map[field]])
+     cond do
+       is_binary(map[field]) ->
+         Map.put(map, field, [map[field]])
+       is_nil(map[field]) ->
+         Map.put(map, field, [])
+       true ->
+         map
+     end
+   end
+   def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do
+     explicit_to =
+       to
+       |> Enum.filter(fn x -> x in explicit_mentions end)
+     explicit_cc =
+       to
+       |> Enum.filter(fn x -> x not in explicit_mentions end)
+     final_cc =
+       (cc ++ explicit_cc)
+       |> Enum.uniq()
+     object
+     |> Map.put("to", explicit_to)
+     |> Map.put("cc", final_cc)
+   end
+   def fix_explicit_addressing(object, _explicit_mentions), do: object
+   # if directMessage flag is set to true, leave the addressing alone
+   def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
+   def fix_explicit_addressing(object) do
+     explicit_mentions =
+       object
+       |> Utils.determine_explicit_mentions()
+     explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"]
+     object
+     |> fix_explicit_addressing(explicit_mentions)
+   end
+   # if as:Public is addressed, then make sure the followers collection is also addressed
+   # so that the activities will be delivered to local users.
+   def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
+     recipients = to ++ cc
+     if followers_collection not in recipients do
+       cond do
+         "https://www.w3.org/ns/activitystreams#Public" in cc ->
+           to = to ++ [followers_collection]
+           Map.put(object, "to", to)
+         "https://www.w3.org/ns/activitystreams#Public" in to ->
+           cc = cc ++ [followers_collection]
+           Map.put(object, "cc", cc)
+         true ->
+           object
+       end
      else
-       map
+       object
      end
    end
  
-   def fix_addressing(map) do
-     map
+   def fix_implicit_addressing(object, _), do: object
+   def fix_addressing(object) do
+     %User{} = user = User.get_or_fetch_by_ap_id(object["actor"])
+     followers_collection = User.ap_followers(user)
+     object
      |> fix_addressing_list("to")
      |> fix_addressing_list("cc")
      |> fix_addressing_list("bto")
      |> fix_addressing_list("bcc")
+     |> fix_explicit_addressing
+     |> fix_implicit_addressing(followers_collection)
    end
  
    def fix_actor(%{"attributedTo" => actor} = object) do
      object
 -    |> Map.put("actor", get_actor(%{"actor" => actor}))
 +    |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
    end
  
-   def fix_likes(%{"likes" => likes} = object)
-       when is_bitstring(likes) do
-     # Check for standardisation
-     # This is what Peertube does
-     # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
+   # Check for standardisation
+   # This is what Peertube does
+   # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
+   # Prismo returns only an integer (count) as "likes"
+   def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do
      object
      |> Map.put("likes", [])
      |> Map.put("like_count", 0)
            ""
        end
  
 -    case fetch_obj_helper(in_reply_to_id) do
 +    case get_obj_helper(in_reply_to_id) do
        {:ok, replied_object} ->
-         with %Activity{} = activity <-
-                Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) do
+         with %Activity{} = _activity <-
+                Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
            object
            |> Map.put("inReplyTo", replied_object.data["id"])
            |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
-           |> Map.put("inReplyToStatusId", activity.id)
            |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
            |> Map.put("context", replied_object.data["context"] || object["conversation"])
          else
      attachments =
        attachment
        |> Enum.map(fn data ->
-         url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
-         Map.put(data, "url", url)
+         media_type = data["mediaType"] || data["mimeType"]
+         href = data["url"] || data["href"]
+         url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
+         data
+         |> Map.put("mediaType", media_type)
+         |> Map.put("url", url)
        end)
  
      object
      |> Map.put("url", url["href"])
    end
  
-   def fix_url(%{"url" => url} = object) when is_list(url) do
+   def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
+     first_element = Enum.at(url, 0)
+     link_element =
+       url
+       |> Enum.filter(fn x -> is_map(x) end)
+       |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
+       |> Enum.at(0)
+     object
+     |> Map.put("attachment", [first_element])
+     |> Map.put("url", link_element["href"])
+   end
+   def fix_url(%{"type" => object_type, "url" => url} = object)
+       when object_type != "Video" and is_list(url) do
      first_element = Enum.at(url, 0)
  
      url_string =
      |> Map.put("tag", combined)
    end
  
+   def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
    def fix_tag(object), do: object
  
    # content map usually only has one language so this will do for now.
  
    def fix_content_map(object), do: object
  
+   defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
+     with true <- id =~ "follows",
+          %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
+          %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
+       {:ok, activity}
+     else
+       _ -> {:error, nil}
+     end
+   end
+   defp mastodon_follow_hack(_, _), do: {:error, nil}
+   defp get_follow_activity(follow_object, followed) do
+     with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
+          {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
+       {:ok, activity}
+     else
+       # Can't find the activity. This might a Mastodon 2.3 "Accept"
+       {:activity, nil} ->
+         mastodon_follow_hack(follow_object, followed)
+       _ ->
+         {:error, nil}
+     end
+   end
+   # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
+   # with nil ID.
+   def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do
+     with context <- data["context"] || Utils.generate_context_id(),
+          content <- data["content"] || "",
+          %User{} = actor <- User.get_cached_by_ap_id(actor),
+          # Reduce the object list to find the reported user.
+          %User{} = account <-
+            Enum.reduce_while(objects, nil, fn ap_id, _ ->
+              with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
+                {:halt, user}
+              else
+                _ -> {:cont, nil}
+              end
+            end),
+          # Remove the reported user from the object list.
+          statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
+       params = %{
+         actor: actor,
+         context: context,
+         account: account,
+         statuses: statuses,
+         content: content,
+         additional: %{
+           "cc" => [account.ap_id]
+         }
+       }
+       ActivityPub.flag(params)
+     end
+   end
    # disallow objects with bogus IDs
    def handle_incoming(%{"id" => nil}), do: :error
    def handle_incoming(%{"id" => ""}), do: :error
    # - emoji
    def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
        when objtype in ["Article", "Note", "Video", "Page"] do
 -    actor = get_actor(data)
 +    actor = Containment.get_actor(data)
  
      data =
        Map.put(data, "actor", actor)
        |> fix_addressing
  
-     with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
+     with nil <- Activity.get_create_by_object_ap_id(object["id"]),
           %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
        object = fix_object(data["object"])
  
          additional:
            Map.take(data, [
              "cc",
+             "directMessage",
              "id"
            ])
        }
        if not User.locked?(followed) do
          ActivityPub.accept(%{
            to: [follower.ap_id],
-           actor: followed.ap_id,
+           actor: followed,
            object: data,
            local: true
          })
      end
    end
  
-   defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
-     with true <- id =~ "follows",
-          %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
-          %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
-       {:ok, activity}
-     else
-       _ -> {:error, nil}
-     end
-   end
-   defp mastodon_follow_hack(_), do: {:error, nil}
-   defp get_follow_activity(follow_object, followed) do
-     with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
-          {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
-       {:ok, activity}
-     else
-       # Can't find the activity. This might a Mastodon 2.3 "Accept"
-       {:activity, nil} ->
-         mastodon_follow_hack(follow_object, followed)
-       _ ->
-         {:error, nil}
-     end
-   end
    def handle_incoming(
-         %{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data
+         %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
        ) do
 -    with actor <- get_actor(data),
 +    with actor <- Containment.get_actor(data),
           %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
           {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
           {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
             ActivityPub.accept(%{
               to: follow_activity.data["to"],
               type: "Accept",
-              actor: followed.ap_id,
+              actor: followed,
               object: follow_activity.data["id"],
               local: false
             }) do
        if not User.following?(follower, followed) do
-         {:ok, follower} = User.follow(follower, followed)
+         {:ok, _follower} = User.follow(follower, followed)
        end
  
        {:ok, activity}
    end
  
    def handle_incoming(
-         %{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data
+         %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
        ) do
 -    with actor <- get_actor(data),
 +    with actor <- Containment.get_actor(data),
           %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
           {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
           {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
           %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
           {:ok, activity} <-
-            ActivityPub.accept(%{
+            ActivityPub.reject(%{
               to: follow_activity.data["to"],
-              type: "Accept",
-              actor: followed.ap_id,
+              type: "Reject",
+              actor: followed,
               object: follow_activity.data["id"],
               local: false
             }) do
    end
  
    def handle_incoming(
-         %{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data
+         %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
        ) do
 -    with actor <- get_actor(data),
 +    with actor <- Containment.get_actor(data),
           %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
 -         {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
 +         {:ok, object} <- get_obj_helper(object_id),
           {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
        {:ok, activity}
      else
    end
  
    def handle_incoming(
-         %{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
+         %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
        ) do
 -    with actor <- get_actor(data),
 +    with actor <- Containment.get_actor(data),
           %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
 -         {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
 +         {:ok, object} <- get_obj_helper(object_id),
-          {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
+          public <- Visibility.is_public?(data),
+          {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
        {:ok, activity}
      else
        _e -> :error
        ) do
      object_id = Utils.get_ap_id(object_id)
  
 -    with actor <- get_actor(data),
 +    with actor <- Containment.get_actor(data),
           %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
 -         {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
 -         :ok <- contain_origin(actor.ap_id, object.data),
 +         {:ok, object} <- get_obj_helper(object_id),
 +         :ok <- Containment.contain_origin(actor.ap_id, object.data),
           {:ok, activity} <- ActivityPub.delete(object, false) do
        {:ok, activity}
      else
          %{
            "type" => "Undo",
            "object" => %{"type" => "Announce", "object" => object_id},
-           "actor" => actor,
+           "actor" => _actor,
            "id" => id
          } = data
        ) do
 -    with actor <- get_actor(data),
 +    with actor <- Containment.get_actor(data),
           %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
 -         {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
 +         {:ok, object} <- get_obj_helper(object_id),
           {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
        {:ok, activity}
      else
        User.unfollow(follower, followed)
        {:ok, activity}
      else
-       e -> :error
+       _e -> :error
      end
    end
  
        User.unblock(blocker, blocked)
        {:ok, activity}
      else
-       e -> :error
+       _e -> :error
      end
    end
  
    def handle_incoming(
-         %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data
+         %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
        ) do
      with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
           %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
        User.block(blocker, blocked)
        {:ok, activity}
      else
-       e -> :error
+       _e -> :error
      end
    end
  
          %{
            "type" => "Undo",
            "object" => %{"type" => "Like", "object" => object_id},
-           "actor" => actor,
+           "actor" => _actor,
            "id" => id
          } = data
        ) do
 -    with actor <- get_actor(data),
 +    with actor <- Containment.get_actor(data),
           %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
 -         {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
 +         {:ok, object} <- get_obj_helper(object_id),
           {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
        {:ok, activity}
      else
  
    def handle_incoming(_), do: :error
  
 -  def fetch_obj_helper(id) when is_bitstring(id), do: ActivityPub.fetch_object_from_id(id)
 -  def fetch_obj_helper(obj) when is_map(obj), do: ActivityPub.fetch_object_from_id(obj["id"])
 -
    def get_obj_helper(id) do
      if object = Object.normalize(id), do: {:ok, object}, else: nil
    end
  
-   def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do
-     with false <- String.starts_with?(inReplyTo, "http"),
-          {:ok, %{data: replied_to_object}} <- get_obj_helper(inReplyTo) do
-       Map.put(object, "inReplyTo", replied_to_object["external_url"] || inReplyTo)
+   def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
+     with false <- String.starts_with?(in_reply_to, "http"),
+          {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
+       Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
      else
        _e -> object
      end
      |> add_mention_tags
      |> add_emoji_tags
      |> add_attributed_to
+     |> add_likes
      |> prepare_attachments
      |> set_conversation
      |> set_reply_to_uri
    #  internal -> Mastodon
    #  """
  
 -  def prepare_outgoing(%{"type" => "Create", "object" => object} = data) do
 +  def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
      object =
 -      object
 +      Object.normalize(object_id).data
        |> prepare_object
  
      data =
    def prepare_outgoing(%{"type" => _type} = data) do
      data =
        data
+       |> strip_internal_fields
        |> maybe_fix_object_url
        |> Map.merge(Utils.make_json_ld_header())
  
  
    def maybe_fix_object_url(data) do
      if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
 -      case fetch_obj_helper(data["object"]) do
 +      case get_obj_helper(data["object"]) do
          {:ok, relative_object} ->
            if relative_object.data["external_url"] do
              _data =
    def add_hashtags(object) do
      tags =
        (object["tag"] || [])
-       |> Enum.map(fn tag ->
-         %{
-           "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
-           "name" => "##{tag}",
-           "type" => "Hashtag"
-         }
+       |> Enum.map(fn
+         # Expand internal representation tags into AS2 tags.
+         tag when is_binary(tag) ->
+           %{
+             "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
+             "name" => "##{tag}",
+             "type" => "Hashtag"
+           }
+         # Do not process tags which are already AS2 tag objects.
+         tag when is_map(tag) ->
+           tag
        end)
  
      object
    end
  
    def add_attributed_to(object) do
-     attributedTo = object["attributedTo"] || object["actor"]
+     attributed_to = object["attributedTo"] || object["actor"]
+     object
+     |> Map.put("attributedTo", attributed_to)
+   end
+   def add_likes(%{"id" => id, "like_count" => likes} = object) do
+     likes = %{
+       "id" => "#{id}/likes",
+       "first" => "#{id}/likes?page=1",
+       "type" => "OrderedCollection",
+       "totalItems" => likes
+     }
+     object
+     |> Map.put("likes", likes)
+   end
  
+   def add_likes(object) do
      object
-     |> Map.put("attributedTo", attributedTo)
    end
  
    def prepare_attachments(object) do
    defp strip_internal_fields(object) do
      object
      |> Map.drop([
-       "likes",
        "like_count",
        "announcements",
        "announcement_count",
        "emoji",
-       "context_id"
+       "context_id",
+       "deleted_activity_id"
      ])
    end
  
  
    defp strip_internal_tags(object), do: object
  
-   defp user_upgrade_task(user) do
-     old_follower_address = User.ap_followers(user)
+   def perform(:user_upgrade, user) do
+     # we pass a fake user so that the followers collection is stripped away
+     old_follower_address = User.ap_followers(%User{nickname: user.nickname})
  
      q =
        from(
  
      maybe_retire_websub(user.ap_id)
  
-     # Only do this for recent activties, don't go through the whole db.
-     # Only look at the last 1000 activities.
-     since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000
      q =
        from(
          a in Activity,
          where: ^old_follower_address in a.recipients,
-         where: a.id > ^since,
          update: [
            set: [
              recipients:
      Repo.update_all(q, [])
    end
  
-   def upgrade_user_from_ap_id(ap_id, async \\ true) do
+   def upgrade_user_from_ap_id(ap_id) do
      with %User{local: false} = user <- User.get_by_ap_id(ap_id),
-          {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
-       already_ap = User.ap_enabled?(user)
-       {:ok, user} =
-         User.upgrade_changeset(user, data)
-         |> Repo.update()
-       if !already_ap do
-         # This could potentially take a long time, do it in the background
-         if async do
-           Task.start(fn ->
-             user_upgrade_task(user)
-           end)
-         else
-           user_upgrade_task(user)
-         end
+          {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
+          already_ap <- User.ap_enabled?(user),
+          {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
+       unless already_ap do
+         PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
        end
  
        {:ok, user}
      else
+       %User{} = user -> {:ok, user}
        e -> e
      end
    end
index bc5b98f1a0594edbd4b5c6bfb3ba4d43d3e6c97f,ccc9da7c667dc03c39a984d164007aad0d6495da..581b9d1ab3e0f232b4e35e954dd5dbeffb4999a6
@@@ -1,9 -1,22 +1,22 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.ActivityPub.Utils do
-   alias Pleroma.{Repo, Web, Object, Activity, User, Notification}
-   alias Pleroma.Web.Router.Helpers
+   alias Ecto.Changeset
+   alias Ecto.UUID
+   alias Pleroma.Activity
+   alias Pleroma.Notification
+   alias Pleroma.Object
+   alias Pleroma.Repo
+   alias Pleroma.User
+   alias Pleroma.Web
+   alias Pleroma.Web.ActivityPub.Visibility
    alias Pleroma.Web.Endpoint
-   alias Ecto.{Changeset, UUID}
+   alias Pleroma.Web.Router.Helpers
    import Ecto.Query
    require Logger
  
    @supported_object_types ["Article", "Note", "Video", "Page"]
      Map.put(params, "actor", get_ap_id(params["actor"]))
    end
  
+   def determine_explicit_mentions(%{"tag" => tag} = _object) when is_list(tag) do
+     tag
+     |> Enum.filter(fn x -> is_map(x) end)
+     |> Enum.filter(fn x -> x["type"] == "Mention" end)
+     |> Enum.map(fn x -> x["href"] end)
+   end
+   def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
+     Map.put(object, "tag", [tag])
+     |> determine_explicit_mentions()
+   end
+   def determine_explicit_mentions(_), do: []
    defp recipient_in_collection(ap_id, coll) when is_binary(coll), do: ap_id == coll
    defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll
    defp recipient_in_collection(_, _), do: false
  
-   def recipient_in_message(ap_id, params) do
+   def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params) do
      cond do
        recipient_in_collection(ap_id, params["to"]) ->
          true
        !params["to"] && !params["cc"] && !params["bto"] && !params["bcc"] ->
          true
  
+       # if the message is sent from somebody the user is following, then assume it
+       # is addressed to the recipient
+       User.following?(recipient, actor) ->
+         true
        true ->
          false
      end
      %{
        "@context" => [
          "https://www.w3.org/ns/activitystreams",
-         "#{Web.base_url()}/schemas/litepub-0.1.jsonld"
+         "#{Web.base_url()}/schemas/litepub-0.1.jsonld",
+         %{
+           "@language" => "und"
+         }
        ]
      }
    end
          _ -> 5
        end
  
-     Pleroma.Web.Federator.enqueue(:publish, activity, priority)
+     Pleroma.Web.Federator.publish(activity, priority)
      :ok
    end
  
    Adds an id and a published data if they aren't there,
    also adds it to an included object
    """
-   def lazy_put_activity_defaults(map) do
-     %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
+   def lazy_put_activity_defaults(map, fake \\ false) do
      map =
-       map
-       |> Map.put_new_lazy("id", &generate_activity_id/0)
-       |> Map.put_new_lazy("published", &make_date/0)
-       |> Map.put_new("context", context)
-       |> Map.put_new("context_id", context_id)
+       unless fake do
+         %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
+         map
+         |> Map.put_new_lazy("id", &generate_activity_id/0)
+         |> Map.put_new_lazy("published", &make_date/0)
+         |> Map.put_new("context", context)
+         |> Map.put_new("context_id", context_id)
+       else
+         map
+         |> Map.put_new("id", "pleroma:fakeid")
+         |> Map.put_new_lazy("published", &make_date/0)
+         |> Map.put_new("context", "pleroma:fakecontext")
+         |> Map.put_new("context_id", -1)
+       end
  
      if is_map(map["object"]) do
-       object = lazy_put_object_defaults(map["object"], map)
+       object = lazy_put_object_defaults(map["object"], map, fake)
        %{map | "object" => object}
      else
        map
    @doc """
    Adds an id and published date if they aren't there.
    """
-   def lazy_put_object_defaults(map, activity \\ %{}) do
+   def lazy_put_object_defaults(map, activity \\ %{}, fake)
+   def lazy_put_object_defaults(map, activity, true = _fake) do
+     map
+     |> Map.put_new_lazy("published", &make_date/0)
+     |> Map.put_new("id", "pleroma:fake_object_id")
+     |> Map.put_new("context", activity["context"])
+     |> Map.put_new("fake", true)
+     |> Map.put_new("context_id", activity["context_id"])
+   end
+   def lazy_put_object_defaults(map, activity, _fake) do
      map
      |> Map.put_new_lazy("id", &generate_object_id/0)
      |> Map.put_new_lazy("published", &make_date/0)
    @doc """
    Inserts a full object if it is contained in an activity.
    """
 -  def insert_full_object(%{"object" => %{"type" => type} = object_data})
 +  def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
        when is_map(object_data) and type in @supported_object_types do
      with {:ok, object} <- Object.create(object_data) do
 -      {:ok, object}
 +      map =
 +        map
 +        |> Map.put("object", object.data["id"])
 +
-       {:ok, map}
++      {:ok, map, object}
      end
    end
  
-   def insert_full_object(map), do: {:ok, map}
 -  def insert_full_object(_), do: {:ok, nil}
++  def insert_full_object(map), do: {:ok, map, nil}
  
    def update_object_in_activities(%{data: %{"id" => id}} = object) do
      # TODO
      # Update activities that already had this. Could be done in a seperate process.
      # Alternatively, just don't do this and fetch the current object each time. Most
      # could probably be taken from cache.
-     relevant_activities = Activity.all_by_object_ap_id(id)
+     relevant_activities = Activity.get_all_create_by_object_ap_id(id)
  
      Enum.map(relevant_activities, fn activity ->
        new_activity_data = activity.data |> Map.put("object", object.data)
      Repo.one(query)
    end
  
-   def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object, activity_id) do
+   @doc """
+   Returns like activities targeting an object
+   """
+   def get_object_likes(%{data: %{"id" => id}}) do
+     query =
+       from(
+         activity in Activity,
+         # this is to use the index
+         where:
+           fragment(
+             "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
+             activity.data,
+             activity.data,
+             ^id
+           ),
+         where: fragment("(?)->>'type' = 'Like'", activity.data)
+       )
+     Repo.all(query)
+   end
+   def make_like_data(
+         %User{ap_id: ap_id} = actor,
+         %{data: %{"actor" => object_actor_id, "id" => id}} = object,
+         activity_id
+       ) do
+     object_actor = User.get_cached_by_ap_id(object_actor_id)
+     to =
+       if Visibility.is_public?(object) do
+         [actor.follower_address, object.data["actor"]]
+       else
+         [object.data["actor"]]
+       end
+     cc =
+       (object.data["to"] ++ (object.data["cc"] || []))
+       |> List.delete(actor.ap_id)
+       |> List.delete(object_actor.follower_address)
      data = %{
        "type" => "Like",
        "actor" => ap_id,
        "object" => id,
-       "to" => [actor.follower_address, object.data["actor"]],
-       "cc" => ["https://www.w3.org/ns/activitystreams#Public"],
+       "to" => to,
+       "cc" => cc,
        "context" => object.data["context"]
      }
  
             |> Map.put("#{property}_count", length(element))
             |> Map.put("#{property}s", element),
           changeset <- Changeset.change(object, data: new_data),
-          {:ok, object} <- Repo.update(changeset),
+          {:ok, object} <- Object.update_and_set_cache(changeset),
           _ <- update_object_in_activities(object) do
        {:ok, object}
      end
    @doc """
    Updates a follow activity's state (for locked accounts).
    """
+   def update_follow_state(
+         %Activity{data: %{"actor" => actor, "object" => object, "state" => "pending"}} = activity,
+         state
+       ) do
+     try do
+       Ecto.Adapters.SQL.query!(
+         Repo,
+         "UPDATE activities SET data = jsonb_set(data, '{state}', $1) WHERE data->>'type' = 'Follow' AND data->>'actor' = $2 AND data->>'object' = $3 AND data->>'state' = 'pending'",
+         [state, actor, object]
+       )
+       activity = Activity.get_by_id(activity.id)
+       {:ok, activity}
+     rescue
+       e ->
+         {:error, e}
+     end
+   end
    def update_follow_state(%Activity{} = activity, state) do
      with new_data <-
             activity.data
    """
    def make_follow_data(
          %User{ap_id: follower_id},
-         %User{ap_id: followed_id} = followed,
+         %User{ap_id: followed_id} = _followed,
          activity_id
        ) do
      data = %{
              activity.data
            ),
          where: activity.actor == ^follower_id,
+         # this is to use the index
          where:
            fragment(
-             "? @> ?",
+             "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
+             activity.data,
              activity.data,
-             ^%{object: followed_id}
+             ^followed_id
            ),
-         order_by: [desc: :id],
+         order_by: [fragment("? desc nulls last", activity.id)],
          limit: 1
        )
  
    """
    # for relayed messages, we only want to send to subscribers
    def make_announce_data(
-         %User{ap_id: ap_id, nickname: nil} = user,
+         %User{ap_id: ap_id} = user,
          %Object{data: %{"id" => id}} = object,
-         activity_id
+         activity_id,
+         false
        ) do
      data = %{
        "type" => "Announce",
    def make_announce_data(
          %User{ap_id: ap_id} = user,
          %Object{data: %{"id" => id}} = object,
-         activity_id
+         activity_id,
+         true
        ) do
      data = %{
        "type" => "Announce",
              activity.data
            ),
          where: activity.actor == ^blocker_id,
+         # this is to use the index
          where:
            fragment(
-             "? @> ?",
+             "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
+             activity.data,
              activity.data,
-             ^%{object: blocked_id}
+             ^blocked_id
            ),
-         order_by: [desc: :id],
+         order_by: [fragment("? desc nulls last", activity.id)],
          limit: 1
        )
  
      }
      |> Map.merge(additional)
    end
+   #### Flag-related helpers
+   def make_flag_data(params, additional) do
+     status_ap_ids =
+       Enum.map(params.statuses || [], fn
+         %Activity{} = act -> act.data["id"]
+         act when is_map(act) -> act["id"]
+         act when is_binary(act) -> act
+       end)
+     object = [params.account.ap_id] ++ status_ap_ids
+     %{
+       "type" => "Flag",
+       "actor" => params.actor.ap_id,
+       "content" => params.content,
+       "object" => object,
+       "context" => params.context
+     }
+     |> Map.merge(additional)
+   end
+   @doc """
+   Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
+   the first one to `pages_left` pages.
+   If the amount of pages is higher than the collection has, it returns whatever was there.
+   """
+   def fetch_ordered_collection(from, pages_left, acc \\ []) do
+     with {:ok, response} <- Tesla.get(from),
+          {:ok, collection} <- Poison.decode(response.body) do
+       case collection["type"] do
+         "OrderedCollection" ->
+           # If we've encountered the OrderedCollection and not the page,
+           # just call the same function on the page address
+           fetch_ordered_collection(collection["first"], pages_left)
+         "OrderedCollectionPage" ->
+           if pages_left > 0 do
+             # There are still more pages
+             if Map.has_key?(collection, "next") do
+               # There are still more pages, go deeper saving what we have into the accumulator
+               fetch_ordered_collection(
+                 collection["next"],
+                 pages_left - 1,
+                 acc ++ collection["orderedItems"]
+               )
+             else
+               # No more pages left, just return whatever we already have
+               acc ++ collection["orderedItems"]
+             end
+           else
+             # Got the amount of pages needed, add them all to the accumulator
+             acc ++ collection["orderedItems"]
+           end
+         _ ->
+           {:error, "Not an OrderedCollection or OrderedCollectionPage"}
+       end
+     end
+   end
  end
index c83f8a6a9427dc552d13c312de573b2d280e93eb,74babdf147c84da900f6ac4bfdd041b9b69e1ed8..9c3daac2c994c49d323f35c60f419fce1e031024
@@@ -1,14 -1,73 +1,73 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.CommonAPI do
-   alias Pleroma.{User, Repo, Activity, Object}
-   alias Pleroma.Web.ActivityPub.ActivityPub
+   alias Pleroma.Activity
    alias Pleroma.Formatter
+   alias Pleroma.Object
+   alias Pleroma.ThreadMute
+   alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ActivityPub
+   alias Pleroma.Web.ActivityPub.Utils
  
    import Pleroma.Web.CommonAPI.Utils
  
+   def follow(follower, followed) do
+     with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
+          {:ok, activity} <- ActivityPub.follow(follower, followed),
+          {:ok, follower, followed} <-
+            User.wait_and_refresh(
+              Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
+              follower,
+              followed
+            ) do
+       {:ok, follower, followed, activity}
+     end
+   end
+   def unfollow(follower, unfollowed) do
+     with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
+          {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do
+       {:ok, follower}
+     end
+   end
+   def accept_follow_request(follower, followed) do
+     with {:ok, follower} <- User.maybe_follow(follower, followed),
+          %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
+          {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
+          {:ok, _activity} <-
+            ActivityPub.accept(%{
+              to: [follower.ap_id],
+              actor: followed,
+              object: follow_activity.data["id"],
+              type: "Accept"
+            }) do
+       {:ok, follower}
+     end
+   end
+   def reject_follow_request(follower, followed) do
+     with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
+          {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
+          {:ok, _activity} <-
+            ActivityPub.reject(%{
+              to: [follower.ap_id],
+              actor: followed,
+              object: follow_activity.data["id"],
+              type: "Reject"
+            }) do
+       {:ok, follower}
+     end
+   end
    def delete(activity_id, user) do
-     with %Activity{data: %{"object" => object_id}} <- Repo.get(Activity, activity_id),
-          %Object{} = object <- Object.normalize(object_id),
-          true <- user.info.is_moderator || user.ap_id == object.data["actor"],
+     with %Activity{data: %{"object" => _}} = activity <-
+            Activity.get_by_id_with_object(activity_id),
+          %Object{} = object <- Object.normalize(activity),
+          true <- User.superuser?(user) || user.ap_id == object.data["actor"],
+          {:ok, _} <- unpin(activity_id, user),
           {:ok, delete} <- ActivityPub.delete(object) do
        {:ok, delete}
      end
@@@ -16,7 -75,8 +75,8 @@@
  
    def repeat(id_or_ap_id, user) do
      with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
-          object <- Object.normalize(activity.data["object"]) do
+          object <- Object.normalize(activity),
+          nil <- Utils.get_existing_announce(user.ap_id, object) do
        ActivityPub.announce(user, object)
      else
        _ ->
@@@ -26,7 -86,7 +86,7 @@@
  
    def unrepeat(id_or_ap_id, user) do
      with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
-          object <- Object.normalize(activity.data["object"]) do
+          object <- Object.normalize(activity) do
        ActivityPub.unannounce(user, object)
      else
        _ ->
@@@ -36,7 -96,8 +96,8 @@@
  
    def favorite(id_or_ap_id, user) do
      with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
-          object <- Object.normalize(activity.data["object"]) do
+          object <- Object.normalize(activity),
+          nil <- Utils.get_existing_like(user.ap_id, object) do
        ActivityPub.like(user, object)
      else
        _ ->
  
    def unfavorite(id_or_ap_id, user) do
      with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
-          object <- Object.normalize(activity.data["object"]) do
+          object <- Object.normalize(activity) do
        ActivityPub.unlike(user, object)
      else
        _ ->
        nil ->
          "public"
  
-       inReplyTo ->
+       in_reply_to ->
 -        Pleroma.Web.MastodonAPI.StatusView.get_visibility(in_reply_to.data["object"])
 +        # XXX: these heuristics should be moved out of MastodonAPI.
-         with %Object{} = object <- Object.normalize(inReplyTo.data["object"]) do
++        with %Object{} = object <- Object.normalize(in_reply_to) do
 +          Pleroma.Web.MastodonAPI.StatusView.get_visibility(object.data)
 +        end
      end
    end
  
    def get_visibility(_), do: "public"
  
-   defp get_content_type(content_type) do
-     if Enum.member?(Pleroma.Config.get([:instance, :allowed_post_formats]), content_type) do
-       content_type
-     else
-       "text/plain"
-     end
-   end
    def post(user, %{"status" => status} = data) do
      visibility = get_visibility(data)
      limit = Pleroma.Config.get([:instance, :limit])
  
      with status <- String.trim(status),
-          attachments <- attachments_from_ids(data["media_ids"]),
-          mentions <- Formatter.parse_mentions(status),
-          inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]),
-          {to, cc} <- to_for_user_and_mentions(user, mentions, inReplyTo, visibility),
-          tags <- Formatter.parse_tags(status, data),
-          content_html <-
+          attachments <- attachments_from_ids(data),
+          in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
+          {content_html, mentions, tags} <-
             make_content_html(
               status,
-              mentions,
               attachments,
-              tags,
-              get_content_type(data["content_type"]),
-              data["no_attachment_links"]
+              data,
+              visibility
             ),
-          context <- make_context(inReplyTo),
+          {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
+          context <- make_context(in_reply_to),
           cw <- data["spoiler_text"],
           full_payload <- String.trim(status <> (data["spoiler_text"] || "")),
           length when length in 1..limit <- String.length(full_payload),
               context,
               content_html,
               attachments,
-              inReplyTo,
+              in_reply_to,
               tags,
               cw,
               cc
             Map.put(
               object,
               "emoji",
-              Formatter.get_emoji(status)
-              |> Enum.reduce(%{}, fn {name, file}, acc ->
+              (Formatter.get_emoji(status) ++ Formatter.get_emoji(data["spoiler_text"]))
+              |> Enum.reduce(%{}, fn {name, file, _}, acc ->
                 Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
               end)
             ) do
        res =
-         ActivityPub.create(%{
-           to: to,
-           actor: user,
-           context: context,
-           object: object,
-           additional: %{"cc" => cc}
-         })
+         ActivityPub.create(
+           %{
+             to: to,
+             actor: user,
+             context: context,
+             object: object,
+             additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
+           },
+           Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
+         )
  
        res
      end
        object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
      })
    end
+   def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
+     with %Activity{
+            actor: ^user_ap_id,
+            data: %{
+              "type" => "Create",
+              "object" => %{
+                "to" => object_to,
+                "type" => "Note"
+              }
+            }
+          } = activity <- get_by_id_or_ap_id(id_or_ap_id),
+          true <- Enum.member?(object_to, "https://www.w3.org/ns/activitystreams#Public"),
+          %{valid?: true} = info_changeset <-
+            Pleroma.User.Info.add_pinnned_activity(user.info, activity),
+          changeset <-
+            Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
+          {:ok, _user} <- User.update_and_set_cache(changeset) do
+       {:ok, activity}
+     else
+       %{errors: [pinned_activities: {err, _}]} ->
+         {:error, err}
+       _ ->
+         {:error, "Could not pin"}
+     end
+   end
+   def unpin(id_or_ap_id, user) do
+     with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
+          %{valid?: true} = info_changeset <-
+            Pleroma.User.Info.remove_pinnned_activity(user.info, activity),
+          changeset <-
+            Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
+          {:ok, _user} <- User.update_and_set_cache(changeset) do
+       {:ok, activity}
+     else
+       %{errors: [pinned_activities: {err, _}]} ->
+         {:error, err}
+       _ ->
+         {:error, "Could not unpin"}
+     end
+   end
+   def add_mute(user, activity) do
+     with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
+       {:ok, activity}
+     else
+       {:error, _} -> {:error, "conversation is already muted"}
+     end
+   end
+   def remove_mute(user, activity) do
+     ThreadMute.remove_mute(user.id, activity.data["context"])
+     {:ok, activity}
+   end
+   def thread_muted?(%{id: nil} = _user, _activity), do: false
+   def thread_muted?(user, activity) do
+     with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
+       false
+     else
+       _ -> true
+     end
+   end
+   def report(user, data) do
+     with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
+          {:account, %User{} = account} <- {:account, User.get_by_id(account_id)},
+          {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
+          {:ok, statuses} <- get_report_statuses(account, data),
+          {:ok, activity} <-
+            ActivityPub.flag(%{
+              context: Utils.generate_context_id(),
+              actor: user,
+              account: account,
+              statuses: statuses,
+              content: content_html,
+              forward: data["forward"] || false
+            }) do
+       {:ok, activity}
+     else
+       {:error, err} -> {:error, err}
+       {:account_id, %{}} -> {:error, "Valid `account_id` required"}
+       {:account, nil} -> {:error, "Account not found"}
+     end
+   end
+   def hide_reblogs(user, muted) do
+     ap_id = muted.ap_id
+     if ap_id not in user.info.muted_reblogs do
+       info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
+       changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
+       User.update_and_set_cache(changeset)
+     end
+   end
+   def show_reblogs(user, muted) do
+     ap_id = muted.ap_id
+     if ap_id in user.info.muted_reblogs do
+       info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
+       changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
+       User.update_and_set_cache(changeset)
+     end
+   end
  end
index ec66452c23eef3e4d521a9aac8c24cbf6060d809,185292878b6e94f72ee9b5fbeb4730f63185c8b5..7781f16350ab105d5c8ec27e71226224b33f2859
@@@ -1,38 -1,66 +1,66 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.CommonAPI.Utils do
-   alias Pleroma.{Repo, Object, Formatter, Activity}
+   alias Calendar.Strftime
+   alias Comeonin.Pbkdf2
+   alias Pleroma.Activity
+   alias Pleroma.Config
+   alias Pleroma.Formatter
+   alias Pleroma.Object
+   alias Pleroma.Repo
+   alias Pleroma.User
    alias Pleroma.Web.ActivityPub.Utils
+   alias Pleroma.Web.ActivityPub.Visibility
    alias Pleroma.Web.Endpoint
    alias Pleroma.Web.MediaProxy
-   alias Pleroma.User
-   alias Calendar.Strftime
-   alias Comeonin.Pbkdf2
+   require Logger
  
    # This is a hack for twidere.
    def get_by_id_or_ap_id(id) do
-     activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id)
+     activity =
+       Activity.get_by_id_with_object(id) || Activity.get_create_by_object_ap_id_with_object(id)
  
      activity &&
        if activity.data["type"] == "Create" do
          activity
        else
-         Activity.get_create_activity_by_object_ap_id(activity.data["object"])
+         Activity.get_create_by_object_ap_id_with_object(activity.data["object"])
        end
    end
  
    def get_replied_to_activity(""), do: nil
  
    def get_replied_to_activity(id) when not is_nil(id) do
-     Repo.get(Activity, id)
+     Activity.get_by_id(id)
    end
  
    def get_replied_to_activity(_), do: nil
  
-   def attachments_from_ids(ids) do
+   def attachments_from_ids(data) do
+     if Map.has_key?(data, "descriptions") do
+       attachments_from_ids_descs(data["media_ids"], data["descriptions"])
+     else
+       attachments_from_ids_no_descs(data["media_ids"])
+     end
+   end
+   def attachments_from_ids_no_descs(ids) do
      Enum.map(ids || [], fn media_id ->
        Repo.get(Object, media_id).data
      end)
    end
  
+   def attachments_from_ids_descs(ids, descs_str) do
+     {_, descs} = Jason.decode(descs_str)
+     Enum.map(ids || [], fn media_id ->
+       Map.put(Repo.get(Object, media_id).data, "name", descs[media_id])
+     end)
+   end
    def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
      mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
  
  
    def make_content_html(
          status,
-         mentions,
          attachments,
-         tags,
-         content_type,
-         no_attachment_links \\ false
+         data,
+         visibility
        ) do
+     no_attachment_links =
+       data
+       |> Map.get("no_attachment_links", Config.get([:instance, :no_attachment_links]))
+       |> Kernel.in([true, "true"])
+     content_type = get_content_type(data["content_type"])
+     options =
+       if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
+         [safe_mention: true]
+       else
+         []
+       end
      status
-     |> format_input(mentions, tags, content_type)
+     |> format_input(content_type, options)
      |> maybe_add_attachments(attachments, no_attachment_links)
+     |> maybe_add_nsfw_tag(data)
+   end
+   defp get_content_type(content_type) do
+     if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do
+       content_type
+     else
+       "text/plain"
+     end
    end
  
+   defp maybe_add_nsfw_tag({text, mentions, tags}, %{"sensitive" => sensitive})
+        when sensitive in [true, "True", "true", "1"] do
+     {text, mentions, [{"#nsfw", "nsfw"} | tags]}
+   end
+   defp maybe_add_nsfw_tag(data, _), do: data
    def make_context(%Activity{data: %{"context" => context}}), do: context
    def make_context(_), do: Utils.generate_context_id()
  
-   def maybe_add_attachments(text, _attachments, _no_links = true), do: text
+   def maybe_add_attachments(parsed, _attachments, true = _no_links), do: parsed
  
-   def maybe_add_attachments(text, attachments, _no_links) do
-     add_attachments(text, attachments)
+   def maybe_add_attachments({text, mentions, tags}, attachments, _no_links) do
+     text = add_attachments(text, attachments)
+     {text, mentions, tags}
    end
  
    def add_attachments(text, attachments) do
      Enum.join([text | attachment_text], "<br>")
    end
  
-   def format_input(text, mentions, tags, "text/plain") do
+   def format_input(text, format, options \\ [])
+   @doc """
+   Formatting text to plain text.
+   """
+   def format_input(text, "text/plain", options) do
      text
      |> Formatter.html_escape("text/plain")
-     |> String.replace(~r/\r?\n/, "<br>")
-     |> (&{[], &1}).()
-     |> Formatter.add_links()
-     |> Formatter.add_user_links(mentions)
-     |> Formatter.add_hashtag_links(tags)
-     |> Formatter.finalize()
+     |> Formatter.linkify(options)
+     |> (fn {text, mentions, tags} ->
+           {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
+         end).()
    end
  
-   def format_input(text, mentions, tags, "text/html") do
+   @doc """
+   Formatting text to html.
+   """
+   def format_input(text, "text/html", options) do
      text
      |> Formatter.html_escape("text/html")
-     |> String.replace(~r/\r?\n/, "<br>")
-     |> (&{[], &1}).()
-     |> Formatter.add_user_links(mentions)
-     |> Formatter.finalize()
+     |> Formatter.linkify(options)
    end
  
-   def format_input(text, mentions, tags, "text/markdown") do
+   @doc """
+   Formatting text to markdown.
+   """
+   def format_input(text, "text/markdown", options) do
      text
+     |> Formatter.mentions_escape(options)
      |> Earmark.as_html!()
+     |> Formatter.linkify(options)
      |> Formatter.html_escape("text/html")
-     |> String.replace(~r/\r?\n/, "")
-     |> (&{[], &1}).()
-     |> Formatter.add_user_links(mentions)
-     |> Formatter.add_hashtag_links(tags)
-     |> Formatter.finalize()
-   end
-   def add_tag_links(text, tags) do
-     tags =
-       tags
-       |> Enum.sort_by(fn {tag, _} -> -String.length(tag) end)
-     Enum.reduce(tags, text, fn {full, tag}, text ->
-       url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>"
-       String.replace(text, full, url)
-     end)
    end
  
    def make_note_data(
      }
  
      if inReplyTo do
 +      inReplyToObject = Object.normalize(inReplyTo.data["object"])
 +
        object
 -      |> Map.put("inReplyTo", inReplyTo.data["object"]["id"])
 +      |> Map.put("inReplyTo", inReplyToObject.data["id"])
-       |> Map.put("inReplyToStatusId", inReplyTo.id)
      else
        object
      end
      Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")
    end
  
-   def date_to_asctime(date) do
-     with {:ok, date, _offset} <- date |> DateTime.from_iso8601() do
+   def date_to_asctime(date) when is_binary(date) do
+     with {:ok, date, _offset} <- DateTime.from_iso8601(date) do
        format_asctime(date)
      else
        _e ->
+         Logger.warn("Date #{date} in wrong format, must be ISO 8601")
          ""
      end
    end
  
+   def date_to_asctime(date) do
+     Logger.warn("Date #{date} in wrong format, must be ISO 8601")
+     ""
+   end
    def to_masto_date(%NaiveDateTime{} = date) do
      date
      |> NaiveDateTime.to_iso8601()
    end
  
    def confirm_current_password(user, password) do
-     with %User{local: true} = db_user <- Repo.get(User, user.id),
+     with %User{local: true} = db_user <- User.get_by_id(user.id),
           true <- Pbkdf2.checkpw(password, db_user.password_hash) do
        {:ok, db_user}
      else
      end
    end
  
-   def emoji_from_profile(%{info: info} = user) do
+   def emoji_from_profile(%{info: _info} = user) do
      (Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
-     |> Enum.map(fn {shortcode, url} ->
+     |> Enum.map(fn {shortcode, url, _} ->
        %{
          "type" => "Emoji",
          "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
        }
      end)
    end
+   def maybe_notify_to_recipients(
+         recipients,
+         %Activity{data: %{"to" => to, "type" => _type}} = _activity
+       ) do
+     recipients ++ to
+   end
+   def maybe_notify_mentioned_recipients(
+         recipients,
+         %Activity{data: %{"to" => _to, "type" => type} = data} = activity
+       )
+       when type == "Create" do
+     object = Object.normalize(activity)
+     object_data =
+       cond do
+         !is_nil(object) ->
+           object.data
+         is_map(data["object"]) ->
+           data["object"]
+         true ->
+           %{}
+       end
+     tagged_mentions = maybe_extract_mentions(object_data)
+     recipients ++ tagged_mentions
+   end
+   def maybe_notify_mentioned_recipients(recipients, _), do: recipients
+   def maybe_notify_subscribers(
+         recipients,
+         %Activity{data: %{"actor" => actor, "type" => type}} = activity
+       )
+       when type == "Create" do
+     with %User{} = user <- User.get_cached_by_ap_id(actor) do
+       subscriber_ids =
+         user
+         |> User.subscribers()
+         |> Enum.filter(&Visibility.visible_for_user?(activity, &1))
+         |> Enum.map(& &1.ap_id)
+       recipients ++ subscriber_ids
+     end
+   end
+   def maybe_notify_subscribers(recipients, _), do: recipients
+   def maybe_extract_mentions(%{"tag" => tag}) do
+     tag
+     |> Enum.filter(fn x -> is_map(x) end)
+     |> Enum.filter(fn x -> x["type"] == "Mention" end)
+     |> Enum.map(fn x -> x["href"] end)
+   end
+   def maybe_extract_mentions(_), do: []
+   def make_report_content_html(nil), do: {:ok, {nil, [], []}}
+   def make_report_content_html(comment) do
+     max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)
+     if String.length(comment) <= max_size do
+       {:ok, format_input(comment, "text/plain")}
+     else
+       {:error, "Comment must be up to #{max_size} characters"}
+     end
+   end
+   def get_report_statuses(%User{ap_id: actor}, %{"status_ids" => status_ids}) do
+     {:ok, Activity.all_by_actor_and_id(actor, status_ids)}
+   end
+   def get_report_statuses(_, _), do: {:ok, nil}
+   # DEPRECATED mostly, context objects are now created at insertion time.
+   def context_to_conversation_id(context) do
+     with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
+       id
+     else
+       _e ->
+         changeset = Object.context_mapping(context)
+         case Repo.insert(changeset) do
+           {:ok, %{id: id}} ->
+             id
+           # This should be solved by an upsert, but it seems ecto
+           # has problems accessing the constraint inside the jsonb.
+           {:error, _} ->
+             Object.get_cached_by_ap_id(context).id
+         end
+     end
+   end
+   def conversation_id_to_context(id) do
+     with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
+       context
+     else
+       _e ->
+         {:error, "No such conversation"}
+     end
+   end
  end
index 0644f8d0a4536c45b7c74bb9ed58a0c1a34c8074,c47328e13a4e096182ffc97ae1688f7c559eb099..a1f6373a45b0b2d9a3a58249ef12976516cc5d3c
@@@ -1,55 -1,84 +1,85 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.Federator do
-   use GenServer
-   alias Pleroma.User
    alias Pleroma.Activity
-   alias Pleroma.Object.Containment
-   alias Pleroma.Web.{WebFinger, Websub}
-   alias Pleroma.Web.Federator.RetryQueue
+   alias Pleroma.User
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Relay
    alias Pleroma.Web.ActivityPub.Transmogrifier
    alias Pleroma.Web.ActivityPub.Utils
+   alias Pleroma.Web.ActivityPub.Visibility
+   alias Pleroma.Web.Federator.RetryQueue
    alias Pleroma.Web.OStatus
++  alias Pleroma.Object.Containment
+   alias Pleroma.Web.Salmon
+   alias Pleroma.Web.WebFinger
+   alias Pleroma.Web.Websub
    require Logger
  
    @websub Application.get_env(:pleroma, :websub)
    @ostatus Application.get_env(:pleroma, :ostatus)
-   @httpoison Application.get_env(:pleroma, :httpoison)
-   @max_jobs 20
  
-   def init(args) do
-     {:ok, args}
+   def init do
+     # 1 minute
+     Process.sleep(1000 * 60)
+     refresh_subscriptions()
    end
  
-   def start_link do
-     spawn(fn ->
-       # 1 minute
-       Process.sleep(1000 * 60 * 1)
-       enqueue(:refresh_subscriptions, nil)
-     end)
+   # Client API
+   def incoming_doc(doc) do
+     PleromaJobQueue.enqueue(:federator_incoming, __MODULE__, [:incoming_doc, doc])
+   end
+   def incoming_ap_doc(params) do
+     PleromaJobQueue.enqueue(:federator_incoming, __MODULE__, [:incoming_ap_doc, params])
+   end
+   def publish(activity, priority \\ 1) do
+     PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority)
+   end
+   def publish_single_ap(params) do
+     PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params])
+   end
+   def publish_single_websub(websub) do
+     PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub])
+   end
+   def verify_websub(websub) do
+     PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub])
+   end
+   def request_subscription(sub) do
+     PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:request_subscription, sub])
+   end
+   def refresh_subscriptions do
+     PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])
+   end
  
-     GenServer.start_link(
-       __MODULE__,
-       %{
-         in: {:sets.new(), []},
-         out: {:sets.new(), []}
-       },
-       name: __MODULE__
-     )
+   def publish_single_salmon(params) do
+     PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params])
    end
  
-   def handle(:refresh_subscriptions, _) do
+   # Job Worker Callbacks
+   def perform(:refresh_subscriptions) do
      Logger.debug("Federator running refresh subscriptions")
      Websub.refresh_subscriptions()
  
      spawn(fn ->
        # 6 hours
        Process.sleep(1000 * 60 * 60 * 6)
-       enqueue(:refresh_subscriptions, nil)
+       refresh_subscriptions()
      end)
    end
  
-   def handle(:request_subscription, websub) do
+   def perform(:request_subscription, websub) do
      Logger.debug("Refreshing #{websub.topic}")
  
      with {:ok, websub} <- Websub.request_subscription(websub) do
      end
    end
  
-   def handle(:publish, activity) do
+   def perform(:publish, activity) do
      Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
  
      with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do
        {:ok, actor} = WebFinger.ensure_keys_present(actor)
  
-       if ActivityPub.is_public?(activity) do
+       if Visibility.is_public?(activity) do
          if OStatus.is_representable?(activity) do
            Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end)
            Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
      end
    end
  
-   def handle(:verify_websub, websub) do
+   def perform(:verify_websub, websub) do
      Logger.debug(fn ->
        "Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})"
      end)
      @websub.verify(websub)
    end
  
-   def handle(:incoming_doc, doc) do
+   def perform(:incoming_doc, doc) do
      Logger.info("Got document, trying to parse")
      @ostatus.handle_incoming(doc)
    end
  
-   def handle(:incoming_ap_doc, params) do
+   def perform(:incoming_ap_doc, params) do
      Logger.info("Handling incoming AP activity")
  
      params = Utils.normalize_params(params)
      # actor shouldn't be acting on objects outside their own AP server.
      with {:ok, _user} <- ap_enabled_actor(params["actor"]),
           nil <- Activity.normalize(params["id"]),
 -         :ok <- Transmogrifier.contain_origin_from_id(params["actor"], params),
 +         :ok <- Containment.contain_origin_from_id(params["actor"], params),
           {:ok, activity} <- Transmogrifier.handle_incoming(params) do
        {:ok, activity}
      else
      end
    end
  
-   def handle(:publish_single_ap, params) do
+   def perform(:publish_single_salmon, params) do
+     Salmon.send_to_user(params)
+   end
+   def perform(:publish_single_ap, params) do
      case ActivityPub.publish_one(params) do
        {:ok, _} ->
          :ok
      end
    end
  
-   def handle(
+   def perform(
          :publish_single_websub,
-         %{xml: xml, topic: topic, callback: callback, secret: secret} = params
+         %{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params
        ) do
      case Websub.publish_one(params) do
        {:ok, _} ->
      end
    end
  
-   def handle(type, _) do
+   def perform(type, _) do
      Logger.debug(fn -> "Unknown task: #{type}" end)
      {:error, "Don't know what to do with this"}
    end
  
-   if Mix.env() == :test do
-     def enqueue(type, payload, priority \\ 1) do
-       if Pleroma.Config.get([:instance, :federating]) do
-         handle(type, payload)
-       end
-     end
-   else
-     def enqueue(type, payload, priority \\ 1) do
-       if Pleroma.Config.get([:instance, :federating]) do
-         GenServer.cast(__MODULE__, {:enqueue, type, payload, priority})
-       end
-     end
-   end
-   def maybe_start_job(running_jobs, queue) do
-     if :sets.size(running_jobs) < @max_jobs && queue != [] do
-       {{type, payload}, queue} = queue_pop(queue)
-       {:ok, pid} = Task.start(fn -> handle(type, payload) end)
-       mref = Process.monitor(pid)
-       {:sets.add_element(mref, running_jobs), queue}
-     else
-       {running_jobs, queue}
-     end
-   end
-   def handle_cast({:enqueue, type, payload, _priority}, state)
-       when type in [:incoming_doc, :incoming_ap_doc] do
-     %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
-     i_queue = enqueue_sorted(i_queue, {type, payload}, 1)
-     {i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
-     {:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
-   end
-   def handle_cast({:enqueue, type, payload, _priority}, state) do
-     %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
-     o_queue = enqueue_sorted(o_queue, {type, payload}, 1)
-     {o_running_jobs, o_queue} = maybe_start_job(o_running_jobs, o_queue)
-     {:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
-   end
-   def handle_cast(m, state) do
-     IO.inspect("Unknown: #{inspect(m)}, #{inspect(state)}")
-     {:noreply, state}
-   end
-   def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
-     %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
-     i_running_jobs = :sets.del_element(ref, i_running_jobs)
-     o_running_jobs = :sets.del_element(ref, o_running_jobs)
-     {i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
-     {o_running_jobs, o_queue} = maybe_start_job(o_running_jobs, o_queue)
-     {:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
-   end
-   def enqueue_sorted(queue, element, priority) do
-     [%{item: element, priority: priority} | queue]
-     |> Enum.sort_by(fn %{priority: priority} -> priority end)
-   end
-   def queue_pop([%{item: element} | queue]) do
-     {element, queue}
-   end
    def ap_enabled_actor(id) do
      user = User.get_by_ap_id(id)
  
index 71390be0df0dfc5a1d968925c872ff92a253e154,63fadce3811ceea8a681860888d49ad8f8f4cb8d..24a2d4cb9cd8db210d8e7918174b1a1adf8aa38b
@@@ -1,35 -1,64 +1,64 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
    use Pleroma.Web, :controller
-   alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
 -
 +  alias Pleroma.Object.Fetcher
+   alias Ecto.Changeset
+   alias Pleroma.Activity
+   alias Pleroma.Config
+   alias Pleroma.Filter
+   alias Pleroma.Notification
+   alias Pleroma.Object
+   alias Pleroma.Pagination
+   alias Pleroma.Repo
+   alias Pleroma.ScheduledActivity
+   alias Pleroma.Stats
+   alias Pleroma.User
    alias Pleroma.Web
-   alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView, FilterView}
    alias Pleroma.Web.ActivityPub.ActivityPub
-   alias Pleroma.Web.ActivityPub.Utils
+   alias Pleroma.Web.ActivityPub.Visibility
    alias Pleroma.Web.CommonAPI
-   alias Pleroma.Web.OAuth.{Authorization, Token, App}
+   alias Pleroma.Web.MastodonAPI.AccountView
+   alias Pleroma.Web.MastodonAPI.AppView
+   alias Pleroma.Web.MastodonAPI.FilterView
+   alias Pleroma.Web.MastodonAPI.ListView
+   alias Pleroma.Web.MastodonAPI.MastodonAPI
+   alias Pleroma.Web.MastodonAPI.MastodonView
+   alias Pleroma.Web.MastodonAPI.NotificationView
+   alias Pleroma.Web.MastodonAPI.ReportView
+   alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+   alias Pleroma.Web.MastodonAPI.StatusView
    alias Pleroma.Web.MediaProxy
-   alias Comeonin.Pbkdf2
+   alias Pleroma.Web.OAuth.App
+   alias Pleroma.Web.OAuth.Authorization
+   alias Pleroma.Web.OAuth.Token
+   import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
    import Ecto.Query
    require Logger
  
    @httpoison Application.get_env(:pleroma, :httpoison)
+   @local_mastodon_name "Mastodon-Local"
  
    action_fallback(:errors)
  
    def create_app(conn, params) do
-     with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
-          {:ok, app} <- Repo.insert(cs) |> IO.inspect() do
-       res = %{
-         id: app.id |> to_string,
-         name: app.client_name,
-         client_id: app.client_id,
-         client_secret: app.client_secret,
-         redirect_uri: app.redirect_uris,
-         website: app.website
-       }
+     scopes = oauth_scopes(params, ["read"])
  
-       json(conn, res)
+     app_attrs =
+       params
+       |> Map.drop(["scope", "scopes"])
+       |> Map.put("scopes", scopes)
+     with cs <- App.register_changeset(%App{}, app_attrs),
+          false <- cs.changes[:client_name] == @local_mastodon_name,
+          {:ok, app} <- Repo.insert(cs) do
+       conn
+       |> put_view(AppView)
+       |> render("show.json", %{app: app})
      end
    end
  
      json(conn, account)
    end
  
-   def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
-     with %User{} = user <- Repo.get(User, id) do
+   def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
+     with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
+       conn
+       |> put_view(AppView)
+       |> render("short.json", %{app: app})
+     end
+   end
+   def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
+     with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
+          true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
        account = AccountView.render("account.json", %{user: user, for: for_user})
        json(conn, account)
      else
    @mastodon_api_level "2.5.0"
  
    def masto_instance(conn, _params) do
-     instance = Pleroma.Config.get(:instance)
+     instance = Config.get(:instance)
  
      response = %{
        uri: Web.base_url(),
        version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
        email: Keyword.get(instance, :email),
        urls: %{
-         streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
+         streaming_api: Pleroma.Web.Endpoint.websocket_url()
        },
        stats: Stats.get_stats(),
        thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
+       languages: ["en"],
+       registrations: Pleroma.Config.get([:instance, :registrations_open]),
+       # Extra (not present in Mastodon):
        max_toot_chars: Keyword.get(instance, :limit)
      }
  
  
    defp mastodonized_emoji do
      Pleroma.Emoji.get_all()
-     |> Enum.map(fn {shortcode, relative_url} ->
+     |> Enum.map(fn {shortcode, relative_url, tags} ->
        url = to_string(URI.merge(Web.base_url(), relative_url))
  
        %{
          "shortcode" => shortcode,
          "static_url" => url,
          "visible_in_picker" => true,
-         "url" => url
+         "url" => url,
+         "tags" => String.split(tags, ",")
        }
      end)
    end
    end
  
    defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
+     params =
+       conn.params
+       |> Map.drop(["since_id", "max_id", "min_id"])
+       |> Map.merge(params)
      last = List.last(activities)
-     first = List.first(activities)
  
      if last do
-       min = last.id
-       max = first.id
+       max_id = last.id
+       limit =
+         params
+         |> Map.get("limit", "20")
+         |> String.to_integer()
+       min_id =
+         if length(activities) <= limit do
+           activities
+           |> List.first()
+           |> Map.get(:id)
+         else
+           activities
+           |> Enum.at(limit * -1)
+           |> Map.get(:id)
+         end
  
        {next_url, prev_url} =
          if param do
                Pleroma.Web.Endpoint,
                method,
                param,
-               Map.merge(params, %{max_id: min})
+               Map.merge(params, %{max_id: max_id})
              ),
              mastodon_api_url(
                Pleroma.Web.Endpoint,
                method,
                param,
-               Map.merge(params, %{since_id: max})
+               Map.merge(params, %{min_id: min_id})
              )
            }
          else
              mastodon_api_url(
                Pleroma.Web.Endpoint,
                method,
-               Map.merge(params, %{max_id: min})
+               Map.merge(params, %{max_id: max_id})
              ),
              mastodon_api_url(
                Pleroma.Web.Endpoint,
                method,
-               Map.merge(params, %{since_id: max})
+               Map.merge(params, %{min_id: min_id})
              )
            }
          end
        params
        |> Map.put("type", ["Create", "Announce"])
        |> Map.put("blocking_user", user)
+       |> Map.put("muting_user", user)
        |> Map.put("user", user)
  
      activities =
-       ActivityPub.fetch_activities([user.ap_id | user.following], params)
+       [user.ap_id | user.following]
+       |> ActivityPub.fetch_activities(params)
        |> ActivityPub.contain_timeline(user)
        |> Enum.reverse()
  
      conn
      |> add_link_headers(:home_timeline, activities)
-     |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
+     |> put_view(StatusView)
+     |> render("index.json", %{activities: activities, for: user, as: :activity})
    end
  
    def public_timeline(%{assigns: %{user: user}} = conn, params) do
      local_only = params["local"] in [true, "True", "true", "1"]
  
-     params =
+     activities =
        params
        |> Map.put("type", ["Create", "Announce"])
        |> Map.put("local_only", local_only)
        |> Map.put("blocking_user", user)
-     activities =
-       ActivityPub.fetch_public_activities(params)
+       |> Map.put("muting_user", user)
+       |> ActivityPub.fetch_public_activities()
        |> Enum.reverse()
  
      conn
      |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
-     |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
+     |> put_view(StatusView)
+     |> render("index.json", %{activities: activities, for: user, as: :activity})
    end
  
    def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
-     with %User{} = user <- Repo.get(User, params["id"]) do
-       # Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
-       activities =
-         if params["pinned"] == "true" do
-           []
-         else
-           ActivityPub.fetch_user_activities(user, reading_user, params)
-         end
+     with %User{} = user <- User.get_by_id(params["id"]) do
+       activities = ActivityPub.fetch_user_activities(user, reading_user, params)
  
        conn
        |> add_link_headers(:user_statuses, activities, params["id"])
-       |> render(StatusView, "index.json", %{
+       |> put_view(StatusView)
+       |> render("index.json", %{
          activities: activities,
          for: reading_user,
          as: :activity
    end
  
    def dm_timeline(%{assigns: %{user: user}} = conn, params) do
-     query =
-       ActivityPub.fetch_activities_query(
-         [user.ap_id],
-         Map.merge(params, %{"type" => "Create", visibility: "direct"})
-       )
+     params =
+       params
+       |> Map.put("type", "Create")
+       |> Map.put("blocking_user", user)
+       |> Map.put("user", user)
+       |> Map.put(:visibility, "direct")
  
-     activities = Repo.all(query)
+     activities =
+       [user.ap_id]
+       |> ActivityPub.fetch_activities_query(params)
+       |> Pagination.fetch_paginated(params)
  
      conn
      |> add_link_headers(:dm_timeline, activities)
-     |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
+     |> put_view(StatusView)
+     |> render("index.json", %{activities: activities, for: user, as: :activity})
    end
  
    def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     with %Activity{} = activity <- Repo.get(Activity, id),
-          true <- ActivityPub.visible_for_user?(activity, user) do
-       try_render(conn, StatusView, "status.json", %{activity: activity, for: user})
+     with %Activity{} = activity <- Activity.get_by_id(id),
+          true <- Visibility.visible_for_user?(activity, user) do
+       conn
+       |> put_view(StatusView)
+       |> try_render("status.json", %{activity: activity, for: user})
      end
    end
  
    def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-     with %Activity{} = activity <- Repo.get(Activity, id),
+     with %Activity{} = activity <- Activity.get_by_id(id),
           activities <-
             ActivityPub.fetch_activities_for_context(activity.data["context"], %{
               "blocking_user" => user,
              as: :activity
            )
            |> Enum.reverse(),
+         # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
          descendants:
            StatusView.render(
              "index.json",
              as: :activity
            )
            |> Enum.reverse()
+         # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
        }
  
        json(conn, result)
      end
    end
  
+   def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
+     with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
+       conn
+       |> add_link_headers(:scheduled_statuses, scheduled_activities)
+       |> put_view(ScheduledActivityView)
+       |> render("index.json", %{scheduled_activities: scheduled_activities})
+     end
+   end
+   def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
+     with %ScheduledActivity{} = scheduled_activity <-
+            ScheduledActivity.get(user, scheduled_activity_id) do
+       conn
+       |> put_view(ScheduledActivityView)
+       |> render("show.json", %{scheduled_activity: scheduled_activity})
+     else
+       _ -> {:error, :not_found}
+     end
+   end
+   def update_scheduled_status(
+         %{assigns: %{user: user}} = conn,
+         %{"id" => scheduled_activity_id} = params
+       ) do
+     with %ScheduledActivity{} = scheduled_activity <-
+            ScheduledActivity.get(user, scheduled_activity_id),
+          {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
+       conn
+       |> put_view(ScheduledActivityView)
+       |> render("show.json", %{scheduled_activity: scheduled_activity})
+     else
+       nil -> {:error, :not_found}
+       error -> error
+     end
+   end
+   def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
+     with %ScheduledActivity{} = scheduled_activity <-
+            ScheduledActivity.get(user, scheduled_activity_id),
+          {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
+       conn
+       |> put_view(ScheduledActivityView)
+       |> render("show.json", %{scheduled_activity: scheduled_activity})
+     else
+       nil -> {:error, :not_found}
+       error -> error
+     end
+   end
    def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
        when length(media_ids) > 0 do
      params =
      params =
        params
        |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
-       |> Map.put("no_attachment_links", true)
  
      idempotency_key =
        case get_req_header(conn, "idempotency-key") do
          _ -> Ecto.UUID.generate()
        end
  
-     {:ok, activity} =
-       Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
+     scheduled_at = params["scheduled_at"]
  
-     try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
+     if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
+       with {:ok, scheduled_activity} <-
+              ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
+         conn
+         |> put_view(ScheduledActivityView)
+         |> render("show.json", %{scheduled_activity: scheduled_activity})
+       end
+     else
+       params = Map.drop(params, ["scheduled_at"])
+       {:ok, activity} =
+         Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
+           CommonAPI.post(user, params)
+         end)
+       conn
+       |> put_view(StatusView)
+       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+     end
    end
  
    def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
  
    def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
      with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
-       try_render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
+       conn
+       |> put_view(StatusView)
+       |> try_render("status.json", %{activity: announce, for: user, as: :activity})
      end
    end
  
    def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
      with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
-          %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
-       try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
+          %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+       conn
+       |> put_view(StatusView)
+       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
      end
    end
  
    def fav_status(%{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_activity_by_object_ap_id(id) do
-       try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
+          %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+       conn
+       |> put_view(StatusView)
+       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
      end
    end
  
    def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
      with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
-          %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
-       try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
+          %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+       conn
+       |> put_view(StatusView)
+       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
      end
    end
  
-   def notifications(%{assigns: %{user: user}} = conn, params) do
-     notifications = Notification.for_user(user, params)
+   def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+     with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
+       conn
+       |> put_view(StatusView)
+       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+     else
+       {:error, reason} ->
+         conn
+         |> put_resp_content_type("application/json")
+         |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
+     end
+   end
  
-     result =
-       Enum.map(notifications, fn x ->
-         render_notification(user, x)
-       end)
-       |> Enum.filter(& &1)
+   def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+     with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
+       conn
+       |> put_view(StatusView)
+       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+     end
+   end
+   def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %Activity{} = activity <- Activity.get_by_id(id),
+          %User{} = user <- User.get_by_nickname(user.nickname),
+          true <- Visibility.visible_for_user?(activity, user),
+          {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
+       conn
+       |> put_view(StatusView)
+       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+     end
+   end
+   def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %Activity{} = activity <- Activity.get_by_id(id),
+          %User{} = user <- User.get_by_nickname(user.nickname),
+          true <- Visibility.visible_for_user?(activity, user),
+          {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
+       conn
+       |> put_view(StatusView)
+       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+     end
+   end
+   def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     activity = Activity.get_by_id(id)
+     with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
+       conn
+       |> put_view(StatusView)
+       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+     else
+       {:error, reason} ->
+         conn
+         |> put_resp_content_type("application/json")
+         |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
+     end
+   end
+   def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     activity = Activity.get_by_id(id)
+     with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
+       conn
+       |> put_view(StatusView)
+       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+     end
+   end
+   def notifications(%{assigns: %{user: user}} = conn, params) do
+     notifications = MastodonAPI.get_notifications(user, params)
  
      conn
      |> add_link_headers(:notifications, notifications)
-     |> json(result)
+     |> put_view(NotificationView)
+     |> render("index.json", %{notifications: notifications, for: user})
    end
  
    def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
      with {:ok, notification} <- Notification.get(user, id) do
-       json(conn, render_notification(user, notification))
+       conn
+       |> put_view(NotificationView)
+       |> render("show.json", %{notification: notification, for: user})
      else
        {:error, reason} ->
          conn
      end
    end
  
+   def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
+     Notification.destroy_multiple(user, ids)
+     json(conn, %{})
+   end
    def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
      id = List.wrap(id)
      q = from(u in User, where: u.id in ^id)
      targets = Repo.all(q)
-     render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
-   end
  
-   # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
-   def relationships(%{assigns: %{user: user}} = conn, _) do
      conn
-     |> json([])
+     |> put_view(AccountView)
+     |> render("relationships.json", %{user: user, targets: targets})
    end
  
-   def update_media(%{assigns: %{user: _}} = conn, data) do
+   # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
+   def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
+   def update_media(%{assigns: %{user: user}} = conn, data) do
      with %Object{} = object <- Repo.get(Object, data["id"]),
+          true <- Object.authorize_mutation(object, user),
           true <- is_binary(data["description"]),
           description <- data["description"] do
        new_data = %{object.data | "name" => description}
  
-       change = Object.change(object, %{data: new_data})
-       {:ok, _} = Repo.update(change)
+       {:ok, _} =
+         object
+         |> Object.change(%{data: new_data})
+         |> Repo.update()
  
-       data =
-         new_data
-         |> Map.put("id", object.id)
+       attachment_data = Map.put(new_data, "id", object.id)
  
-       render(conn, StatusView, "attachment.json", %{attachment: data})
+       conn
+       |> put_view(StatusView)
+       |> render("attachment.json", %{attachment: attachment_data})
      end
    end
  
-   def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do
-     with {:ok, object} <- ActivityPub.upload(file, description: Map.get(data, "description")) do
-       change = Object.change(object, %{data: object.data})
-       {:ok, object} = Repo.update(change)
-       objdata =
-         object.data
-         |> Map.put("id", object.id)
+   def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
+     with {:ok, object} <-
+            ActivityPub.upload(
+              file,
+              actor: User.ap_id(user),
+              description: Map.get(data, "description")
+            ) do
+       attachment_data = Map.put(object.data, "id", object.id)
  
-       render(conn, StatusView, "attachment.json", %{attachment: objdata})
+       conn
+       |> put_view(StatusView)
+       |> render("attachment.json", %{attachment: attachment_data})
      end
    end
  
    def favourited_by(conn, %{"id" => id}) do
 -    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
 +    with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
 +         %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
        q = from(u in User, where: u.ap_id in ^likes)
        users = Repo.all(q)
-       render(conn, AccountView, "accounts.json", %{users: users, as: :user})
+       conn
+       |> put_view(AccountView)
+       |> render(AccountView, "accounts.json", %{users: users, as: :user})
      else
        _ -> json(conn, [])
      end
    end
  
    def reblogged_by(conn, %{"id" => id}) do
 -    with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
 +    with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
 +         %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
        q = from(u in User, where: u.ap_id in ^announces)
        users = Repo.all(q)
-       render(conn, AccountView, "accounts.json", %{users: users, as: :user})
+       conn
+       |> put_view(AccountView)
+       |> render("accounts.json", %{users: users, as: :user})
      else
        _ -> json(conn, [])
      end
    def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
      local_only = params["local"] in [true, "True", "true", "1"]
  
-     params =
+     tags =
+       [params["tag"], params["any"]]
+       |> List.flatten()
+       |> Enum.uniq()
+       |> Enum.filter(& &1)
+       |> Enum.map(&String.downcase(&1))
+     tag_all =
+       params["all"] ||
+         []
+         |> Enum.map(&String.downcase(&1))
+     tag_reject =
+       params["none"] ||
+         []
+         |> Enum.map(&String.downcase(&1))
+     activities =
        params
        |> Map.put("type", "Create")
        |> Map.put("local_only", local_only)
        |> Map.put("blocking_user", user)
-       |> Map.put("tag", String.downcase(params["tag"]))
-     activities =
-       ActivityPub.fetch_public_activities(params)
+       |> Map.put("muting_user", user)
+       |> Map.put("tag", tags)
+       |> Map.put("tag_all", tag_all)
+       |> Map.put("tag_reject", tag_reject)
+       |> ActivityPub.fetch_public_activities()
        |> Enum.reverse()
  
      conn
      |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
-     |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
-   end
+     |> put_view(StatusView)
+     |> render("index.json", %{activities: activities, for: user, as: :activity})
+   end
+   def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
+     with %User{} = user <- User.get_by_id(id),
+          followers <- MastodonAPI.get_followers(user, params) do
+       followers =
+         cond do
+           for_user && user.id == for_user.id -> followers
+           user.info.hide_followers -> []
+           true -> followers
+         end
  
-   # TODO: Pagination
-   def followers(conn, %{"id" => id}) do
-     with %User{} = user <- Repo.get(User, id),
-          {:ok, followers} <- User.get_followers(user) do
-       render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
+       conn
+       |> add_link_headers(:followers, followers, user)
+       |> put_view(AccountView)
+       |> render("accounts.json", %{users: followers, as: :user})
      end
    end
  
-   def following(conn, %{"id" => id}) do
-     with %User{} = user <- Repo.get(User, id),
-          {:ok, followers} <- User.get_friends(user) do
-       render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
+   def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
+     with %User{} = user <- User.get_by_id(id),
+          followers <- MastodonAPI.get_friends(user, params) do
+       followers =
+         cond do
+           for_user && user.id == for_user.id -> followers
+           user.info.hide_follows -> []
+           true -> followers
+         end
+       conn
+       |> add_link_headers(:following, followers, user)
+       |> put_view(AccountView)
+       |> render("accounts.json", %{users: followers, as: :user})
      end
    end
  
    def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
      with {:ok, follow_requests} <- User.get_follow_requests(followed) do
-       render(conn, AccountView, "accounts.json", %{users: follow_requests, as: :user})
+       conn
+       |> put_view(AccountView)
+       |> render("accounts.json", %{users: follow_requests, as: :user})
      end
    end
  
    def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
-     with %User{} = follower <- Repo.get(User, id),
-          {:ok, follower} <- User.maybe_follow(follower, followed),
-          %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
-          {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
-          {:ok, _activity} <-
-            ActivityPub.accept(%{
-              to: [follower.ap_id],
-              actor: followed.ap_id,
-              object: follow_activity.data["id"],
-              type: "Accept"
-            }) do
-       render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
+     with %User{} = follower <- User.get_by_id(id),
+          {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: followed, target: follower})
      else
        {:error, message} ->
          conn
    end
  
    def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
-     with %User{} = follower <- Repo.get(User, id),
-          %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
-          {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
-          {:ok, _activity} <-
-            ActivityPub.reject(%{
-              to: [follower.ap_id],
-              actor: followed.ap_id,
-              object: follow_activity.data["id"],
-              type: "Reject"
-            }) do
-       render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
+     with %User{} = follower <- User.get_by_id(id),
+          {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: followed, target: follower})
      else
        {:error, message} ->
          conn
    end
  
    def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
-     with %User{} = followed <- Repo.get(User, id),
-          {:ok, follower} <- User.maybe_direct_follow(follower, followed),
-          {:ok, _activity} <- ActivityPub.follow(follower, followed),
-          {:ok, follower, followed} <-
-            User.wait_and_refresh(
-              Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
-              follower,
-              followed
-            ) do
-       render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
+     with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
+          {_, true} <- {:followed, follower.id != followed.id},
+          false <- User.following?(follower, followed),
+          {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: follower, target: followed})
      else
+       {:followed, _} ->
+         {:error, :not_found}
+       true ->
+         followed = User.get_cached_by_id(id)
+         {:ok, follower} =
+           case conn.params["reblogs"] do
+             true -> CommonAPI.show_reblogs(follower, followed)
+             false -> CommonAPI.hide_reblogs(follower, followed)
+           end
+         conn
+         |> put_view(AccountView)
+         |> render("relationship.json", %{user: follower, target: followed})
        {:error, message} ->
          conn
          |> put_resp_content_type("application/json")
    end
  
    def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
-     with %User{} = followed <- Repo.get_by(User, nickname: uri),
-          {:ok, follower} <- User.maybe_direct_follow(follower, followed),
-          {:ok, _activity} <- ActivityPub.follow(follower, followed) do
-       render(conn, AccountView, "account.json", %{user: followed, for: follower})
+     with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
+          {_, true} <- {:followed, follower.id != followed.id},
+          {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
+       conn
+       |> put_view(AccountView)
+       |> render("account.json", %{user: followed, for: follower})
      else
+       {:followed, _} ->
+         {:error, :not_found}
        {:error, message} ->
          conn
          |> put_resp_content_type("application/json")
    end
  
    def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
-     with %User{} = followed <- Repo.get(User, id),
-          {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
-          {:ok, follower, _} <- User.unfollow(follower, followed) do
-       render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
+     with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
+          {_, true} <- {:followed, follower.id != followed.id},
+          {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: follower, target: followed})
+     else
+       {:followed, _} ->
+         {:error, :not_found}
+       error ->
+         error
+     end
+   end
+   def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
+     with %User{} = muted <- User.get_by_id(id),
+          {:ok, muter} <- User.mute(muter, muted) do
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: muter, target: muted})
+     else
+       {:error, message} ->
+         conn
+         |> put_resp_content_type("application/json")
+         |> send_resp(403, Jason.encode!(%{"error" => message}))
+     end
+   end
+   def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
+     with %User{} = muted <- User.get_by_id(id),
+          {:ok, muter} <- User.unmute(muter, muted) do
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: muter, target: muted})
+     else
+       {:error, message} ->
+         conn
+         |> put_resp_content_type("application/json")
+         |> send_resp(403, Jason.encode!(%{"error" => message}))
+     end
+   end
+   def mutes(%{assigns: %{user: user}} = conn, _) do
+     with muted_accounts <- User.muted_users(user) do
+       res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
+       json(conn, res)
      end
    end
  
    def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
-     with %User{} = blocked <- Repo.get(User, id),
+     with %User{} = blocked <- User.get_by_id(id),
           {:ok, blocker} <- User.block(blocker, blocked),
           {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
-       render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: blocker, target: blocked})
      else
        {:error, message} ->
          conn
    end
  
    def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
-     with %User{} = blocked <- Repo.get(User, id),
+     with %User{} = blocked <- User.get_by_id(id),
           {:ok, blocker} <- User.unblock(blocker, blocked),
           {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
-       render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: blocker, target: blocked})
      else
        {:error, message} ->
          conn
      end
    end
  
-   # TODO: Use proper query
    def blocks(%{assigns: %{user: user}} = conn, _) do
-     with blocked_users <- user.info.blocks || [],
-          accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
-       res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
+     with blocked_accounts <- User.blocked_users(user) do
+       res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
        json(conn, res)
      end
    end
      json(conn, %{})
    end
  
-   def status_search(query) do
+   def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %User{} = subscription_target <- User.get_cached_by_id(id),
+          {:ok, subscription_target} = User.subscribe(user, subscription_target) do
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: user, target: subscription_target})
+     else
+       {:error, message} ->
+         conn
+         |> put_resp_content_type("application/json")
+         |> send_resp(403, Jason.encode!(%{"error" => message}))
+     end
+   end
+   def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+     with %User{} = subscription_target <- User.get_cached_by_id(id),
+          {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: user, target: subscription_target})
+     else
+       {:error, message} ->
+         conn
+         |> put_resp_content_type("application/json")
+         |> send_resp(403, Jason.encode!(%{"error" => message}))
+     end
+   end
+   def status_search(user, query) do
      fetched =
        if Regex.match?(~r/https?:/, query) do
-         with {:ok, object} <- Fetcher.fetch_object_from_id(query) do
-           [Activity.get_create_activity_by_object_ap_id(object.data["id"])]
 -        with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
++        with {:ok, object} <- Fetcher.fetch_object_from_id(query),
+              %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
+              true <- Visibility.visible_for_user?(activity, user) do
+           [activity]
          else
            _e -> []
          end
    end
  
    def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
-     accounts = User.search(query, params["resolve"] == "true")
+     accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
  
-     statuses = status_search(query)
+     statuses = status_search(user, query)
  
      tags_path = Web.base_url() <> "/tag/"
  
      tags =
-       String.split(query)
+       query
+       |> String.split()
        |> Enum.uniq()
        |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
        |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
    end
  
    def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
-     accounts = User.search(query, params["resolve"] == "true")
+     accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
  
-     statuses = status_search(query)
+     statuses = status_search(user, query)
  
      tags =
-       String.split(query)
+       query
+       |> String.split()
        |> Enum.uniq()
        |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
        |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
    end
  
    def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
-     accounts = User.search(query, params["resolve"] == "true")
+     accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
  
      res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
  
      json(conn, res)
    end
  
-   def favourites(%{assigns: %{user: user}} = conn, _) do
+   def favourites(%{assigns: %{user: user}} = conn, params) do
      params =
-       %{}
+       params
        |> Map.put("type", "Create")
        |> Map.put("favorited_by", user.ap_id)
        |> Map.put("blocking_user", user)
  
      activities =
-       ActivityPub.fetch_public_activities(params)
+       ActivityPub.fetch_activities([], params)
+       |> Enum.reverse()
+     conn
+     |> add_link_headers(:favourites, activities)
+     |> put_view(StatusView)
+     |> render("index.json", %{activities: activities, for: user, as: :activity})
+   end
+   def bookmarks(%{assigns: %{user: user}} = conn, _) do
+     user = User.get_by_id(user.id)
+     activities =
+       user.bookmarks
+       |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
        |> Enum.reverse()
  
      conn
-     |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
+     |> put_view(StatusView)
+     |> render("index.json", %{activities: activities, for: user, as: :activity})
    end
  
    def get_lists(%{assigns: %{user: user}} = conn, opts) do
        res = ListView.render("list.json", list: list)
        json(conn, res)
      else
-       _e -> json(conn, "error")
+       _e ->
+         conn
+         |> put_status(404)
+         |> json(%{error: "Record not found"})
      end
    end
  
      accounts
      |> Enum.each(fn account_id ->
        with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
-            %User{} = followed <- Repo.get(User, account_id) do
+            %User{} = followed <- User.get_by_id(account_id) do
          Pleroma.List.follow(list, followed)
        end
      end)
      accounts
      |> Enum.each(fn account_id ->
        with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
-            %User{} = followed <- Repo.get(Pleroma.User, account_id) do
+            %User{} = followed <- Pleroma.User.get_by_id(account_id) do
          Pleroma.List.unfollow(list, followed)
        end
      end)
    def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
           {:ok, users} = Pleroma.List.get_following(list) do
-       render(conn, AccountView, "accounts.json", %{users: users, as: :user})
+       conn
+       |> put_view(AccountView)
+       |> render("accounts.json", %{users: users, as: :user})
      end
    end
  
    end
  
    def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
-     with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do
+     with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
        params =
          params
          |> Map.put("type", "Create")
          |> Map.put("blocking_user", user)
+         |> Map.put("muting_user", user)
  
        # we must filter the following list for the user to avoid leaking statuses the user
        # does not actually have permission to see (for more info, peruse security issue #270).
-       following_to =
+       activities =
          following
          |> Enum.filter(fn x -> x in user.following end)
-       activities =
-         ActivityPub.fetch_activities_bounded(following_to, following, params)
+         |> ActivityPub.fetch_activities_bounded(following, params)
          |> Enum.reverse()
  
        conn
-       |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
+       |> put_view(StatusView)
+       |> render("index.json", %{activities: activities, for: user, as: :activity})
      else
        _e ->
          conn
    end
  
    def index(%{assigns: %{user: user}} = conn, _params) do
-     token =
-       conn
-       |> get_session(:oauth_token)
+     token = get_session(conn, :oauth_token)
  
      if user && token do
        mastodon_emoji = mastodonized_emoji()
  
-       limit = Pleroma.Config.get([:instance, :limit])
+       limit = Config.get([:instance, :limit])
  
        accounts =
          Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
  
+       flavour = get_user_flavour(user)
        initial_state =
          %{
            meta: %{
              auto_play_gif: false,
              display_sensitive_media: false,
              reduce_motion: false,
-             max_toot_chars: limit
+             max_toot_chars: limit,
+             mascot: "/images/pleroma-fox-tan-smol.png"
            },
            rights: %{
-             delete_others_notice: !!user.info.is_moderator
+             delete_others_notice: present?(user.info.is_moderator),
+             admin: present?(user.info.is_admin)
            },
            compose: %{
              me: "#{user.id}",
              default_privacy: user.info.default_scope,
-             default_sensitive: false
+             default_sensitive: false,
+             allow_content_types: Config.get([:instance, :allowed_post_formats])
            },
            media_attachments: %{
              accept_content_types: [
              ]
            },
            settings:
-             Map.get(user.info, :settings) ||
+             user.info.settings ||
                %{
                  onboarded: true,
                  home: %{
  
        conn
        |> put_layout(false)
-       |> render(MastodonView, "index.html", %{initial_state: initial_state})
+       |> put_view(MastodonView)
+       |> render("index.html", %{initial_state: initial_state, flavour: flavour})
      else
        conn
+       |> put_session(:return_to, conn.request_path)
        |> redirect(to: "/web/login")
      end
    end
  
    def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
-     with new_info <- Map.put(user.info, "settings", settings),
-          change <- User.info_changeset(user, %{info: new_info}),
-          {:ok, _user} <- User.update_and_set_cache(change) do
-       conn
-       |> json(%{})
+     info_cng = User.Info.mastodon_settings_update(user.info, settings)
+     with changeset <- Ecto.Changeset.change(user),
+          changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
+          {:ok, _user} <- User.update_and_set_cache(changeset) do
+       json(conn, %{})
+     else
+       e ->
+         conn
+         |> put_resp_content_type("application/json")
+         |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
+     end
+   end
+   @supported_flavours ["glitch", "vanilla"]
+   def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
+       when flavour in @supported_flavours do
+     flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
+     with changeset <- Ecto.Changeset.change(user),
+          changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
+          {:ok, user} <- User.update_and_set_cache(changeset),
+          flavour <- user.info.flavour do
+       json(conn, flavour)
      else
        e ->
          conn
-         |> json(%{error: inspect(e)})
+         |> put_resp_content_type("application/json")
+         |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
      end
    end
  
-   def login(conn, %{"code" => code}) do
+   def set_flavour(conn, _params) do
+     conn
+     |> put_status(400)
+     |> json(%{error: "Unsupported flavour"})
+   end
+   def get_flavour(%{assigns: %{user: user}} = conn, _params) do
+     json(conn, get_user_flavour(user))
+   end
+   defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
+     flavour
+   end
+   defp get_user_flavour(_) do
+     "glitch"
+   end
+   def login(%{assigns: %{user: %User{}}} = conn, _params) do
+     redirect(conn, to: local_mastodon_root_path(conn))
+   end
+   @doc "Local Mastodon FE login init action"
+   def login(conn, %{"code" => auth_token}) do
      with {:ok, app} <- get_or_make_app(),
-          %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
+          %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
           {:ok, token} <- Token.exchange_token(app, auth) do
        conn
        |> put_session(:oauth_token, token.token)
-       |> redirect(to: "/web/getting-started")
+       |> redirect(to: local_mastodon_root_path(conn))
      end
    end
  
+   @doc "Local Mastodon FE callback action"
    def login(conn, _) do
      with {:ok, app} <- get_or_make_app() do
        path =
            response_type: "code",
            client_id: app.client_id,
            redirect_uri: ".",
-           scope: app.scopes
+           scope: Enum.join(app.scopes, " ")
          )
  
-       conn
-       |> redirect(to: path)
+       redirect(conn, to: path)
      end
    end
  
-   defp get_or_make_app() do
-     with %App{} = app <- Repo.get_by(App, client_name: "Mastodon-Local") do
+   defp local_mastodon_root_path(conn) do
+     case get_session(conn, :return_to) do
+       nil ->
+         mastodon_api_path(conn, :index, ["getting-started"])
+       return_to ->
+         delete_session(conn, :return_to)
+         return_to
+     end
+   end
+   defp get_or_make_app do
+     find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
+     scopes = ["read", "write", "follow", "push"]
+     with %App{} = app <- Repo.get_by(App, find_attrs) do
+       {:ok, app} =
+         if app.scopes == scopes do
+           {:ok, app}
+         else
+           app
+           |> Ecto.Changeset.change(%{scopes: scopes})
+           |> Repo.update()
+         end
        {:ok, app}
      else
        _e ->
          cs =
-           App.register_changeset(%App{}, %{
-             client_name: "Mastodon-Local",
-             redirect_uris: ".",
-             scopes: "read,write,follow"
-           })
+           App.register_changeset(
+             %App{},
+             Map.put(find_attrs, :scopes, scopes)
+           )
  
          Repo.insert(cs)
      end
    def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
      Logger.debug("Unimplemented, returning unmodified relationship")
  
-     with %User{} = target <- Repo.get(User, id) do
-       render(conn, AccountView, "relationship.json", %{user: user, target: target})
+     with %User{} = target <- User.get_by_id(id) do
+       conn
+       |> put_view(AccountView)
+       |> render("relationship.json", %{user: user, target: target})
      end
    end
  
      json(conn, %{})
    end
  
-   def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
-     actor = User.get_cached_by_ap_id(activity.data["actor"])
-     created_at =
-       NaiveDateTime.to_iso8601(created_at)
-       |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
-     id = id |> to_string
-     case activity.data["type"] do
-       "Create" ->
-         %{
-           id: id,
-           type: "mention",
-           created_at: created_at,
-           account: AccountView.render("account.json", %{user: actor, for: user}),
-           status: StatusView.render("status.json", %{activity: activity, for: user})
-         }
-       "Like" ->
-         liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
-         %{
-           id: id,
-           type: "favourite",
-           created_at: created_at,
-           account: AccountView.render("account.json", %{user: actor, for: user}),
-           status: StatusView.render("status.json", %{activity: liked_activity, for: user})
-         }
-       "Announce" ->
-         announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
-         %{
-           id: id,
-           type: "reblog",
-           created_at: created_at,
-           account: AccountView.render("account.json", %{user: actor, for: user}),
-           status: StatusView.render("status.json", %{activity: announced_activity, for: user})
-         }
-       "Follow" ->
-         %{
-           id: id,
-           type: "follow",
-           created_at: created_at,
-           account: AccountView.render("account.json", %{user: actor, for: user})
-         }
-       _ ->
-         nil
-     end
-   end
    def get_filters(%{assigns: %{user: user}} = conn, _) do
-     filters = Pleroma.Filter.get_filters(user)
+     filters = Filter.get_filters(user)
      res = FilterView.render("filters.json", filters: filters)
      json(conn, res)
    end
          %{assigns: %{user: user}} = conn,
          %{"phrase" => phrase, "context" => context} = params
        ) do
-     query = %Pleroma.Filter{
+     query = %Filter{
        user_id: user.id,
        phrase: phrase,
        context: context,
        # expires_at
      }
  
-     {:ok, response} = Pleroma.Filter.create(query)
+     {:ok, response} = Filter.create(query)
      res = FilterView.render("filter.json", filter: response)
      json(conn, res)
    end
  
    def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
-     filter = Pleroma.Filter.get(filter_id, user)
+     filter = Filter.get(filter_id, user)
      res = FilterView.render("filter.json", filter: filter)
      json(conn, res)
    end
          %{assigns: %{user: user}} = conn,
          %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
        ) do
-     query = %Pleroma.Filter{
+     query = %Filter{
        user_id: user.id,
        filter_id: filter_id,
        phrase: phrase,
        # expires_at
      }
  
-     {:ok, response} = Pleroma.Filter.update(query)
+     {:ok, response} = Filter.update(query)
      res = FilterView.render("filter.json", filter: response)
      json(conn, res)
    end
  
    def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
-     query = %Pleroma.Filter{
+     query = %Filter{
        user_id: user.id,
        filter_id: filter_id
      }
  
-     {:ok, _} = Pleroma.Filter.delete(query)
+     {:ok, _} = Filter.delete(query)
      json(conn, %{})
    end
  
+   # fallback action
+   #
+   def errors(conn, {:error, %Changeset{} = changeset}) do
+     error_message =
+       changeset
+       |> Changeset.traverse_errors(fn {message, _opt} -> message end)
+       |> Enum.map_join(", ", fn {_k, v} -> v end)
+     conn
+     |> put_status(422)
+     |> json(%{error: error_message})
+   end
+   def errors(conn, {:error, :not_found}) do
+     conn
+     |> put_status(404)
+     |> json(%{error: "Record not found"})
+   end
    def errors(conn, _) do
      conn
      |> put_status(500)
    end
  
    def suggestions(%{assigns: %{user: user}} = conn, _) do
-     suggestions = Pleroma.Config.get(:suggestions)
+     suggestions = Config.get(:suggestions)
  
      if Keyword.get(suggestions, :enabled, false) do
        api = Keyword.get(suggestions, :third_party_engine, "")
        timeout = Keyword.get(suggestions, :timeout, 5000)
        limit = Keyword.get(suggestions, :limit, 23)
  
-       host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
+       host = Config.get([Pleroma.Web.Endpoint, :url, :host])
  
        user = user.nickname
-       url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
  
-       with {:ok, %{status_code: 200, body: body}} <-
-              @httpoison.get(url, [], timeout: timeout, recv_timeout: timeout),
+       url =
+         api
+         |> String.replace("{{host}}", host)
+         |> String.replace("{{user}}", user)
+       with {:ok, %{status: 200, body: body}} <-
+              @httpoison.get(
+                url,
+                [],
+                adapter: [
+                  recv_timeout: timeout,
+                  pool: :default
+                ]
+              ),
             {:ok, data} <- Jason.decode(body) do
-         data2 =
-           Enum.slice(data, 0, limit)
+         data =
+           data
+           |> Enum.slice(0, limit)
            |> Enum.map(fn x ->
              Map.put(
                x,
            end)
  
          conn
-         |> json(data2)
+         |> json(data)
        else
          e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
        end
      end
    end
  
-   def try_render(conn, renderer, target, params)
+   def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
+     with %Activity{} = activity <- Activity.get_by_id(status_id),
+          true <- Visibility.visible_for_user?(activity, user) do
+       data =
+         StatusView.render(
+           "card.json",
+           Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+         )
+       json(conn, data)
+     else
+       _e ->
+         %{}
+     end
+   end
+   def reports(%{assigns: %{user: user}} = conn, params) do
+     case CommonAPI.report(user, params) do
+       {:ok, activity} ->
+         conn
+         |> put_view(ReportView)
+         |> try_render("report.json", %{activity: activity})
+       {:error, err} ->
+         conn
+         |> put_status(:bad_request)
+         |> json(%{error: err})
+     end
+   end
+   def try_render(conn, target, params)
        when is_binary(target) do
-     res = render(conn, renderer, target, params)
+     res = render(conn, target, params)
  
      if res == nil do
        conn
      end
    end
  
-   def try_render(conn, _, _, _) do
+   def try_render(conn, _, _) do
      conn
      |> put_status(501)
      |> json(%{error: "Can't display this activity"})
    end
+   defp present?(nil), do: false
+   defp present?(false), do: false
+   defp present?(_), do: true
  end
index 31f4675c3a86faca85e857ec0b308fbdc2498f52,a9f607aa5dd8b38d8da006471cc847eb18bf50c6..e4de5ecfb3726b7da261a2a6e022aae53278261a
@@@ -1,53 -1,84 +1,87 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.MastodonAPI.StatusView do
    use Pleroma.Web, :view
-   alias Pleroma.Web.MastodonAPI.{AccountView, StatusView}
-   alias Pleroma.{User, Activity, Object}
+   alias Pleroma.Activity
+   alias Pleroma.HTML
+   alias Pleroma.Repo
++  alias Pleroma.Object
+   alias Pleroma.User
+   alias Pleroma.Web.CommonAPI
    alias Pleroma.Web.CommonAPI.Utils
+   alias Pleroma.Web.MastodonAPI.AccountView
+   alias Pleroma.Web.MastodonAPI.StatusView
    alias Pleroma.Web.MediaProxy
-   alias Pleroma.Repo
-   alias Pleroma.HTML
  
    # TODO: Add cached version.
    defp get_replied_to_activities(activities) do
      activities
      |> Enum.map(fn
 -      %{data: %{"type" => "Create", "object" => %{"inReplyTo" => in_reply_to}}} ->
 -        in_reply_to != "" && in_reply_to
 +      %{data: %{"type" => "Create", "object" => object}} ->
 +        object = Object.normalize(object)
 +        object.data["inReplyTo"] != "" && object.data["inReplyTo"]
  
        _ ->
          nil
      end)
      |> Enum.filter(& &1)
-     |> Activity.create_activity_by_object_id_query()
+     |> Activity.create_by_object_ap_id()
      |> Repo.all()
      |> Enum.reduce(%{}, fn activity, acc ->
 -      Map.put(acc, activity.data["object"]["id"], activity)
 +      object = Object.normalize(activity.data["object"])
 +      Map.put(acc, object.data["id"], activity)
      end)
    end
  
 -    object = activity.data["object"] || %{}
 -    present?(user && user.ap_id in (object["announcements"] || []))
+   defp get_user(ap_id) do
+     cond do
+       user = User.get_cached_by_ap_id(ap_id) ->
+         user
+       user = User.get_by_guessed_nickname(ap_id) ->
+         user
+       true ->
+         User.error_user(ap_id)
+     end
+   end
+   defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
+     do: context_id
+   defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
+     do: Utils.context_to_conversation_id(context)
+   defp get_context_id(_), do: nil
+   defp reblogged?(activity, user) do
++    object = Object.normalize(activity) || %{}
++    present?(user && user.ap_id in (object.data["announcements"] || []))
+   end
    def render("index.json", opts) do
      replied_to_activities = get_replied_to_activities(opts.activities)
  
-     render_many(
-       opts.activities,
+     opts.activities
+     |> safe_render_many(
        StatusView,
        "status.json",
        Map.put(opts, :replied_to_activities, replied_to_activities)
      )
-     |> Enum.filter(fn x -> not is_nil(x) end)
    end
  
    def render(
          "status.json",
          %{activity: %{data: %{"type" => "Announce", "object" => object}} = activity} = opts
        ) do
-     user = User.get_cached_by_ap_id(activity.data["actor"])
+     user = get_user(activity.data["actor"])
      created_at = Utils.to_masto_date(activity.data["published"])
  
-     reblogged = Activity.get_create_activity_by_object_ap_id(object)
-     reblogged = render("status.json", Map.put(opts, :activity, reblogged))
+     reblogged_activity = Activity.get_create_by_object_ap_id(object)
+     reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))
  
      mentions =
        activity.recipients
        reblogs_count: 0,
        replies_count: 0,
        favourites_count: 0,
-       reblogged: false,
+       reblogged: reblogged?(reblogged_activity, opts[:for]),
        favourited: false,
+       bookmarked: false,
        muted: false,
+       pinned: pinned?(activity, user),
        sensitive: false,
        spoiler_text: "",
        visibility: "public",
-       media_attachments: [],
+       media_attachments: reblogged[:media_attachments] || [],
        mentions: mentions,
-       tags: [],
+       tags: reblogged[:tags] || [],
        application: %{
          name: "Web",
          website: nil
        },
        language: nil,
-       emojis: []
+       emojis: [],
+       pleroma: %{
+         local: activity.local
+       }
      }
    end
  
    def render("status.json", %{activity: %{data: %{"object" => object}} = activity} = opts) do
-     user = User.get_cached_by_ap_id(activity.data["actor"])
 +    object = Object.normalize(object)
 +
+     user = get_user(activity.data["actor"])
  
 -    like_count = object["like_count"] || 0
 -    announcement_count = object["announcement_count"] || 0
 +    like_count = object.data["like_count"] || 0
 +    announcement_count = object.data["announcement_count"] || 0
  
 -    tags = object["tag"] || []
 -    sensitive = object["sensitive"] || Enum.member?(tags, "nsfw")
 +    tags = object.data["tag"] || []
 +    sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
  
      mentions =
        activity.recipients
        |> Enum.filter(& &1)
        |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
  
-     repeated = opts[:for] && opts[:for].ap_id in (object.data["announcements"] || [])
 -    favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
 -    bookmarked = opts[:for] && object["id"] in opts[:for].bookmarks
 +    favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
 +
++    bookmarked = opts[:for] && object.data["id"] in opts[:for].bookmarks
 -    attachment_data = object["attachment"] || []
 +    attachment_data = object.data["attachment"] || []
-     attachment_data = attachment_data ++ if object.data["type"] == "Video", do: [object], else: []
      attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
  
 -    created_at = Utils.to_masto_date(object["published"])
 +    created_at = Utils.to_masto_date(object.data["published"])
  
      reply_to = get_reply_to(activity, opts)
-     reply_to_user = reply_to && User.get_cached_by_ap_id(reply_to.data["actor"])
 +
-     emojis =
-       (object.data["emoji"] || [])
-       |> Enum.map(fn {name, url} ->
-         name = HTML.strip_tags(name)
-         url =
-           HTML.strip_tags(url)
-           |> MediaProxy.url()
-         %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
-       end)
+     reply_to_user = reply_to && get_user(reply_to.data["actor"])
  
      content =
-       render_content(object.data)
-       |> HTML.filter_tags(User.html_filter_policy(opts[:for]))
+       object
+       |> render_content()
+     content_html =
+       content
+       |> HTML.get_cached_scrubbed_html_for_activity(
+         User.html_filter_policy(opts[:for]),
+         activity,
+         "mastoapi:content"
+       )
+     content_plaintext =
+       content
+       |> HTML.get_cached_stripped_html_for_activity(
+         activity,
+         "mastoapi:content"
+       )
 -    summary = object["summary"] || ""
++    summary = object.data["summary"] || ""
+     summary_html =
+       summary
+       |> HTML.get_cached_scrubbed_html_for_activity(
+         User.html_filter_policy(opts[:for]),
+         activity,
+         "mastoapi:summary"
+       )
+     summary_plaintext =
+       summary
+       |> HTML.get_cached_stripped_html_for_activity(
+         activity,
+         "mastoapi:summary"
+       )
+     card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
+     url =
+       if user.local do
+         Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
+       else
 -        object["external_url"] || object["id"]
++        object.data["external_url"] || object.data["id"]
+       end
  
      %{
        id: to_string(activity.id),
 -      uri: object["id"],
 +      uri: object.data["id"],
-       url: object.data["external_url"] || object.data["id"],
+       url: url,
        account: AccountView.render("account.json", %{user: user}),
        in_reply_to_id: reply_to && to_string(reply_to.id),
        in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
        reblog: nil,
-       content: content,
+       card: card,
+       content: content_html,
        created_at: created_at,
        reblogs_count: announcement_count,
-       replies_count: 0,
 -      replies_count: object["repliesCount"] || 0,
++      replies_count: object.data["repliesCount"] || 0,
        favourites_count: like_count,
-       reblogged: !!repeated,
-       favourited: !!favorited,
-       muted: false,
+       reblogged: reblogged?(activity, opts[:for]),
+       favourited: present?(favorited),
+       bookmarked: present?(bookmarked),
+       muted: CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user),
+       pinned: pinned?(activity, user),
        sensitive: sensitive,
-       spoiler_text: object.data["summary"] || "",
-       visibility: get_visibility(object.data),
-       media_attachments: attachments |> Enum.take(4),
+       spoiler_text: summary_html,
+       visibility: get_visibility(object),
+       media_attachments: attachments,
        mentions: mentions,
-       # fix,
-       tags: [],
+       tags: build_tags(tags),
        application: %{
          name: "Web",
          website: nil
        },
        language: nil,
-       emojis: emojis
 -      emojis: build_emojis(activity.data["object"]["emoji"]),
++      emojis: build_emojis(object.data["emoji"]),
+       pleroma: %{
+         local: activity.local,
+         conversation_id: get_context_id(activity),
+         content: %{"text/plain" => content_plaintext},
+         spoiler_text: %{"text/plain" => summary_plaintext}
+       }
      }
    end
  
      nil
    end
  
+   def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
+     page_url_data = URI.parse(page_url)
+     page_url_data =
+       if rich_media[:url] != nil do
+         URI.merge(page_url_data, URI.parse(rich_media[:url]))
+       else
+         page_url_data
+       end
+     page_url = page_url_data |> to_string
+     image_url =
+       if rich_media[:image] != nil do
+         URI.merge(page_url_data, URI.parse(rich_media[:image]))
+         |> to_string
+       else
+         nil
+       end
+     site_name = rich_media[:site_name] || page_url_data.host
+     %{
+       type: "link",
+       provider_name: site_name,
+       provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
+       url: page_url,
+       image: image_url |> MediaProxy.url(),
+       title: rich_media[:title],
+       description: rich_media[:description],
+       pleroma: %{
+         opengraph: rich_media
+       }
+     }
+   end
+   def render("card.json", _) do
+     nil
+   end
    def render("attachment.json", %{attachment: attachment}) do
      [attachment_url | _] = attachment["url"]
      media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
        preview_url: href,
        text_url: href,
        type: type,
-       description: attachment["name"]
+       description: attachment["name"],
+       pleroma: %{mime_type: media_type}
      }
    end
  
    def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
 -    with nil <- replied_to_activities[activity.data["object"]["inReplyTo"]] do
 +    object = Object.normalize(activity.data["object"])
-     replied_to_activities[object.data["inReplyTo"]]
++
++    with nil <- replied_to_activities[object.data["inReplyTo"]] do
+       # If user didn't participate in the thread
+       Activity.get_in_reply_to_activity(activity)
+     end
    end
  
    def get_reply_to(%{data: %{"object" => object}}, _) do
 -    if object["inReplyTo"] && object["inReplyTo"] != "" do
 -      Activity.get_create_by_object_ap_id(object["inReplyTo"])
 +    object = Object.normalize(object)
 +
 +    if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
-       Activity.get_create_activity_by_object_ap_id(object.data["inReplyTo"])
++      Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
      else
        nil
      end
  
    def get_visibility(object) do
      public = "https://www.w3.org/ns/activitystreams#Public"
--    to = object["to"] || []
--    cc = object["cc"] || []
++    to = object.data["to"] || []
++    cc = object.data["cc"] || []
  
      cond do
        public in to ->
        Enum.any?(to, &String.contains?(&1, "/followers")) ->
          "private"
  
+       length(cc) > 0 ->
+         "private"
        true ->
          "direct"
      end
    end
  
--  def render_content(%{"type" => "Video"} = object) do
-     name = object["name"]
 -    with name when not is_nil(name) and name != "" <- object["name"] do
 -      "<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
++  def render_content(%{data: %{"type" => "Video"}} = object) do
++    with name when not is_nil(name) and name != "" <- object.data["name"] do
++      "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
+     else
 -      _ -> object["content"] || ""
++      _ -> object.data["content"] || ""
+     end
+   end
  
-     content =
-       if !!name and name != "" do
-         "<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
-       else
-         object["content"] || ""
-       end
 -  def render_content(%{"type" => object_type} = object)
++  def render_content(%{data: %{"type" => object_type}} = object)
+       when object_type in ["Article", "Page"] do
 -    with summary when not is_nil(summary) and summary != "" <- object["name"],
 -         url when is_bitstring(url) <- object["url"] do
 -      "<p><a href=\"#{url}\">#{summary}</a></p>#{object["content"]}"
++    with summary when not is_nil(summary) and summary != "" <- object.data["name"],
++         url when is_bitstring(url) <- object.data["url"] do
++      "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
+     else
 -      _ -> object["content"] || ""
++      _ -> object.data["content"] || ""
+     end
+   end
  
-     content
 -  def render_content(object), do: object["content"] || ""
++  def render_content(object), do: object.data["content"] || ""
+   @doc """
+   Builds a dictionary tags.
+   ## Examples
+   iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
+   [{"name": "fediverse", "url": "/tag/fediverse"},
+    {"name": "nextcloud", "url": "/tag/nextcloud"}]
+   """
+   @spec build_tags(list(any())) :: list(map())
+   def build_tags(object_tags) when is_list(object_tags) do
+     object_tags = for tag when is_binary(tag) <- object_tags, do: tag
+     Enum.reduce(object_tags, [], fn tag, tags ->
+       tags ++ [%{name: tag, url: "/tag/#{tag}"}]
+     end)
    end
  
-   def render_content(%{"type" => object_type} = object) when object_type in ["Article", "Page"] do
-     summary = object["name"]
+   def build_tags(_), do: []
  
-     content =
-       if !!summary and summary != "" and is_bitstring(object["url"]) do
-         "<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
-       else
-         object["content"] || ""
-       end
+   @doc """
+   Builds list emojis.
  
-     content
+   Arguments: `nil` or list tuple of name and url.
+   Returns list emojis.
+   ## Examples
+   iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
+   [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
+   """
+   @spec build_emojis(nil | list(tuple())) :: list(map())
+   def build_emojis(nil), do: []
+   def build_emojis(emojis) do
+     emojis
+     |> Enum.map(fn {name, url} ->
+       name = HTML.strip_tags(name)
+       url =
+         url
+         |> HTML.strip_tags()
+         |> MediaProxy.url()
+       %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
+     end)
    end
  
-   def render_content(object), do: object["content"] || ""
+   defp present?(nil), do: false
+   defp present?(false), do: false
+   defp present?(_), do: true
+   defp pinned?(%Activity{id: id}, %User{info: %{pinned_activities: pinned_activities}}),
+     do: id in pinned_activities
  end
index fefd9459a0e94beec6bad2b82dfa50a7dc8322c5,1a1b74bb0b987b09a26e1a26bc5f18ac3d44c55b..b11a2b5ce3e31bd3431f63b2db972559de918a48
@@@ -1,6 -1,13 +1,13 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.OStatus.ActivityRepresenter do
-   alias Pleroma.{Activity, User, Object}
+   alias Pleroma.Activity
+   alias Pleroma.Object
+   alias Pleroma.User
    alias Pleroma.Web.OStatus.UserRepresenter
    require Logger
  
    defp get_href(id) do
      end)
    end
  
 -  defp get_links(%{local: true, data: data}) do
 +  defp get_links(%{local: true}, %{"id" => object_id}) do
      h = fn str -> [to_charlist(str)] end
  
      [
 -      {:link, [type: ['application/atom+xml'], href: h.(data["object"]["id"]), rel: 'self'], []},
 -      {:link, [type: ['text/html'], href: h.(data["object"]["id"]), rel: 'alternate'], []}
 +      {:link, [type: ['application/atom+xml'], href: h.(object_id), rel: 'self'], []},
 +      {:link, [type: ['text/html'], href: h.(object_id), rel: 'alternate'], []}
      ]
    end
  
 -  defp get_links(%{
 -         local: false,
 -         data: %{
 -           "object" => %{
 -             "external_url" => external_url
 -           }
 -         }
 -       }) do
 +  defp get_links(%{local: false}, %{"external_url" => external_url}) do
      h = fn str -> [to_charlist(str)] end
  
      [
@@@ -64,7 -78,7 +71,7 @@@
      ]
    end
  
 -  defp get_links(_activity), do: []
 +  defp get_links(_activity, _object_data), do: []
  
    defp get_emoji_links(emojis) do
      Enum.map(emojis, fn {emoji, file} ->
  
    def to_simple_form(activity, user, with_author \\ false)
  
 -  def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user, with_author) do
 +  def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) do
      h = fn str -> [to_charlist(str)] end
  
 -    updated_at = activity.data["object"]["published"]
 -    inserted_at = activity.data["object"]["published"]
 +    object = Object.normalize(activity.data["object"])
 +
 +    updated_at = object.data["published"]
 +    inserted_at = object.data["published"]
  
      attachments =
 -      Enum.map(activity.data["object"]["attachment"] || [], fn attachment ->
 +      Enum.map(object.data["attachment"] || [], fn attachment ->
          url = hd(attachment["url"])
  
          {:link,
      mentions = activity.recipients |> get_mentions
  
      categories =
 -      (activity.data["object"]["tag"] || [])
 +      (object.data["tag"] || [])
        |> Enum.map(fn tag ->
          if is_binary(tag) do
            {:category, [term: to_charlist(tag)], []}
        end)
        |> Enum.filter(& &1)
  
 -    emoji_links = get_emoji_links(activity.data["object"]["emoji"] || %{})
 +    emoji_links = get_emoji_links(object.data["emoji"] || %{})
  
      summary =
 -      if activity.data["object"]["summary"] do
 -        [{:summary, [], h.(activity.data["object"]["summary"])}]
 +      if object.data["summary"] do
 +        [{:summary, [], h.(object.data["summary"])}]
        else
          []
        end
        {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
        {:"activity:verb", ['http://activitystrea.ms/schema/1.0/post']},
        # For notes, federate the object id.
 -      {:id, h.(activity.data["object"]["id"])},
 +      {:id, h.(object.data["id"])},
        {:title, ['New note by #{user.nickname}']},
 -      {:content, [type: 'html'],
 -       h.(activity.data["object"]["content"] |> String.replace(~r/[\n\r]/, ""))},
 +      {:content, [type: 'html'], h.(object.data["content"] |> String.replace(~r/[\n\r]/, ""))},
        {:published, h.(inserted_at)},
        {:updated, h.(updated_at)},
        {:"ostatus:conversation", [ref: h.(activity.data["context"])],
        {:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}
      ] ++
        summary ++
 -      get_links(activity) ++
 +      get_links(activity, object.data) ++
        categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links
    end
  
      _in_reply_to = get_in_reply_to(activity.data)
      author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
  
-     retweeted_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
+     retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
      retweeted_user = User.get_cached_by_ap_id(retweeted_activity.data["actor"])
  
      retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
index ba232b0ecb7fa45a9841db5409f1ca121fd42d8f,db995ec77d2307da309192dc57dcefe9fdbb77a5..ec6e5cfaf3226cdb61e4ba7b1e72d3bba161a532
@@@ -1,10 -1,17 +1,17 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.OStatus.NoteHandler do
    require Logger
-   alias Pleroma.Web.{XML, OStatus}
-   alias Pleroma.{Object, Activity}
+   alias Pleroma.Activity
+   alias Pleroma.Object
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Utils
    alias Pleroma.Web.CommonAPI
+   alias Pleroma.Web.OStatus
+   alias Pleroma.Web.XML
  
    @doc """
    Get the context for this note. Uses this:
    2. The conversation reference in the ostatus xml
    3. A newly generated context id.
    """
-   def get_context(entry, inReplyTo) do
+   def get_context(entry, in_reply_to) do
      context =
        (XML.string_from_xpath("//ostatus:conversation[1]", entry) ||
           XML.string_from_xpath("//ostatus:conversation[1]/@ref", entry) || "")
        |> String.trim()
  
-     with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(inReplyTo) do
+     with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(in_reply_to) do
        context
      else
        _e ->
      Map.put(note, "external_url", url)
    end
  
-   def fetch_replied_to_activity(entry, inReplyTo) do
-     with %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(inReplyTo) do
+   def fetch_replied_to_activity(entry, in_reply_to) do
+     with %Activity{} = activity <- Activity.get_create_by_object_ap_id(in_reply_to) do
        activity
      else
        _e ->
-         with inReplyToHref when not is_nil(inReplyToHref) <-
+         with in_reply_to_href when not is_nil(in_reply_to_href) <-
                 XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry),
-              {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(inReplyToHref) do
+              {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href) do
            activity
          else
            _e -> nil
    # TODO: Clean this up a bit.
    def handle_note(entry, doc \\ nil) do
      with id <- XML.string_from_xpath("//id", entry),
-          activity when is_nil(activity) <- Activity.get_create_activity_by_object_ap_id(id),
+          activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id),
           [author] <- :xmerl_xpath.string('//author[1]', doc),
           {:ok, actor} <- OStatus.find_make_or_update_user(author),
           content_html <- OStatus.get_content(entry),
           cw <- OStatus.get_cw(entry),
-          inReplyTo <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
-          inReplyToActivity <- fetch_replied_to_activity(entry, inReplyTo),
-          inReplyToObject <-
-            (inReplyToActivity && Object.normalize(inReplyToActivity.data["object"])) || nil,
-          inReplyTo <- (inReplyToObject && inReplyToObject.data["id"]) || inReplyTo,
+          in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
+          in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to),
 -         in_reply_to <-
 -           (in_reply_to_activity && in_reply_to_activity.data["object"]["id"]) || in_reply_to,
++         in_reply_to_object <-
++           (in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil,
++         in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to,
           attachments <- OStatus.get_attachments(entry),
-          context <- get_context(entry, inReplyTo),
+          context <- get_context(entry, in_reply_to),
           tags <- OStatus.get_tags(entry),
           mentions <- get_mentions(entry),
           to <- make_to_list(actor, mentions),
               context,
               content_html,
               attachments,
-              inReplyToActivity,
+              in_reply_to_activity,
               [],
               cw
             ),
           # TODO: Handle this case in make_note_data
           note <-
             if(
-              inReplyTo && !inReplyToActivity,
-              do: note |> Map.put("inReplyTo", inReplyTo),
+              in_reply_to && !in_reply_to_activity,
+              do: note |> Map.put("inReplyTo", in_reply_to),
               else: note
             ) do
        ActivityPub.create(%{
index b0ed8387e904c5e2083cac6accd3af4c60e6d089,ed45ca73574fc62b407ff0429f08beb3f88c107f..9441984c7e1de3ba74559385c2a6e214e2b11f7e
@@@ -1,18 -1,28 +1,28 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.TwitterAPI.UtilController do
    use Pleroma.Web, :controller
    require Logger
+   alias Comeonin.Pbkdf2
+   alias Pleroma.Activity
+   alias Pleroma.Emoji
+   alias Pleroma.Notification
+   alias Pleroma.PasswordResetToken
+   alias Pleroma.Repo
+   alias Pleroma.User
    alias Pleroma.Web
+   alias Pleroma.Web.ActivityPub.ActivityPub
+   alias Pleroma.Web.CommonAPI
    alias Pleroma.Web.OStatus
    alias Pleroma.Web.WebFinger
-   alias Pleroma.Web.CommonAPI
-   alias Comeonin.Pbkdf2
-   alias Pleroma.{Formatter, Emoji}
-   alias Pleroma.Web.ActivityPub.ActivityPub
-   alias Pleroma.{Repo, PasswordResetToken, User}
  
    def show_password_reset(conn, %{"token" => token}) do
      with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
-          %User{} = user <- Repo.get(User, token.user_id) do
+          %User{} = user <- User.get_by_id(token.user_id) do
        render(conn, "password_reset.html", %{
          token: token,
          user: user
    end
  
    def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
-     {err, followee} = OStatus.find_or_make_user(acct)
-     avatar = User.avatar_url(followee)
-     name = followee.nickname
-     id = followee.id
-     if !!user do
-       conn
-       |> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id})
+     if is_status?(acct) do
 -      {:ok, object} = ActivityPub.fetch_object_from_id(acct)
++      {:ok, object} = Pleroma.Object.Fetcher.fetch_object_from_id(acct)
+       %Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"])
+       redirect(conn, to: "/notice/#{activity_id}")
      else
-       conn
-       |> render("follow_login.html", %{
-         error: false,
-         acct: acct,
-         avatar: avatar,
-         name: name,
-         id: id
-       })
+       {err, followee} = OStatus.find_or_make_user(acct)
+       avatar = User.avatar_url(followee)
+       name = followee.nickname
+       id = followee.id
+       if !!user do
+         conn
+         |> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id})
+       else
+         conn
+         |> render("follow_login.html", %{
+           error: false,
+           acct: acct,
+           avatar: avatar,
+           name: name,
+           id: id
+         })
+       end
+     end
+   end
+   defp is_status?(acct) do
 -    case ActivityPub.fetch_and_contain_remote_object_from_id(acct) do
++    case Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(acct) do
+       {:ok, %{"type" => type}} when type in ["Article", "Note", "Video", "Page", "Question"] ->
+         true
+       _ ->
+         false
      end
    end
  
    def do_remote_follow(conn, %{
          "authorization" => %{"name" => username, "password" => password, "id" => id}
        }) do
-     followee = Repo.get(User, id)
+     followee = User.get_by_id(id)
      avatar = User.avatar_url(followee)
      name = followee.nickname
  
      with %User{} = user <- User.get_cached_by_nickname(username),
           true <- Pbkdf2.checkpw(password, user.password_hash),
-          %User{} = _followed <- Repo.get(User, id),
+          %User{} = _followed <- User.get_by_id(id),
           {:ok, follower} <- User.follow(user, followee),
           {:ok, _activity} <- ActivityPub.follow(follower, followee) do
        conn
    end
  
    def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do
-     with %User{} = followee <- Repo.get(User, id),
+     with %User{} = followee <- User.get_by_id(id),
           {:ok, follower} <- User.follow(user, followee),
           {:ok, _activity} <- ActivityPub.follow(follower, followee) do
        conn
      end
    end
  
+   def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
+     with {:ok, _} <- Notification.read_one(user, notification_id) do
+       json(conn, %{status: "success"})
+     else
+       {:error, message} ->
+         conn
+         |> put_resp_content_type("application/json")
+         |> send_resp(403, Jason.encode!(%{"error" => message}))
+     end
+   end
    def config(conn, _params) do
      instance = Pleroma.Config.get(:instance)
      instance_fe = Pleroma.Config.get(:fe)
          |> send_resp(200, response)
  
        _ ->
+         vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
+         uploadlimit = %{
+           uploadlimit: to_string(Keyword.get(instance, :upload_limit)),
+           avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)),
+           backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)),
+           bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit))
+         }
          data = %{
            name: Keyword.get(instance, :name),
            description: Keyword.get(instance, :description),
            server: Web.base_url(),
            textlimit: to_string(Keyword.get(instance, :limit)),
+           uploadlimit: uploadlimit,
            closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"),
-           private: if(Keyword.get(instance, :public, true), do: "0", else: "1")
+           private: if(Keyword.get(instance, :public, true), do: "0", else: "1"),
+           vapidPublicKey: vapid_public_key,
+           accountActivationRequired:
+             if(Keyword.get(instance, :account_activation_required, false), do: "1", else: "0"),
+           invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0"),
+           safeDMMentionsEnabled:
+             if(Pleroma.Config.get([:instance, :safe_dm_mentions]), do: "1", else: "0")
          }
  
-         pleroma_fe = %{
-           theme: Keyword.get(instance_fe, :theme),
-           background: Keyword.get(instance_fe, :background),
-           logo: Keyword.get(instance_fe, :logo),
-           logoMask: Keyword.get(instance_fe, :logo_mask),
-           logoMargin: Keyword.get(instance_fe, :logo_margin),
-           redirectRootNoLogin: Keyword.get(instance_fe, :redirect_root_no_login),
-           redirectRootLogin: Keyword.get(instance_fe, :redirect_root_login),
-           chatDisabled: !Keyword.get(instance_chat, :enabled),
-           showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel),
-           scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled),
-           formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled),
-           collapseMessageWithSubject: Keyword.get(instance_fe, :collapse_message_with_subject),
-           hidePostStats: Keyword.get(instance_fe, :hide_post_stats),
-           hideUserStats: Keyword.get(instance_fe, :hide_user_stats)
-         }
+         pleroma_fe =
+           if instance_fe do
+             %{
+               theme: Keyword.get(instance_fe, :theme),
+               background: Keyword.get(instance_fe, :background),
+               logo: Keyword.get(instance_fe, :logo),
+               logoMask: Keyword.get(instance_fe, :logo_mask),
+               logoMargin: Keyword.get(instance_fe, :logo_margin),
+               redirectRootNoLogin: Keyword.get(instance_fe, :redirect_root_no_login),
+               redirectRootLogin: Keyword.get(instance_fe, :redirect_root_login),
+               chatDisabled: !Keyword.get(instance_chat, :enabled),
+               showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel),
+               scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled),
+               formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled),
+               collapseMessageWithSubject:
+                 Keyword.get(instance_fe, :collapse_message_with_subject),
+               hidePostStats: Keyword.get(instance_fe, :hide_post_stats),
+               hideUserStats: Keyword.get(instance_fe, :hide_user_stats),
+               scopeCopy: Keyword.get(instance_fe, :scope_copy),
+               subjectLineBehavior: Keyword.get(instance_fe, :subject_line_behavior),
+               alwaysShowSubjectInput: Keyword.get(instance_fe, :always_show_subject_input)
+             }
+           else
+             Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
+           end
  
          managed_config = Keyword.get(instance, :managed_config)
  
      end
    end
  
+   def frontend_configurations(conn, _params) do
+     config =
+       Pleroma.Config.get(:frontend_configurations, %{})
+       |> Enum.into(%{})
+     json(conn, config)
+   end
    def version(conn, _params) do
      version = Pleroma.Application.named_version()
  
    end
  
    def emoji(conn, _params) do
-     json(conn, Enum.into(Emoji.get_all(), %{}))
+     emoji =
+       Emoji.get_all()
+       |> Enum.map(fn {short_code, path, tags} ->
+         {short_code, %{image_url: path, tags: String.split(tags, ",")}}
+       end)
+       |> Enum.into(%{})
+     json(conn, emoji)
+   end
+   def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do
+     with {:ok, _} <- User.update_notification_settings(user, params) do
+       json(conn, %{status: "success"})
+     end
    end
  
    def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
      follow_import(conn, %{"list" => File.read!(listfile.path)})
    end
  
-   def follow_import(%{assigns: %{user: user}} = conn, %{"list" => list}) do
-     Task.start(fn ->
-       String.split(list)
-       |> Enum.map(fn account ->
-         with %User{} = follower <- User.get_cached_by_ap_id(user.ap_id),
-              %User{} = followed <- User.get_or_fetch(account),
-              {:ok, follower} <- User.maybe_direct_follow(follower, followed) do
-           ActivityPub.follow(follower, followed)
-         else
-           err -> Logger.debug("follow_import: following #{account} failed with #{inspect(err)}")
-         end
-       end)
-     end)
+   def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do
+     with lines <- String.split(list, "\n"),
+          followed_identifiers <-
+            Enum.map(lines, fn line ->
+              String.split(line, ",") |> List.first()
+            end)
+            |> List.delete("Account address"),
+          {:ok, _} = Task.start(fn -> User.follow_import(follower, followed_identifiers) end) do
+       json(conn, "job started")
+     end
+   end
+   def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
+     blocks_import(conn, %{"list" => File.read!(listfile.path)})
+   end
  
-     json(conn, "job started")
+   def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do
+     with blocked_identifiers <- String.split(list),
+          {:ok, _} = Task.start(fn -> User.blocks_import(blocker, blocked_identifiers) end) do
+       json(conn, "job started")
+     end
    end
  
    def change_password(%{assigns: %{user: user}} = conn, params) do
          json(conn, %{error: msg})
      end
    end
+   def captcha(conn, _params) do
+     json(conn, Pleroma.Captcha.new())
+   end
  end
index 18b2ebb0b69aba708f16a05b5f9ef5ae48c335e3,ecb2b437ba4cff9249284a7ca2da2d6ce6474975..c64152da8f844cde4f5e70dfdee7fba3bb7af7bf
@@@ -1,19 -1,24 +1,24 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.TwitterAPI.ActivityView do
    use Pleroma.Web, :view
-   alias Pleroma.Web.CommonAPI.Utils
-   alias Pleroma.User
-   alias Pleroma.Web.TwitterAPI.UserView
-   alias Pleroma.Web.TwitterAPI.ActivityView
-   alias Pleroma.Web.TwitterAPI.TwitterAPI
-   alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
    alias Pleroma.Activity
-   alias Pleroma.Object
-   alias Pleroma.User
-   alias Pleroma.Repo
    alias Pleroma.Formatter
    alias Pleroma.HTML
+   alias Pleroma.Object
+   alias Pleroma.Repo
+   alias Pleroma.User
+   alias Pleroma.Web.CommonAPI
+   alias Pleroma.Web.CommonAPI.Utils
+   alias Pleroma.Web.MastodonAPI.StatusView
+   alias Pleroma.Web.TwitterAPI.ActivityView
+   alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
+   alias Pleroma.Web.TwitterAPI.UserView
  
    import Ecto.Query
+   require Logger
  
    defp query_context_ids([]), do: []
  
@@@ -72,7 -77,7 +77,7 @@@
    defp get_context_id(%{data: %{"context" => context}}, options) do
      cond do
        id = options[:context_ids][context] -> id
-       true -> TwitterAPI.context_to_conversation_id(context)
+       true -> Utils.context_to_conversation_id(context)
      end
    end
  
        ap_id == "https://www.w3.org/ns/activitystreams#Public" ->
          nil
  
+       user = User.get_cached_by_ap_id(ap_id) ->
+         user
+       user = User.get_by_guessed_nickname(ap_id) ->
+         user
        true ->
-         User.get_cached_by_ap_id(ap_id)
+         User.error_user(ap_id)
      end
    end
  
        |> Map.put(:context_ids, context_ids)
        |> Map.put(:users, users)
  
-     render_many(
+     safe_render_many(
        opts.activities,
        ActivityView,
        "activity.json",
    def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activity} = opts) do
      user = get_user(activity.data["actor"], opts)
      created_at = activity.data["published"] |> Utils.date_to_asctime()
-     announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
+     announced_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
  
      text = "#{user.nickname} retweeted a status."
  
  
    def render("activity.json", %{activity: %{data: %{"type" => "Like"}} = activity} = opts) do
      user = get_user(activity.data["actor"], opts)
-     liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
+     liked_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
      liked_activity_id = if liked_activity, do: liked_activity.id, else: nil
  
      created_at =
  
      text = "#{user.nickname} favorited a status."
  
+     favorited_status =
+       if liked_activity,
+         do: render("activity.json", Map.merge(opts, %{activity: liked_activity})),
+         else: nil
      %{
        "id" => activity.id,
        "user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
        "is_post_verb" => false,
        "uri" => "tag:#{activity.data["id"]}:objectType=Favourite",
        "created_at" => created_at,
+       "favorited_status" => favorited_status,
        "in_reply_to_status_id" => liked_activity_id,
        "external_url" => activity.data["id"],
        "activity_type" => "like"
  
    def render(
          "activity.json",
 -        %{activity: %{data: %{"type" => "Create", "object" => object}} = activity} = opts
 +        %{activity: %{data: %{"type" => "Create", "object" => object_id}} = activity} = opts
        ) do
      user = get_user(activity.data["actor"], opts)
  
 -    created_at = object["published"] |> Utils.date_to_asctime()
 -    like_count = object["like_count"] || 0
 -    announcement_count = object["announcement_count"] || 0
 -    favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
 -    repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
 +    object = Object.normalize(object_id)
 +
 +    created_at = object.data["published"] |> Utils.date_to_asctime()
 +    like_count = object.data["like_count"] || 0
 +    announcement_count = object.data["announcement_count"] || 0
 +    favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
 +    repeated = opts[:for] && opts[:for].ap_id in (object.data["announcements"] || [])
+     pinned = activity.id in user.info.pinned_activities
  
      attentions =
-       activity.recipients
+       []
+       |> Utils.maybe_notify_to_recipients(activity)
+       |> Utils.maybe_notify_mentioned_recipients(activity)
        |> Enum.map(fn ap_id -> get_user(ap_id, opts) end)
        |> Enum.filter(& &1)
        |> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
  
      conversation_id = get_context_id(activity, opts)
  
 -    tags = activity.data["object"]["tag"] || []
 -    possibly_sensitive = activity.data["object"]["sensitive"] || Enum.member?(tags, "nsfw")
 +    tags = object.data["tag"] || []
 +    possibly_sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
  
      tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags
  
 -    {summary, content} = render_content(object)
 +    {summary, content} = render_content(object.data)
  
      html =
-       HTML.filter_tags(content, User.html_filter_policy(opts[:for]))
+       content
+       |> HTML.get_cached_scrubbed_html_for_activity(
+         User.html_filter_policy(opts[:for]),
+         activity,
+         "twitterapi:content"
+       )
 -      |> Formatter.emojify(object["emoji"])
 +      |> Formatter.emojify(object.data["emoji"])
  
+     text =
+       if content do
+         content
+         |> String.replace(~r/<br\s?\/?>/, "\n")
+         |> HTML.get_cached_stripped_html_for_activity(activity, "twitterapi:content")
+       else
+         ""
+       end
      reply_parent = Activity.get_in_reply_to_activity(activity)
  
      reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)
  
+     summary = HTML.strip_tags(summary)
+     card =
+       StatusView.render(
+         "card.json",
+         Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+       )
      %{
        "id" => activity.id,
 -      "uri" => activity.data["object"]["id"],
 +      "uri" => object.data["id"],
        "user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
        "statusnet_html" => html,
-       "text" => HTML.strip_tags(content),
+       "text" => text,
        "is_local" => activity.local,
        "is_post_verb" => true,
        "created_at" => created_at,
-       "in_reply_to_status_id" => object.data["inReplyToStatusId"],
+       "in_reply_to_status_id" => reply_parent && reply_parent.id,
        "in_reply_to_screen_name" => reply_user && reply_user.nickname,
        "in_reply_to_profileurl" => User.profile_url(reply_user),
        "in_reply_to_ostatus_uri" => reply_user && reply_user.ap_id,
        "in_reply_to_user_id" => reply_user && reply_user.id,
        "statusnet_conversation_id" => conversation_id,
 -      "attachments" => (object["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
 +      "attachments" => (object.data["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
        "attentions" => attentions,
        "fave_num" => like_count,
        "repeat_num" => announcement_count,
        "favorited" => !!favorited,
        "repeated" => !!repeated,
 -      "external_url" => object["external_url"] || object["id"],
+       "pinned" => pinned,
 +      "external_url" => object.data["external_url"] || object.data["id"],
        "tags" => tags,
        "activity_type" => "post",
        "possibly_sensitive" => possibly_sensitive,
-       "visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object.data),
-       "summary" => summary
+       "visibility" => StatusView.get_visibility(object),
+       "summary" => summary,
 -      "summary_html" => summary |> Formatter.emojify(object["emoji"]),
++      "summary_html" => summary |> Formatter.emojify(object.data["emoji"]),
+       "card" => card,
+       "muted" => CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user)
      }
    end
  
+   def render("activity.json", %{activity: unhandled_activity}) do
+     Logger.warn("#{__MODULE__} unhandled activity: #{inspect(unhandled_activity)}")
+     nil
+   end
    def render_content(%{"type" => "Note"} = object) do
      summary = object["summary"]
  
      {summary, content}
    end
  
-   def render_content(%{"type" => object_type} = object) when object_type in ["Article", "Page"] do
+   def render_content(%{"type" => object_type} = object)
+       when object_type in ["Article", "Page", "Video"] do
      summary = object["name"] || object["summary"]
  
      content =
diff --combined test/object_test.exs
index dac6c3be78393d38fefa1d98b5262aeb2819be52,911757d57c601e83f5e54dbd668247d6e818c2ab..a30efd48c7791cfca8c3c66040c2579f74188a39
@@@ -1,7 -1,12 +1,12 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.ObjectTest do
    use Pleroma.DataCase
    import Pleroma.Factory
-   alias Pleroma.{Repo, Object}
+   alias Pleroma.Object
+   alias Pleroma.Repo
  
    test "returns an object by it's AP id" do
      object = insert(:note)
@@@ -32,6 -37,8 +37,8 @@@
        found_object = Object.get_by_ap_id(object.data["id"])
  
        refute object == found_object
+       assert found_object.data["type"] == "Tombstone"
      end
  
      test "ensures cache is cleared for the object" do
        cached_object = Object.get_cached_by_ap_id(object.data["id"])
  
        refute object == cached_object
+       assert cached_object.data["type"] == "Tombstone"
      end
    end
 +
 +  describe "normalizer" do
 +    test "fetches unknown objects by default" do
 +      %Object{} =
 +        object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367")
 +
 +      assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367"
 +    end
 +
 +    test "fetches unknown objects when fetch_remote is explicitly true" do
 +      %Object{} =
 +        object = Object.normalize("http://mastodon.example.org/@admin/99541947525187367", true)
 +
 +      assert object.data["url"] == "http://mastodon.example.org/@admin/99541947525187367"
 +    end
 +
 +    test "does not fetch unknown objects when fetch_remote is false" do
 +      assert is_nil(
 +               Object.normalize("http://mastodon.example.org/@admin/99541947525187367", false)
 +             )
 +    end
 +  end
  end
index bc9fcc75df0427670f525f7482632913bc292108,17fec05b102e193183d09a3b30453a2cebd86a0d..68bfb3858f7a89f2199da7f47bbf638353cf17b7
@@@ -1,12 -1,70 +1,70 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
    use Pleroma.DataCase
+   alias Pleroma.Activity
+   alias Pleroma.Builders.ActivityBuilder
+   alias Pleroma.Instances
+   alias Pleroma.Object
+   alias Pleroma.User
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Utils
    alias Pleroma.Web.CommonAPI
-   alias Pleroma.{Activity, Object, User}
-   alias Pleroma.Builders.ActivityBuilder
  
    import Pleroma.Factory
+   import Tesla.Mock
+   import Mock
+   setup do
+     mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+     :ok
+   end
+   describe "fetching restricted by visibility" do
+     test "it restricts by the appropriate visibility" do
+       user = insert(:user)
+       {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
+       {:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
+       {:ok, unlisted_activity} =
+         CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
+       {:ok, private_activity} =
+         CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
+       activities =
+         ActivityPub.fetch_activities([], %{:visibility => "direct", "actor_id" => user.ap_id})
+       assert activities == [direct_activity]
+       activities =
+         ActivityPub.fetch_activities([], %{:visibility => "unlisted", "actor_id" => user.ap_id})
+       assert activities == [unlisted_activity]
+       activities =
+         ActivityPub.fetch_activities([], %{:visibility => "private", "actor_id" => user.ap_id})
+       assert activities == [private_activity]
+       activities =
+         ActivityPub.fetch_activities([], %{:visibility => "public", "actor_id" => user.ap_id})
+       assert activities == [public_activity]
+       activities =
+         ActivityPub.fetch_activities([], %{
+           :visibility => ~w[private public],
+           "actor_id" => user.ap_id
+         })
+       assert activities == [public_activity, private_activity]
+     end
+   end
  
    describe "building a user from his ap id" do
      test "it returns a user" do
        assert user.info.ap_enabled
        assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
      end
+     test "it fetches the appropriate tag-restricted posts" do
+       user = insert(:user)
+       {:ok, status_one} = CommonAPI.post(user, %{"status" => ". #test"})
+       {:ok, status_two} = CommonAPI.post(user, %{"status" => ". #essais"})
+       {:ok, status_three} = CommonAPI.post(user, %{"status" => ". #test #reject"})
+       fetch_one = ActivityPub.fetch_activities([], %{"tag" => "test"})
+       fetch_two = ActivityPub.fetch_activities([], %{"tag" => ["test", "essais"]})
+       fetch_three =
+         ActivityPub.fetch_activities([], %{
+           "tag" => ["test", "essais"],
+           "tag_reject" => ["reject"]
+         })
+       fetch_four =
+         ActivityPub.fetch_activities([], %{
+           "tag" => ["test"],
+           "tag_all" => ["test", "reject"]
+         })
+       assert fetch_one == [status_one, status_three]
+       assert fetch_two == [status_one, status_two, status_three]
+       assert fetch_three == [status_one, status_two]
+       assert fetch_four == [status_three]
+     end
    end
  
    describe "insertion" do
+     test "drops activities beyond a certain limit" do
+       limit = Pleroma.Config.get([:instance, :remote_limit])
+       random_text =
+         :crypto.strong_rand_bytes(limit + 1)
+         |> Base.encode64()
+         |> binary_part(0, limit + 1)
+       data = %{
+         "ok" => true,
+         "object" => %{
+           "content" => random_text
+         }
+       }
+       assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data)
+     end
+     test "doesn't drop activities with content being null" do
+       data = %{
+         "ok" => true,
+         "object" => %{
+           "content" => nil
+         }
+       }
+       assert {:ok, _} = ActivityPub.insert(data)
+     end
      test "returns the activity if one with the same id is already in" do
        activity = insert(:note_activity)
        {:ok, new_activity} = ActivityPub.insert(activity.data)
  
-       assert activity == new_activity
+       assert activity.id == new_activity.id
      end
  
      test "inserts a given map into the activity database, giving it an id if it has none." do
        }
  
        {:ok, %Activity{} = activity} = ActivityPub.insert(data)
 -      assert is_binary(activity.data["object"]["id"])
 -      assert %Object{} = Object.get_by_ap_id(activity.data["object"]["id"])
 +      object = Object.normalize(activity.data["object"])
 +
 +      assert is_binary(object.data["id"])
 +      assert %Object{} = Object.get_by_ap_id(activity.data["object"])
      end
    end
  
            to: ["user1", "user1", "user2"],
            actor: user,
            context: "",
 -          object: %{}
 +          object: %{
 +            "to" => ["user1", "user1", "user2"],
 +            "type" => "Note",
 +            "content" => "testing"
 +          }
          })
  
        assert activity.data["to"] == ["user1", "user2"]
        assert activity.actor == user.ap_id
-       assert activity.recipients == ["user1", "user2"]
+       assert activity.recipients == ["user1", "user2", user.ap_id]
+     end
+     test "increases user note count only for public activities" do
+       user = insert(:user)
+       {:ok, _} =
+         CommonAPI.post(User.get_by_id(user.id), %{"status" => "1", "visibility" => "public"})
+       {:ok, _} =
+         CommonAPI.post(User.get_by_id(user.id), %{"status" => "2", "visibility" => "unlisted"})
+       {:ok, _} =
+         CommonAPI.post(User.get_by_id(user.id), %{"status" => "2", "visibility" => "private"})
+       {:ok, _} =
+         CommonAPI.post(User.get_by_id(user.id), %{"status" => "3", "visibility" => "direct"})
+       user = User.get_by_id(user.id)
+       assert user.info.note_count == 2
+     end
+     test "increases replies count" do
+       user = insert(:user)
+       user2 = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
+       ap_id = activity.data["id"]
+       reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
+       # public
+       {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
+       assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
+       assert data["object"]["repliesCount"] == 1
+       assert object.data["repliesCount"] == 1
+       # unlisted
+       {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
+       assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
+       assert data["object"]["repliesCount"] == 2
+       assert object.data["repliesCount"] == 2
+       # private
+       {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
+       assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
+       assert data["object"]["repliesCount"] == 2
+       assert object.data["repliesCount"] == 2
+       # direct
+       {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
+       assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
+       assert data["object"]["repliesCount"] == 2
+       assert object.data["repliesCount"] == 2
      end
    end
  
      booster = insert(:user)
      {:ok, user} = User.block(user, %{ap_id: activity_one.data["actor"]})
  
-     activities = ActivityPub.fetch_activities([], %{"blocking_user" => user})
+     activities =
+       ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
  
      assert Enum.member?(activities, activity_two)
      assert Enum.member?(activities, activity_three)
  
      {:ok, user} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
  
-     activities = ActivityPub.fetch_activities([], %{"blocking_user" => user})
+     activities =
+       ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
  
      assert Enum.member?(activities, activity_two)
      assert Enum.member?(activities, activity_three)
  
      {:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]})
      {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
-     %Activity{} = boost_activity = Activity.get_create_activity_by_object_ap_id(id)
-     activity_three = Repo.get(Activity, activity_three.id)
+     %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
+     activity_three = Activity.get_by_id(activity_three.id)
+     activities =
+       ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
+     assert Enum.member?(activities, activity_two)
+     refute Enum.member?(activities, activity_three)
+     refute Enum.member?(activities, boost_activity)
+     assert Enum.member?(activities, activity_one)
+     activities =
+       ActivityPub.fetch_activities([], %{"blocking_user" => nil, "skip_preload" => true})
+     assert Enum.member?(activities, activity_two)
+     assert Enum.member?(activities, activity_three)
+     assert Enum.member?(activities, boost_activity)
+     assert Enum.member?(activities, activity_one)
+   end
+   test "doesn't return muted activities" do
+     activity_one = insert(:note_activity)
+     activity_two = insert(:note_activity)
+     activity_three = insert(:note_activity)
+     user = insert(:user)
+     booster = insert(:user)
+     {:ok, user} = User.mute(user, %User{ap_id: activity_one.data["actor"]})
+     activities =
+       ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
+     assert Enum.member?(activities, activity_two)
+     assert Enum.member?(activities, activity_three)
+     refute Enum.member?(activities, activity_one)
+     # Calling with 'with_muted' will deliver muted activities, too.
+     activities =
+       ActivityPub.fetch_activities([], %{
+         "muting_user" => user,
+         "with_muted" => true,
+         "skip_preload" => true
+       })
+     assert Enum.member?(activities, activity_two)
+     assert Enum.member?(activities, activity_three)
+     assert Enum.member?(activities, activity_one)
+     {:ok, user} = User.unmute(user, %User{ap_id: activity_one.data["actor"]})
+     activities =
+       ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
+     assert Enum.member?(activities, activity_two)
+     assert Enum.member?(activities, activity_three)
+     assert Enum.member?(activities, activity_one)
+     {:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]})
+     {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
+     %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
+     activity_three = Activity.get_by_id(activity_three.id)
  
-     activities = ActivityPub.fetch_activities([], %{"blocking_user" => user})
+     activities =
+       ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
  
      assert Enum.member?(activities, activity_two)
      refute Enum.member?(activities, activity_three)
      refute Enum.member?(activities, boost_activity)
      assert Enum.member?(activities, activity_one)
  
-     activities = ActivityPub.fetch_activities([], %{"blocking_user" => nil})
+     activities = ActivityPub.fetch_activities([], %{"muting_user" => nil, "skip_preload" => true})
  
      assert Enum.member?(activities, activity_two)
      assert Enum.member?(activities, activity_three)
      assert Enum.member?(activities, activity_one)
    end
  
+   test "does include announces on request" do
+     activity_three = insert(:note_activity)
+     user = insert(:user)
+     booster = insert(:user)
+     {:ok, user} = User.follow(user, booster)
+     {:ok, announce, _object} = CommonAPI.repeat(activity_three.id, booster)
+     [announce_activity] = ActivityPub.fetch_activities([user.ap_id | user.following])
+     assert announce_activity.id == announce.id
+   end
+   test "excludes reblogs on request" do
+     user = insert(:user)
+     {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})
+     {:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user})
+     [activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"})
+     assert activity == expected_activity
+   end
    describe "public fetch activities" do
      test "doesn't retrieve unlisted activities" do
        user = insert(:user)
  
-       {:ok, unlisted_activity} =
+       {:ok, _unlisted_activity} =
          CommonAPI.post(user, %{"status" => "yeah", "visibility" => "unlisted"})
  
        {:ok, listed_activity} = CommonAPI.post(user, %{"status" => "yeah"})
        assert length(activities) == 20
        assert last == last_expected
      end
+     test "doesn't return reblogs for users for whom reblogs have been muted" do
+       activity = insert(:note_activity)
+       user = insert(:user)
+       booster = insert(:user)
+       {:ok, user} = CommonAPI.hide_reblogs(user, booster)
+       {:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
+       activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
+       refute Enum.any?(activities, fn %{id: id} -> id == activity.id end)
+     end
+     test "returns reblogs for users for whom reblogs have not been muted" do
+       activity = insert(:note_activity)
+       user = insert(:user)
+       booster = insert(:user)
+       {:ok, user} = CommonAPI.hide_reblogs(user, booster)
+       {:ok, user} = CommonAPI.show_reblogs(user, booster)
+       {:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
+       activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
+       assert Enum.any?(activities, fn %{id: id} -> id == activity.id end)
+     end
    end
  
    describe "like an object" do
        assert like_activity == same_like_activity
        assert object.data["likes"] == [user.ap_id]
  
-       [note_activity] = Activity.all_by_object_ap_id(object.data["id"])
+       [note_activity] = Activity.get_all_create_by_object_ap_id(object.data["id"])
        assert note_activity.data["object"]["like_count"] == 1
  
        {:ok, _like_activity, object} = ActivityPub.like(user_two, object)
        {:ok, _, _, object} = ActivityPub.unlike(user, object)
        assert object.data["like_count"] == 0
  
-       assert Repo.get(Activity, like_activity.id) == nil
+       assert Activity.get_by_id(like_activity.id) == nil
      end
    end
  
        assert unannounce_activity.data["actor"] == user.ap_id
        assert unannounce_activity.data["context"] == announce_activity.data["context"]
  
-       assert Repo.get(Activity, announce_activity.id) == nil
+       assert Activity.get_by_id(announce_activity.id) == nil
      end
    end
  
      end
    end
  
 +  describe "fetch the latest Follow" do
 +    test "fetches the latest Follow activity" do
 +      %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
 +      follower = Repo.get_by(User, ap_id: activity.data["actor"])
 +      followed = Repo.get_by(User, ap_id: activity.data["object"])
 +
 +      assert activity == Utils.fetch_latest_follow(follower, followed)
 +    end
 +  end
 +
+   describe "fetching an object" do
+     test "it fetches an object" do
+       {:ok, object} =
+         ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
+       assert activity = Activity.get_create_by_object_ap_id(object.data["id"])
+       assert activity.data["id"]
+       {:ok, object_again} =
+         ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
+       assert [attachment] = object.data["attachment"]
+       assert is_list(attachment["url"])
+       assert object == object_again
+     end
+     test "it works with objects only available via Ostatus" do
+       {:ok, object} = ActivityPub.fetch_object_from_id("https://shitposter.club/notice/2827873")
+       assert activity = Activity.get_create_by_object_ap_id(object.data["id"])
+       assert activity.data["id"]
+       {:ok, object_again} =
+         ActivityPub.fetch_object_from_id("https://shitposter.club/notice/2827873")
+       assert object == object_again
+     end
+     test "it correctly stitches up conversations between ostatus and ap" do
+       last = "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
+       {:ok, object} = ActivityPub.fetch_object_from_id(last)
+       object = Object.get_by_ap_id(object.data["inReplyTo"])
+       assert object
+     end
+   end
    describe "following / unfollowing" do
      test "creates a follow activity" do
        follower = insert(:user)
        assert delete.data["actor"] == note.data["actor"]
        assert delete.data["object"] == note.data["object"]["id"]
  
-       assert Repo.get(Activity, delete.id) != nil
+       assert Activity.get_by_id(delete.id) != nil
  
-       assert Repo.get(Object, object.id) == nil
+       assert Repo.get(Object, object.id).data["type"] == "Tombstone"
+     end
+     test "decrements user note count only for public activities" do
+       user = insert(:user, info: %{note_count: 10})
+       {:ok, a1} =
+         CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "public"})
+       {:ok, a2} =
+         CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "unlisted"})
+       {:ok, a3} =
+         CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "private"})
+       {:ok, a4} =
+         CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "direct"})
+       {:ok, _} = a1.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete()
+       {:ok, _} = a2.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete()
+       {:ok, _} = a3.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete()
+       {:ok, _} = a4.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete()
+       user = User.get_by_id(user.id)
+       assert user.info.note_count == 10
+     end
+     test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do
+       user = insert(:user)
+       note = insert(:note_activity)
+       {:ok, object} =
+         Object.get_by_ap_id(note.data["object"]["id"])
+         |> Object.change(%{
+           data: %{
+             "actor" => note.data["object"]["actor"],
+             "id" => note.data["object"]["id"],
+             "to" => [user.ap_id],
+             "type" => "Note"
+           }
+         })
+         |> Object.update_and_set_cache()
+       {:ok, delete} = ActivityPub.delete(object)
+       assert user.ap_id in delete.data["to"]
+     end
+     test "decreases reply count" do
+       user = insert(:user)
+       user2 = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
+       reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
+       ap_id = activity.data["id"]
+       {:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
+       {:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
+       {:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
+       {:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
+       _ = CommonAPI.delete(direct_reply.id, user2)
+       assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
+       assert data["object"]["repliesCount"] == 2
+       assert object.data["repliesCount"] == 2
+       _ = CommonAPI.delete(private_reply.id, user2)
+       assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
+       assert data["object"]["repliesCount"] == 2
+       assert object.data["repliesCount"] == 2
+       _ = CommonAPI.delete(public_reply.id, user2)
+       assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
+       assert data["object"]["repliesCount"] == 1
+       assert object.data["repliesCount"] == 1
+       _ = CommonAPI.delete(unlisted_reply.id, user2)
+       assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
+       assert data["object"]["repliesCount"] == 0
+       assert object.data["repliesCount"] == 0
      end
    end
  
            "in_reply_to_status_id" => private_activity_2.id
          })
  
-       assert user1.following == [user3.ap_id <> "/followers", user1.ap_id]
        activities = ActivityPub.fetch_activities([user1.ap_id | user1.following])
  
+       private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
        assert [public_activity, private_activity_1, private_activity_3] == activities
        assert length(activities) == 3
  
      end
    end
  
 -  test "it can fetch plume articles" do
 -    {:ok, object} =
 -      ActivityPub.fetch_object_from_id(
 -        "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"
 -      )
 -
 -    assert object
 -  end
 -
    describe "update" do
      test "it creates an update activity with the new user data" do
        user = insert(:user)
      end
    end
  
+   test "it can fetch peertube videos" do
+     {:ok, object} =
+       ActivityPub.fetch_object_from_id(
+         "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
+       )
+     assert object
+   end
+   test "returned pinned statuses" do
+     Pleroma.Config.put([:instance, :max_pinned_statuses], 3)
+     user = insert(:user)
+     {:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
+     {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
+     {:ok, activity_three} = CommonAPI.post(user, %{"status" => "HI!!!"})
+     CommonAPI.pin(activity_one.id, user)
+     user = refresh_record(user)
+     CommonAPI.pin(activity_two.id, user)
+     user = refresh_record(user)
+     CommonAPI.pin(activity_three.id, user)
+     user = refresh_record(user)
+     activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"})
+     assert 3 = length(activities)
+   end
+   test "it can create a Flag activity" do
+     reporter = insert(:user)
+     target_account = insert(:user)
+     {:ok, activity} = CommonAPI.post(target_account, %{"status" => "foobar"})
+     context = Utils.generate_context_id()
+     content = "foobar"
+     reporter_ap_id = reporter.ap_id
+     target_ap_id = target_account.ap_id
+     activity_ap_id = activity.data["id"]
+     assert {:ok, activity} =
+              ActivityPub.flag(%{
+                actor: reporter,
+                context: context,
+                account: target_account,
+                statuses: [activity],
+                content: content
+              })
+     assert %Activity{
+              actor: ^reporter_ap_id,
+              data: %{
+                "type" => "Flag",
+                "content" => ^content,
+                "context" => ^context,
+                "object" => [^target_ap_id, ^activity_ap_id]
+              }
+            } = activity
+   end
+   describe "publish_one/1" do
+     test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified",
+                    Instances,
+                    [:passthrough],
+                    [] do
+       actor = insert(:user)
+       inbox = "http://200.site/users/nick1/inbox"
+       assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+       assert called(Instances.set_reachable(inbox))
+     end
+     test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set",
+                    Instances,
+                    [:passthrough],
+                    [] do
+       actor = insert(:user)
+       inbox = "http://200.site/users/nick1/inbox"
+       assert {:ok, _} =
+                ActivityPub.publish_one(%{
+                  inbox: inbox,
+                  json: "{}",
+                  actor: actor,
+                  id: 1,
+                  unreachable_since: NaiveDateTime.utc_now()
+                })
+       assert called(Instances.set_reachable(inbox))
+     end
+     test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil",
+                    Instances,
+                    [:passthrough],
+                    [] do
+       actor = insert(:user)
+       inbox = "http://200.site/users/nick1/inbox"
+       assert {:ok, _} =
+                ActivityPub.publish_one(%{
+                  inbox: inbox,
+                  json: "{}",
+                  actor: actor,
+                  id: 1,
+                  unreachable_since: nil
+                })
+       refute called(Instances.set_reachable(inbox))
+     end
+     test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
+                    Instances,
+                    [:passthrough],
+                    [] do
+       actor = insert(:user)
+       inbox = "http://404.site/users/nick1/inbox"
+       assert {:error, _} =
+                ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+       assert called(Instances.set_unreachable(inbox))
+     end
+     test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind",
+                    Instances,
+                    [:passthrough],
+                    [] do
+       actor = insert(:user)
+       inbox = "http://connrefused.site/users/nick1/inbox"
+       assert {:error, _} =
+                ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+       assert called(Instances.set_unreachable(inbox))
+     end
+     test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable",
+                    Instances,
+                    [:passthrough],
+                    [] do
+       actor = insert(:user)
+       inbox = "http://200.site/users/nick1/inbox"
+       assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+       refute called(Instances.set_unreachable(inbox))
+     end
+     test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`",
+                    Instances,
+                    [:passthrough],
+                    [] do
+       actor = insert(:user)
+       inbox = "http://connrefused.site/users/nick1/inbox"
+       assert {:error, _} =
+                ActivityPub.publish_one(%{
+                  inbox: inbox,
+                  json: "{}",
+                  actor: actor,
+                  id: 1,
+                  unreachable_since: NaiveDateTime.utc_now()
+                })
+       refute called(Instances.set_unreachable(inbox))
+     end
+   end
    def data_uri do
      File.read!("test/fixtures/avatar_data_uri")
    end
index ea9d9fe580b97c2a67f78bce9e853a7f9e0debea,c857a7ec1e44372f7a818b4454b7cfd413598be7..5559cdf879ffe963179fa246e9c74b6f596f404c
@@@ -1,17 -1,27 +1,27 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
    use Pleroma.DataCase
+   alias Pleroma.Activity
+   alias Pleroma.Object
+   alias Pleroma.Repo
+   alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Transmogrifier
    alias Pleroma.Web.ActivityPub.Utils
-   alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.OStatus
-   alias Pleroma.{Activity, Object}
-   alias Pleroma.User
-   alias Pleroma.Repo
    alias Pleroma.Web.Websub.WebsubClientSubscription
  
    import Pleroma.Factory
    alias Pleroma.Web.CommonAPI
  
+   setup_all do
+     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+     :ok
+   end
    describe "handle_incoming" do
      test "it ignores an incoming notice if we already have it" do
        activity = insert(:note_activity)
          |> Map.put("object", object)
  
        {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
 +      returned_object = Object.normalize(returned_activity.data["object"])
  
        assert activity =
-                Activity.get_create_activity_by_object_ap_id(
+                Activity.get_create_by_object_ap_id(
                   "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
                 )
  
 -      assert returned_activity.data["object"]["inReplyToAtomUri"] ==
 -               "https://shitposter.club/notice/2827873"
 +      assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
-       assert returned_object.data["inReplyToStatusId"] == activity.id
      end
  
      test "it works for incoming notices" do
@@@ -72,7 -80,7 +80,7 @@@
  
        assert data["actor"] == "http://mastodon.example.org/users/admin"
  
 -      object = data["object"]
 +      object = Object.normalize(data["object"]).data
        assert object["id"] == "http://mastodon.example.org/users/admin/statuses/99512778738411822"
  
        assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
        data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!()
  
        {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
 -      assert Enum.at(data["object"]["tag"], 2) == "moo"
 +      object = Object.normalize(data["object"])
 +
 +      assert Enum.at(object.data["tag"], 2) == "moo"
      end
  
      test "it works for incoming notices with contentMap" do
          File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!()
  
        {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
 +      object = Object.normalize(data["object"])
  
 -      assert data["object"]["content"] ==
 +      assert object.data["content"] ==
                 "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
      end
  
        data = File.read!("test/fixtures/kroeg-post-activity.json") |> Poison.decode!()
  
        {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
 +      object = Object.normalize(data["object"])
  
 -      assert data["object"]["content"] ==
 +      assert object.data["content"] ==
                 "<p>henlo from my Psion netBook</p><p>message sent from my Psion netBook</p>"
      end
  
        data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Poison.decode!()
  
        {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
 +      object = Object.normalize(data["object"])
  
 -      assert data["object"]["emoji"] == %{
 +      assert object.data["emoji"] == %{
                 "icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png"
               }
  
        data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Poison.decode!()
  
        {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
 +      object = Object.normalize(data["object"])
  
 -      assert "test" in data["object"]["tag"]
 +      assert "test" in object.data["tag"]
      end
  
      test "it works for incoming notices with url not being a string (prismo)" do
        data = File.read!("test/fixtures/prismo-url-map.json") |> Poison.decode!()
  
        {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
 +      object = Object.normalize(data["object"])
  
 -      assert data["object"]["url"] == "https://prismo.news/posts/83"
 +      assert object.data["url"] == "https://prismo.news/posts/83"
      end
  
+     test "it cleans up incoming notices which are not really DMs" do
+       user = insert(:user)
+       other_user = insert(:user)
+       to = [user.ap_id, other_user.ap_id]
+       data =
+         File.read!("test/fixtures/mastodon-post-activity.json")
+         |> Poison.decode!()
+         |> Map.put("to", to)
+         |> Map.put("cc", [])
+       object =
+         data["object"]
+         |> Map.put("to", to)
+         |> Map.put("cc", [])
+       data = Map.put(data, "object", object)
+       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+       assert data["to"] == []
+       assert data["cc"] == to
+       object = data["object"]
+       assert object["to"] == []
+       assert object["cc"] == to
+     end
      test "it works for incoming follow requests" do
        user = insert(:user)
  
        data =
          File.read!("test/fixtures/mastodon-like.json")
          |> Poison.decode!()
 -        |> Map.put("object", activity.data["object"]["id"])
 +        |> Map.put("object", activity.data["object"])
  
        {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
  
        assert data["actor"] == "http://mastodon.example.org/users/admin"
        assert data["type"] == "Like"
        assert data["id"] == "http://mastodon.example.org/users/admin#likes/2"
 -      assert data["object"] == activity.data["object"]["id"]
 +      assert data["object"] == activity.data["object"]
      end
  
      test "it returns an error for incoming unlikes wihout a like activity" do
        data =
          File.read!("test/fixtures/mastodon-undo-like.json")
          |> Poison.decode!()
 -        |> Map.put("object", activity.data["object"]["id"])
 +        |> Map.put("object", activity.data["object"])
  
        assert Transmogrifier.handle_incoming(data) == :error
      end
        like_data =
          File.read!("test/fixtures/mastodon-like.json")
          |> Poison.decode!()
 -        |> Map.put("object", activity.data["object"]["id"])
 +        |> Map.put("object", activity.data["object"])
  
        {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
  
        assert data["object"] ==
                 "http://mastodon.example.org/users/admin/statuses/99541947525187367"
  
-       assert Activity.get_create_activity_by_object_ap_id(data["object"])
+       assert Activity.get_create_by_object_ap_id(data["object"])
      end
  
      test "it works for incoming announces with an existing activity" do
        data =
          File.read!("test/fixtures/mastodon-announce.json")
          |> Poison.decode!()
 -        |> Map.put("object", activity.data["object"]["id"])
 +        |> Map.put("object", activity.data["object"])
  
        {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
  
        assert data["id"] ==
                 "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
  
 -      assert data["object"] == activity.data["object"]["id"]
 +      assert data["object"] == activity.data["object"]
  
-       assert Activity.get_create_activity_by_object_ap_id(data["object"]).id == activity.id
+       assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id
+     end
+     test "it does not clobber the addressing on announce activities" do
+       user = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
+       data =
+         File.read!("test/fixtures/mastodon-announce.json")
+         |> Poison.decode!()
+         |> Map.put("object", activity.data["object"]["id"])
+         |> Map.put("to", ["http://mastodon.example.org/users/admin/followers"])
+         |> Map.put("cc", [])
+       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+       assert data["to"] == ["http://mastodon.example.org/users/admin/followers"]
+     end
+     test "it ensures that as:Public activities make it to their followers collection" do
+       user = insert(:user)
+       data =
+         File.read!("test/fixtures/mastodon-post-activity.json")
+         |> Poison.decode!()
+         |> Map.put("actor", user.ap_id)
+         |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
+         |> Map.put("cc", [])
+       object =
+         data["object"]
+         |> Map.put("attributedTo", user.ap_id)
+         |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
+         |> Map.put("cc", [])
+       data = Map.put(data, "object", object)
+       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+       assert data["cc"] == [User.ap_followers(user)]
+     end
+     test "it ensures that address fields become lists" do
+       user = insert(:user)
+       data =
+         File.read!("test/fixtures/mastodon-post-activity.json")
+         |> Poison.decode!()
+         |> Map.put("actor", user.ap_id)
+         |> Map.put("to", nil)
+         |> Map.put("cc", nil)
+       object =
+         data["object"]
+         |> Map.put("attributedTo", user.ap_id)
+         |> Map.put("to", nil)
+         |> Map.put("cc", nil)
+       data = Map.put(data, "object", object)
+       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+       assert !is_nil(data["to"])
+       assert !is_nil(data["cc"])
      end
  
      test "it works for incoming update activities" do
  
        object =
          data["object"]
 -        |> Map.put("id", activity.data["object"]["id"])
 +        |> Map.put("id", activity.data["object"])
  
        data =
          data
  
        {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
  
-       refute Repo.get(Activity, activity.id)
+       refute Activity.get_by_id(activity.id)
      end
  
      test "it fails for incoming deletes with spoofed origin" do
  
        object =
          data["object"]
 -        |> Map.put("id", activity.data["object"]["id"])
 +        |> Map.put("id", activity.data["object"])
  
        data =
          data
  
        :error = Transmogrifier.handle_incoming(data)
  
-       assert Repo.get(Activity, activity.id)
+       assert Activity.get_by_id(activity.id)
      end
  
      test "it works for incoming unannounces with an existing notice" do
        announce_data =
          File.read!("test/fixtures/mastodon-announce.json")
          |> Poison.decode!()
 -        |> Map.put("object", activity.data["object"]["id"])
 +        |> Map.put("object", activity.data["object"])
  
        {:ok, %Activity{data: announce_data, local: false}} =
          Transmogrifier.handle_incoming(announce_data)
  
        assert data["type"] == "Undo"
        assert data["object"]["type"] == "Announce"
 -      assert data["object"]["object"] == activity.data["object"]["id"]
 +      assert data["object"]["object"] == activity.data["object"]
  
        assert data["object"]["id"] ==
                 "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
  
        assert activity.data["object"] == follow_activity.data["id"]
  
-       follower = Repo.get(User, follower.id)
+       follower = User.get_by_id(follower.id)
  
        assert User.following?(follower, followed) == true
      end
        {:ok, activity} = Transmogrifier.handle_incoming(accept_data)
        assert activity.data["object"] == follow_activity.data["id"]
  
-       follower = Repo.get(User, follower.id)
+       follower = User.get_by_id(follower.id)
  
        assert User.following?(follower, followed) == true
      end
        {:ok, activity} = Transmogrifier.handle_incoming(accept_data)
        assert activity.data["object"] == follow_activity.data["id"]
  
-       follower = Repo.get(User, follower.id)
+       follower = User.get_by_id(follower.id)
  
        assert User.following?(follower, followed) == true
      end
  
        :error = Transmogrifier.handle_incoming(accept_data)
  
-       follower = Repo.get(User, follower.id)
+       follower = User.get_by_id(follower.id)
  
        refute User.following?(follower, followed) == true
      end
  
        :error = Transmogrifier.handle_incoming(accept_data)
  
-       follower = Repo.get(User, follower.id)
+       follower = User.get_by_id(follower.id)
  
        refute User.following?(follower, followed) == true
      end
        {:ok, activity} = Transmogrifier.handle_incoming(reject_data)
        refute activity.local
  
-       follower = Repo.get(User, follower.id)
+       follower = User.get_by_id(follower.id)
  
        assert User.following?(follower, followed) == false
      end
  
        {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data)
  
-       follower = Repo.get(User, follower.id)
+       follower = User.get_by_id(follower.id)
  
        assert User.following?(follower, followed) == false
      end
  
        :error = Transmogrifier.handle_incoming(data)
      end
+     test "it remaps video URLs as attachments if necessary" do
+       {:ok, object} =
+         ActivityPub.fetch_object_from_id(
+           "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
+         )
+       attachment = %{
+         "type" => "Link",
+         "mediaType" => "video/mp4",
+         "href" =>
+           "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
+         "mimeType" => "video/mp4",
+         "size" => 5_015_880,
+         "url" => [
+           %{
+             "href" =>
+               "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
+             "mediaType" => "video/mp4",
+             "type" => "Link"
+           }
+         ],
+         "width" => 480
+       }
+       assert object.data["url"] ==
+                "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
+       assert object.data["attachment"] == [attachment]
+     end
+     test "it accepts Flag activities" do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
+       object = Object.normalize(activity.data["object"])
+       message = %{
+         "@context" => "https://www.w3.org/ns/activitystreams",
+         "cc" => [user.ap_id],
+         "object" => [user.ap_id, object.data["id"]],
+         "type" => "Flag",
+         "content" => "blocked AND reported!!!",
+         "actor" => other_user.ap_id
+       }
+       assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+       assert activity.data["object"] == [user.ap_id, object.data["id"]]
+       assert activity.data["content"] == "blocked AND reported!!!"
+       assert activity.data["actor"] == other_user.ap_id
+       assert activity.data["cc"] == [user.ap_id]
+     end
    end
  
    describe "prepare outgoing" do
        assert length(modified["object"]["tag"]) == 2
  
        assert is_nil(modified["object"]["emoji"])
-       assert is_nil(modified["object"]["likes"])
        assert is_nil(modified["object"]["like_count"])
        assert is_nil(modified["object"]["announcements"])
        assert is_nil(modified["object"]["announcement_count"])
        assert is_nil(modified["object"]["context_id"])
      end
+     test "it strips internal fields of article" do
+       activity = insert(:article_activity)
+       {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+       assert length(modified["object"]["tag"]) == 2
+       assert is_nil(modified["object"]["emoji"])
+       assert is_nil(modified["object"]["like_count"])
+       assert is_nil(modified["object"]["announcements"])
+       assert is_nil(modified["object"]["announcement_count"])
+       assert is_nil(modified["object"]["context_id"])
+     end
+     test "it adds like collection to object" do
+       activity = insert(:note_activity)
+       {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+       assert modified["object"]["likes"]["type"] == "OrderedCollection"
+       assert modified["object"]["likes"]["totalItems"] == 0
+     end
+     test "the directMessage flag is present" do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu :moominmamma:"})
+       {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+       assert modified["directMessage"] == false
+       {:ok, activity} =
+         CommonAPI.post(user, %{"status" => "@#{other_user.nickname} :moominmamma:"})
+       {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+       assert modified["directMessage"] == false
+       {:ok, activity} =
+         CommonAPI.post(user, %{
+           "status" => "@#{other_user.nickname} :moominmamma:",
+           "visibility" => "direct"
+         })
+       {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+       assert modified["directMessage"] == true
+     end
    end
  
    describe "user upgrade" do
        {:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"})
        assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
  
-       user = Repo.get(User, user.id)
+       user = User.get_by_id(user.id)
        assert user.info.note_count == 1
  
        {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
        assert user.info.note_count == 1
        assert user.follower_address == "https://niu.moe/users/rye/followers"
  
-       # Wait for the background task
-       :timer.sleep(1000)
-       user = Repo.get(User, user.id)
+       user = User.get_by_id(user.id)
        assert user.info.note_count == 1
  
-       activity = Repo.get(Activity, activity.id)
+       activity = Activity.get_by_id(activity.id)
        assert user.follower_address in activity.recipients
  
        assert %{
  
        refute "..." in activity.recipients
  
-       unrelated_activity = Repo.get(Activity, unrelated_activity.id)
+       unrelated_activity = Activity.get_by_id(unrelated_activity.id)
        refute user.follower_address in unrelated_activity.recipients
  
-       user_two = Repo.get(User, user_two.id)
+       user_two = User.get_by_id(user_two.id)
        assert user.follower_address in user_two.following
        refute "..." in user_two.following
      end
    end
  
    describe "actor origin containment" do
 -    test "it rejects objects with a bogus origin" do
 -      {:error, _} = ActivityPub.fetch_object_from_id("https://info.pleroma.site/activity.json")
 -    end
 -
      test "it rejects activities which reference objects with bogus origins" do
        data = %{
          "@context" => "https://www.w3.org/ns/activitystreams",
        :error = Transmogrifier.handle_incoming(data)
      end
  
 -    test "it rejects objects when attributedTo is wrong (variant 1)" do
 -      {:error, _} = ActivityPub.fetch_object_from_id("https://info.pleroma.site/activity2.json")
 -    end
 -
      test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
        data = %{
          "@context" => "https://www.w3.org/ns/activitystreams",
        :error = Transmogrifier.handle_incoming(data)
      end
  
 -    test "it rejects objects when attributedTo is wrong (variant 2)" do
 -      {:error, _} = ActivityPub.fetch_object_from_id("https://info.pleroma.site/activity3.json")
 -    end
 -
      test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
        data = %{
          "@context" => "https://www.w3.org/ns/activitystreams",
        :error = Transmogrifier.handle_incoming(data)
      end
    end
+   describe "general origin containment" do
+     test "contain_origin_from_id() catches obvious spoofing attempts" do
+       data = %{
+         "id" => "http://example.com/~alyssa/activities/1234.json"
+       }
+       :error =
+         Transmogrifier.contain_origin_from_id(
+           "http://example.org/~alyssa/activities/1234.json",
+           data
+         )
+     end
+     test "contain_origin_from_id() allows alternate IDs within the same origin domain" do
+       data = %{
+         "id" => "http://example.com/~alyssa/activities/1234.json"
+       }
+       :ok =
+         Transmogrifier.contain_origin_from_id(
+           "http://example.com/~alyssa/activities/1234",
+           data
+         )
+     end
+     test "contain_origin_from_id() allows matching IDs" do
+       data = %{
+         "id" => "http://example.com/~alyssa/activities/1234.json"
+       }
+       :ok =
+         Transmogrifier.contain_origin_from_id(
+           "http://example.com/~alyssa/activities/1234.json",
+           data
+         )
+     end
+     test "users cannot be collided through fake direction spoofing attempts" do
+       insert(:user, %{
+         nickname: "rye@niu.moe",
+         local: false,
+         ap_id: "https://niu.moe/users/rye",
+         follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
+       })
+       {:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
+     end
+     test "all objects with fake directions are rejected by the object fetcher" do
+       {:error, _} =
+         ActivityPub.fetch_and_contain_remote_object_from_id(
+           "https://info.pleroma.site/activity4.json"
+         )
+     end
+   end
+   describe "reserialization" do
+     test "successfully reserializes a message with inReplyTo == nil" do
+       user = insert(:user)
+       message = %{
+         "@context" => "https://www.w3.org/ns/activitystreams",
+         "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+         "cc" => [],
+         "type" => "Create",
+         "object" => %{
+           "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+           "cc" => [],
+           "type" => "Note",
+           "content" => "Hi",
+           "inReplyTo" => nil,
+           "attributedTo" => user.ap_id
+         },
+         "actor" => user.ap_id
+       }
+       {:ok, activity} = Transmogrifier.handle_incoming(message)
+       {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
+     end
+     test "successfully reserializes a message with AS2 objects in IR" do
+       user = insert(:user)
+       message = %{
+         "@context" => "https://www.w3.org/ns/activitystreams",
+         "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+         "cc" => [],
+         "type" => "Create",
+         "object" => %{
+           "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+           "cc" => [],
+           "type" => "Note",
+           "content" => "Hi",
+           "inReplyTo" => nil,
+           "attributedTo" => user.ap_id,
+           "tag" => [
+             %{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"},
+             %{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"}
+           ]
+         },
+         "actor" => user.ap_id
+       }
+       {:ok, activity} = Transmogrifier.handle_incoming(message)
+       {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
+     end
+   end
  end
index 3dc5f6f8441b1407a18b17decc850ed82ac9589d,34aa5bf1884f949c9a8642ffba4a5dc2c07cb6f6..b9ed088e41c6600f736ed5fa865e2d264df2f356
@@@ -1,19 -1,47 +1,50 @@@
- defmodule Pleroma.Web.CommonAPI.Test do
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
+ defmodule Pleroma.Web.CommonAPITest do
    use Pleroma.DataCase
+   alias Pleroma.Activity
+   alias Pleroma.User
++  alias Pleroma.Object
    alias Pleroma.Web.CommonAPI
-   alias Pleroma.{User, Object}
  
    import Pleroma.Factory
  
+   test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do
+     har = insert(:user)
+     jafnhar = insert(:user)
+     tridi = insert(:user)
+     option = Pleroma.Config.get([:instance, :safe_dm_mentions])
+     Pleroma.Config.put([:instance, :safe_dm_mentions], true)
+     {:ok, activity} =
+       CommonAPI.post(har, %{
+         "status" => "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again",
+         "visibility" => "direct"
+       })
+     refute tridi.ap_id in activity.recipients
+     assert jafnhar.ap_id in activity.recipients
+     Pleroma.Config.put([:instance, :safe_dm_mentions], option)
+   end
    test "it de-duplicates tags" do
      user = insert(:user)
      {:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu #2HU"})
  
 -    assert activity.data["object"]["tag"] == ["2hu"]
 +    object = Object.normalize(activity.data["object"])
 +
 +    assert object.data["tag"] == ["2hu"]
    end
  
+   test "it adds emoji in the object" do
+     user = insert(:user)
+     {:ok, activity} = CommonAPI.post(user, %{"status" => ":moominmamma:"})
+     assert activity.data["object"]["emoji"]["moominmamma"]
+   end
    test "it adds emoji when updating profiles" do
      user = insert(:user, %{name: ":karjalanpiirakka:"})
  
@@@ -36,9 -64,8 +67,9 @@@
            "content_type" => "text/html"
          })
  
 -      content = activity.data["object"]["content"]
 -      assert content == "<p><b>2hu</b></p>alert('xss')"
 +      object = Object.normalize(activity.data["object"])
 +
 +      assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')"
      end
  
      test "it filters out obviously bad tags when accepting a post as Markdown" do
            "content_type" => "text/markdown"
          })
  
 -      content = activity.data["object"]["content"]
 -      assert content == "<p><b>2hu</b></p>alert('xss')"
 +      object = Object.normalize(activity.data["object"])
 +
 +      assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')"
      end
    end
+   describe "reactions" do
+     test "repeating a status" do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
+       {:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user)
+     end
+     test "favoriting a status" do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
+       {:ok, %Activity{}, _} = CommonAPI.favorite(activity.id, user)
+     end
+     test "retweeting a status twice returns an error" do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
+       {:ok, %Activity{}, _object} = CommonAPI.repeat(activity.id, user)
+       {:error, _} = CommonAPI.repeat(activity.id, user)
+     end
+     test "favoriting a status twice returns an error" do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
+       {:ok, %Activity{}, _object} = CommonAPI.favorite(activity.id, user)
+       {:error, _} = CommonAPI.favorite(activity.id, user)
+     end
+   end
+   describe "pinned statuses" do
+     setup do
+       Pleroma.Config.put([:instance, :max_pinned_statuses], 1)
+       user = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
+       [user: user, activity: activity]
+     end
+     test "pin status", %{user: user, activity: activity} do
+       assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
+       id = activity.id
+       user = refresh_record(user)
+       assert %User{info: %{pinned_activities: [^id]}} = user
+     end
+     test "only self-authored can be pinned", %{activity: activity} do
+       user = insert(:user)
+       assert {:error, "Could not pin"} = CommonAPI.pin(activity.id, user)
+     end
+     test "max pinned statuses", %{user: user, activity: activity_one} do
+       {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
+       assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user)
+       user = refresh_record(user)
+       assert {:error, "You have already pinned the maximum number of statuses"} =
+                CommonAPI.pin(activity_two.id, user)
+     end
+     test "unpin status", %{user: user, activity: activity} do
+       {:ok, activity} = CommonAPI.pin(activity.id, user)
+       user = refresh_record(user)
+       assert {:ok, ^activity} = CommonAPI.unpin(activity.id, user)
+       user = refresh_record(user)
+       assert %User{info: %{pinned_activities: []}} = user
+     end
+     test "should unpin when deleting a status", %{user: user, activity: activity} do
+       {:ok, activity} = CommonAPI.pin(activity.id, user)
+       user = refresh_record(user)
+       assert {:ok, _} = CommonAPI.delete(activity.id, user)
+       user = refresh_record(user)
+       assert %User{info: %{pinned_activities: []}} = user
+     end
+   end
+   describe "mute tests" do
+     setup do
+       user = insert(:user)
+       activity = insert(:note_activity)
+       [user: user, activity: activity]
+     end
+     test "add mute", %{user: user, activity: activity} do
+       {:ok, _} = CommonAPI.add_mute(user, activity)
+       assert CommonAPI.thread_muted?(user, activity)
+     end
+     test "remove mute", %{user: user, activity: activity} do
+       CommonAPI.add_mute(user, activity)
+       {:ok, _} = CommonAPI.remove_mute(user, activity)
+       refute CommonAPI.thread_muted?(user, activity)
+     end
+     test "check that mutes can't be duplicate", %{user: user, activity: activity} do
+       CommonAPI.add_mute(user, activity)
+       {:error, _} = CommonAPI.add_mute(user, activity)
+     end
+   end
+   describe "reports" do
+     test "creates a report" do
+       reporter = insert(:user)
+       target_user = insert(:user)
+       {:ok, activity} = CommonAPI.post(target_user, %{"status" => "foobar"})
+       reporter_ap_id = reporter.ap_id
+       target_ap_id = target_user.ap_id
+       activity_ap_id = activity.data["id"]
+       comment = "foobar"
+       report_data = %{
+         "account_id" => target_user.id,
+         "comment" => comment,
+         "status_ids" => [activity.id]
+       }
+       assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
+       assert %Activity{
+                actor: ^reporter_ap_id,
+                data: %{
+                  "type" => "Flag",
+                  "content" => ^comment,
+                  "object" => [^target_ap_id, ^activity_ap_id]
+                }
+              } = flag_activity
+     end
+   end
+   describe "reblog muting" do
+     setup do
+       muter = insert(:user)
+       muted = insert(:user)
+       [muter: muter, muted: muted]
+     end
+     test "add a reblog mute", %{muter: muter, muted: muted} do
+       {:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
+       assert Pleroma.User.showing_reblogs?(muter, muted) == false
+     end
+     test "remove a reblog mute", %{muter: muter, muted: muted} do
+       {:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
+       {:ok, muter} = CommonAPI.show_reblogs(muter, muted)
+       assert Pleroma.User.showing_reblogs?(muter, muted) == true
+     end
+   end
  end
index 4f58ce8afb6e77168c388115a8e2d3e40d0383b9,db2fdc2f6dda92a15df7e0f3020ee301f5c387fe..4ea50c7c698dbeec43d33f7b9287b9e0ea3b703b
@@@ -1,24 -1,68 +1,70 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
    use Pleroma.DataCase
  
-   alias Pleroma.Web.MastodonAPI.{StatusView, AccountView}
-   alias Pleroma.{Repo, User, Object}
-   alias Pleroma.Web.OStatus
+   alias Pleroma.Activity
+   alias Pleroma.User
++  alias Pleroma.Repo
++  alias Pleroma.Object
+   alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.CommonAPI
+   alias Pleroma.Web.CommonAPI.Utils
+   alias Pleroma.Web.MastodonAPI.AccountView
+   alias Pleroma.Web.MastodonAPI.StatusView
+   alias Pleroma.Web.OStatus
    import Pleroma.Factory
+   import Tesla.Mock
+   setup do
+     mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+     :ok
+   end
+   test "returns a temporary ap_id based user for activities missing db users" do
+     user = insert(:user)
+     {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
+     Repo.delete(user)
+     Cachex.clear(:user_cache)
+     %{account: ms_user} = StatusView.render("status.json", activity: activity)
+     assert ms_user.acct == "erroruser@example.com"
+   end
+   test "tries to get a user by nickname if fetching by ap_id doesn't work" do
+     user = insert(:user)
+     {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
+     {:ok, user} =
+       user
+       |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"})
+       |> Repo.update()
+     Cachex.clear(:user_cache)
+     result = StatusView.render("status.json", activity: activity)
+     assert result[:account][:id] == to_string(user.id)
+   end
  
    test "a note with null content" do
      note = insert(:note_activity)
 +    note_object = Object.normalize(note.data["object"])
  
      data =
 -      note.data
 -      |> put_in(["object", "content"], nil)
 +      note_object.data
 +      |> Map.put("content", nil)
  
 -    note =
 -      note
 -      |> Map.put(:data, data)
 +    Object.change(note_object, %{data: data})
 +    |> Repo.update()
  
-     user = User.get_cached_by_ap_id(note.data["actor"])
+     User.get_cached_by_ap_id(note.data["actor"])
  
      status = StatusView.render("status.json", %{activity: note})
  
@@@ -29,6 -73,8 +75,8 @@@
      note = insert(:note_activity)
      user = User.get_cached_by_ap_id(note.data["actor"])
  
+     convo_id = Utils.context_to_conversation_id(note.data["object"]["context"])
      status = StatusView.render("status.json", %{activity: note})
  
      created_at =
      expected = %{
        id: to_string(note.id),
        uri: note.data["object"]["id"],
-       url: note.data["object"]["id"],
+       url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note),
        account: AccountView.render("account.json", %{user: user}),
        in_reply_to_id: nil,
        in_reply_to_account_id: nil,
+       card: nil,
        reblog: nil,
        content: HtmlSanitizeEx.basic_html(note.data["object"]["content"]),
        created_at: created_at,
        replies_count: 0,
        favourites_count: 0,
        reblogged: false,
+       bookmarked: false,
        favourited: false,
        muted: false,
+       pinned: false,
        sensitive: false,
-       spoiler_text: note.data["object"]["summary"],
+       spoiler_text: HtmlSanitizeEx.basic_html(note.data["object"]["summary"]),
        visibility: "public",
        media_attachments: [],
        mentions: [],
-       tags: [],
+       tags: [
+         %{
+           name: "#{note.data["object"]["tag"]}",
+           url: "/tag/#{note.data["object"]["tag"]}"
+         }
+       ],
        application: %{
          name: "Web",
          website: nil
            static_url: "corndog.png",
            visible_in_picker: false
          }
-       ]
+       ],
+       pleroma: %{
+         local: true,
+         conversation_id: convo_id,
+         content: %{"text/plain" => HtmlSanitizeEx.strip_tags(note.data["object"]["content"])},
+         spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(note.data["object"]["summary"])}
+       }
      }
  
      assert status == expected
    end
  
+   test "tells if the message is muted for some reason" do
+     user = insert(:user)
+     other_user = insert(:user)
+     {:ok, user} = User.mute(user, other_user)
+     {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
+     status = StatusView.render("status.json", %{activity: activity})
+     assert status.muted == false
+     status = StatusView.render("status.json", %{activity: activity, for: user})
+     assert status.muted == true
+   end
    test "a reply" do
      note = insert(:note_activity)
      user = insert(:user)
  
      status = StatusView.render("status.json", %{activity: activity})
  
-     assert status.mentions == [AccountView.render("mention.json", %{user: user})]
+     actor = User.get_by_ap_id(activity.actor)
+     assert status.mentions ==
+              Enum.map([user, actor], fn u -> AccountView.render("mention.json", %{user: u}) end)
    end
  
    test "attachments" do
        remote_url: "someurl",
        preview_url: "someurl",
        text_url: "someurl",
-       description: nil
+       description: nil,
+       pleroma: %{mime_type: "image/png"}
      }
  
      assert expected == StatusView.render("attachment.json", %{attachment: object})
      assert represented[:reblog][:id] == to_string(activity.id)
      assert represented[:emojis] == []
    end
+   test "a peertube video" do
+     user = insert(:user)
+     {:ok, object} =
+       ActivityPub.fetch_object_from_id(
+         "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
+       )
+     %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
+     represented = StatusView.render("status.json", %{for: user, activity: activity})
+     assert represented[:id] == to_string(activity.id)
+     assert length(represented[:media_attachments]) == 1
+   end
+   describe "build_tags/1" do
+     test "it returns a a dictionary tags" do
+       object_tags = [
+         "fediverse",
+         "mastodon",
+         "nextcloud",
+         %{
+           "href" => "https://kawen.space/users/lain",
+           "name" => "@lain@kawen.space",
+           "type" => "Mention"
+         }
+       ]
+       assert StatusView.build_tags(object_tags) == [
+                %{name: "fediverse", url: "/tag/fediverse"},
+                %{name: "mastodon", url: "/tag/mastodon"},
+                %{name: "nextcloud", url: "/tag/nextcloud"}
+              ]
+     end
+   end
+   describe "rich media cards" do
+     test "a rich media card without a site name renders correctly" do
+       page_url = "http://example.com"
+       card = %{
+         url: page_url,
+         image: page_url <> "/example.jpg",
+         title: "Example website"
+       }
+       %{provider_name: "example.com"} =
+         StatusView.render("card.json", %{page_url: page_url, rich_media: card})
+     end
+     test "a rich media card without a site name or image renders correctly" do
+       page_url = "http://example.com"
+       card = %{
+         url: page_url,
+         title: "Example website"
+       }
+       %{provider_name: "example.com"} =
+         StatusView.render("card.json", %{page_url: page_url, rich_media: card})
+     end
+     test "a rich media card without an image renders correctly" do
+       page_url = "http://example.com"
+       card = %{
+         url: page_url,
+         site_name: "Example site name",
+         title: "Example website"
+       }
+       %{provider_name: "Example site name"} =
+         StatusView.render("card.json", %{page_url: page_url, rich_media: card})
+     end
+     test "a rich media card with all relevant data renders correctly" do
+       page_url = "http://example.com"
+       card = %{
+         url: page_url,
+         site_name: "Example site name",
+         title: "Example website",
+         image: page_url <> "/example.jpg",
+         description: "Example description"
+       }
+       %{provider_name: "Example site name"} =
+         StatusView.render("card.json", %{page_url: page_url, rich_media: card})
+     end
+   end
  end
index b5805c66843ee73fdab7821faccfb0738d778c40,9fd100f63bc0131f992d8a5db64423d590cfcb7a..50467c71fbf1d8b49bd7210d0643ca0eae32332b
@@@ -1,11 -1,24 +1,24 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.OStatusTest do
    use Pleroma.DataCase
+   alias Pleroma.Activity
+   alias Pleroma.Instances
+   alias Pleroma.Object
+   alias Pleroma.Repo
+   alias Pleroma.User
    alias Pleroma.Web.OStatus
    alias Pleroma.Web.XML
-   alias Pleroma.{Object, Repo, User, Activity}
    import Pleroma.Factory
    import ExUnit.CaptureLog
  
+   setup_all do
+     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+     :ok
+   end
    test "don't insert create notes twice" do
      incoming = File.read!("test/fixtures/incoming_note_activity.xml")
      {:ok, [activity]} = OStatus.handle_incoming(incoming)
    test "handle incoming note - GS, Salmon" do
      incoming = File.read!("test/fixtures/incoming_note_activity.xml")
      {:ok, [activity]} = OStatus.handle_incoming(incoming)
 +    object = Object.normalize(activity.data["object"])
  
      user = User.get_by_ap_id(activity.data["actor"])
      assert user.info.note_count == 1
      assert activity.data["type"] == "Create"
 -    assert activity.data["object"]["type"] == "Note"
 +    assert object.data["type"] == "Note"
  
 -    assert activity.data["object"]["id"] ==
 -             "tag:gs.example.org:4040,2017-04-23:noticeId=29:objectType=note"
 +    assert object.data["id"] == "tag:gs.example.org:4040,2017-04-23:noticeId=29:objectType=note"
  
      assert activity.data["published"] == "2017-04-23T14:51:03+00:00"
 -    assert activity.data["object"]["published"] == "2017-04-23T14:51:03+00:00"
 +    assert object.data["published"] == "2017-04-23T14:51:03+00:00"
  
      assert activity.data["context"] ==
               "tag:gs.example.org:4040,2017-04-23:objectType=thread:nonce=f09e22f58abd5c7b"
  
      assert "http://pleroma.example.org:4000/users/lain3" in activity.data["to"]
 -    assert activity.data["object"]["emoji"] == %{"marko" => "marko.png", "reimu" => "reimu.png"}
 +    assert object.data["emoji"] == %{"marko" => "marko.png", "reimu" => "reimu.png"}
      assert activity.local == false
    end
  
    test "handle incoming notes - GS, subscription" do
      incoming = File.read!("test/fixtures/ostatus_incoming_post.xml")
      {:ok, [activity]} = OStatus.handle_incoming(incoming)
 +    object = Object.normalize(activity.data["object"])
  
      assert activity.data["type"] == "Create"
 -    assert activity.data["object"]["type"] == "Note"
 -    assert activity.data["object"]["actor"] == "https://social.heldscal.la/user/23211"
 -    assert activity.data["object"]["content"] == "Will it blend?"
 +    assert object.data["type"] == "Note"
 +    assert object.data["actor"] == "https://social.heldscal.la/user/23211"
 +    assert object.data["content"] == "Will it blend?"
      user = User.get_cached_by_ap_id(activity.data["actor"])
      assert User.ap_followers(user) in activity.data["to"]
      assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
    test "handle incoming notes with attachments - GS, subscription" do
      incoming = File.read!("test/fixtures/incoming_websub_gnusocial_attachments.xml")
      {:ok, [activity]} = OStatus.handle_incoming(incoming)
 +    object = Object.normalize(activity.data["object"])
  
      assert activity.data["type"] == "Create"
 -    assert activity.data["object"]["type"] == "Note"
 -    assert activity.data["object"]["actor"] == "https://social.heldscal.la/user/23211"
 -    assert activity.data["object"]["attachment"] |> length == 2
 -    assert activity.data["object"]["external_url"] == "https://social.heldscal.la/notice/2020923"
 +    assert object.data["type"] == "Note"
 +    assert object.data["actor"] == "https://social.heldscal.la/user/23211"
 +    assert object.data["attachment"] |> length == 2
 +    assert object.data["external_url"] == "https://social.heldscal.la/notice/2020923"
      assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
    end
  
    test "handle incoming notes with tags" do
      incoming = File.read!("test/fixtures/ostatus_incoming_post_tag.xml")
      {:ok, [activity]} = OStatus.handle_incoming(incoming)
 +    object = Object.normalize(activity.data["object"])
  
 -    assert activity.data["object"]["tag"] == ["nsfw"]
 +    assert object.data["tag"] == ["nsfw"]
      assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
    end
  
  
      incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
      {:ok, [activity]} = OStatus.handle_incoming(incoming)
 +    object = Object.normalize(activity.data["object"])
  
      assert activity.data["type"] == "Create"
 -    assert activity.data["object"]["type"] == "Note"
 -    assert activity.data["object"]["actor"] == "https://mastodon.social/users/lambadalambda"
 +    assert object.data["type"] == "Note"
 +    assert object.data["actor"] == "https://mastodon.social/users/lambadalambda"
      assert activity.data["context"] == "2hu"
      assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
    end
    test "handle incoming notes - Mastodon, with CW" do
      incoming = File.read!("test/fixtures/mastodon-note-cw.xml")
      {:ok, [activity]} = OStatus.handle_incoming(incoming)
 +    object = Object.normalize(activity.data["object"])
  
      assert activity.data["type"] == "Create"
 -    assert activity.data["object"]["type"] == "Note"
 -    assert activity.data["object"]["actor"] == "https://mastodon.social/users/lambadalambda"
 -    assert activity.data["object"]["summary"] == "technologic"
 +    assert object.data["type"] == "Note"
 +    assert object.data["actor"] == "https://mastodon.social/users/lambadalambda"
 +    assert object.data["summary"] == "technologic"
      assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
    end
  
    test "handle incoming unlisted messages, put public into cc" do
      incoming = File.read!("test/fixtures/mastodon-note-unlisted.xml")
      {:ok, [activity]} = OStatus.handle_incoming(incoming)
 +    object = Object.normalize(activity.data["object"])
 +
      refute "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
      assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["cc"]
 -    refute "https://www.w3.org/ns/activitystreams#Public" in activity.data["object"]["to"]
 -    assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["object"]["cc"]
 +    refute "https://www.w3.org/ns/activitystreams#Public" in object.data["to"]
 +    assert "https://www.w3.org/ns/activitystreams#Public" in object.data["cc"]
    end
  
    test "handle incoming retweets - Mastodon, with CW" do
      incoming = File.read!("test/fixtures/cw_retweet.xml")
      {:ok, [[_activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
 +    retweeted_object = Object.normalize(retweeted_activity.data["object"])
  
 -    assert retweeted_activity.data["object"]["summary"] == "Hey."
 +    assert retweeted_object.data["summary"] == "Hey."
    end
  
    test "handle incoming notes - GS, subscription, reply" do
      incoming = File.read!("test/fixtures/ostatus_incoming_reply.xml")
      {:ok, [activity]} = OStatus.handle_incoming(incoming)
 +    object = Object.normalize(activity.data["object"])
  
      assert activity.data["type"] == "Create"
 -    assert activity.data["object"]["type"] == "Note"
 -    assert activity.data["object"]["actor"] == "https://social.heldscal.la/user/23211"
 +    assert object.data["type"] == "Note"
 +    assert object.data["actor"] == "https://social.heldscal.la/user/23211"
  
 -    assert activity.data["object"]["content"] ==
 +    assert object.data["content"] ==
               "@<a href=\"https://gs.archae.me/user/4687\" class=\"h-card u-url p-nickname mention\" title=\"shpbot\">shpbot</a> why not indeed."
  
 -    assert activity.data["object"]["inReplyTo"] ==
 +    assert object.data["inReplyTo"] ==
               "tag:gs.archae.me,2017-04-30:noticeId=778260:objectType=note"
  
      assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
  
      assert activity.data["type"] == "Announce"
      assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
 -    assert activity.data["object"] == retweeted_activity.data["object"]["id"]
 +    assert activity.data["object"] == retweeted_activity.data["object"]
      assert "https://pleroma.soykaf.com/users/lain" in activity.data["to"]
      refute activity.local
  
-     retweeted_activity = Repo.get(Activity, retweeted_activity.id)
+     retweeted_activity = Activity.get_by_id(retweeted_activity.id)
 +    retweeted_object = Object.normalize(retweeted_activity.data["object"])
      assert retweeted_activity.data["type"] == "Create"
      assert retweeted_activity.data["actor"] == "https://pleroma.soykaf.com/users/lain"
      refute retweeted_activity.local
 -    assert retweeted_activity.data["object"]["announcement_count"] == 1
 -    assert String.contains?(retweeted_activity.data["object"]["content"], "mastodon")
 -    refute String.contains?(retweeted_activity.data["object"]["content"], "Test account")
 +    assert retweeted_object.data["announcement_count"] == 1
 +    assert String.contains?(retweeted_object.data["content"], "mastodon")
 +    refute String.contains?(retweeted_object.data["content"], "Test account")
    end
  
    test "handle incoming retweets - GS, subscription - local message" do
      assert user.ap_id in activity.data["to"]
      refute activity.local
  
-     retweeted_activity = Repo.get(Activity, retweeted_activity.id)
+     retweeted_activity = Activity.get_by_id(retweeted_activity.id)
      assert note_activity.id == retweeted_activity.id
      assert retweeted_activity.data["type"] == "Create"
      assert retweeted_activity.data["actor"] == user.ap_id
    test "handle incoming retweets - Mastodon, salmon" do
      incoming = File.read!("test/fixtures/share.xml")
      {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
 +    retweeted_object = Object.normalize(retweeted_activity.data["object"])
  
      assert activity.data["type"] == "Announce"
      assert activity.data["actor"] == "https://mastodon.social/users/lambadalambda"
 -    assert activity.data["object"] == retweeted_activity.data["object"]["id"]
 +    assert activity.data["object"] == retweeted_activity.data["object"]
  
      assert activity.data["id"] ==
               "tag:mastodon.social,2017-05-03:objectId=4934452:objectType=Status"
      assert retweeted_activity.data["type"] == "Create"
      assert retweeted_activity.data["actor"] == "https://pleroma.soykaf.com/users/lain"
      refute retweeted_activity.local
 -    refute String.contains?(retweeted_activity.data["object"]["content"], "Test account")
 +    refute String.contains?(retweeted_object.data["content"], "Test account")
    end
  
    test "handle incoming favorites - GS, websub" do
  
        assert activity.data["type"] == "Like"
        assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
 -      assert activity.data["object"] == favorited_activity.data["object"]["id"]
 +      assert activity.data["object"] == favorited_activity.data["object"]
  
        assert activity.data["id"] ==
                 "tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061643:2017-05-05T09:12:50+00:00"
        assert favorited_activity.data["type"] == "Create"
        assert favorited_activity.data["actor"] == "https://shitposter.club/user/1"
  
 -      assert favorited_activity.data["object"]["id"] ==
 +      assert favorited_activity.data["object"] ==
                 "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
  
        refute favorited_activity.local
    test "handle incoming replies" do
      incoming = File.read!("test/fixtures/incoming_note_activity_answer.xml")
      {:ok, [activity]} = OStatus.handle_incoming(incoming)
 +    object = Object.normalize(activity.data["object"])
  
      assert activity.data["type"] == "Create"
 -    assert activity.data["object"]["type"] == "Note"
 +    assert object.data["type"] == "Note"
  
 -    assert activity.data["object"]["inReplyTo"] ==
 +    assert object.data["inReplyTo"] ==
               "http://pleroma.example.org:4000/objects/55bce8fc-b423-46b1-af71-3759ab4670bc"
  
      assert "http://pleroma.example.org:4000/users/lain5" in activity.data["to"]
  
 -    assert activity.data["object"]["id"] ==
 -             "tag:gs.example.org:4040,2017-04-25:noticeId=55:objectType=note"
 +    assert object.data["id"] == "tag:gs.example.org:4040,2017-04-25:noticeId=55:objectType=note"
  
      assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
    end
      refute User.following?(follower, followed)
    end
  
+   test "it clears `unreachable` federation status of the sender" do
+     incoming_reaction_xml = File.read!("test/fixtures/share-gs.xml")
+     doc = XML.parse_document(incoming_reaction_xml)
+     actor_uri = XML.string_from_xpath("//author/uri[1]", doc)
+     reacted_to_author_uri = XML.string_from_xpath("//author/uri[2]", doc)
+     Instances.set_consistently_unreachable(actor_uri)
+     Instances.set_consistently_unreachable(reacted_to_author_uri)
+     refute Instances.reachable?(actor_uri)
+     refute Instances.reachable?(reacted_to_author_uri)
+     {:ok, _} = OStatus.handle_incoming(incoming_reaction_xml)
+     assert Instances.reachable?(actor_uri)
+     refute Instances.reachable?(reacted_to_author_uri)
+   end
    describe "new remote user creation" do
      test "returns local users" do
        local_user = insert(:user)
  
        {:ok, user} = OStatus.find_or_make_user(uri)
  
-       user = Repo.get(Pleroma.User, user.id)
+       user = Pleroma.User.get_by_id(user.id)
        assert user.name == "Constance Variable"
        assert user.nickname == "lambadalambda@social.heldscal.la"
        assert user.local == false
  
          assert activity.data["actor"] == "https://shitposter.club/user/1"
  
 -        assert activity.data["object"]["id"] ==
 +        assert activity.data["object"] ==
                   "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
        end)
      end
        url = "https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056"
        {:ok, [activity]} = OStatus.fetch_activity_from_url(url)
        assert activity.data["actor"] == "https://social.sakamoto.gq/users/eal"
 -      assert activity.data["object"]["id"] == url
 +      assert activity.data["object"] == url
      end
    end
  
          note_object.data
          |> Map.put("type", "Article")
  
+       Cachex.clear(:object_cache)
        cs = Object.change(note_object, %{data: note_data})
-       {:ok, article_object} = Repo.update(cs)
+       {:ok, _article_object} = Repo.update(cs)
  
        # the underlying object is now an Article instead of a note, so this should fail
        refute OStatus.is_representable?(note_activity)
index bc53fe68a1d4b53f3350e0626b078e8bef1e35fc,4c9ae2da8033e24c502537ad38ecbab652fc62af..5bea1037ad10643e6df28a40a74034efac8d6d71
@@@ -1,16 -1,29 +1,29 @@@
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
    use Pleroma.DataCase
-   alias Pleroma.Builders.UserBuilder
-   alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView}
-   alias Pleroma.{Activity, User, Object, Repo, UserInviteToken}
+   alias Pleroma.Activity
+   alias Pleroma.Object
+   alias Pleroma.Repo
+   alias Pleroma.User
+   alias Pleroma.UserInviteToken
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.TwitterAPI.ActivityView
+   alias Pleroma.Web.TwitterAPI.TwitterAPI
+   alias Pleroma.Web.TwitterAPI.UserView
  
    import Pleroma.Factory
  
+   setup_all do
+     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+     :ok
+   end
    test "create a status" do
      user = insert(:user)
-     _mentioned_user = insert(:user, %{nickname: "shp", ap_id: "shp"})
+     mentioned_user = insert(:user, %{nickname: "shp", ap_id: "shp"})
  
      object_data = %{
        "type" => "Image",
      }
  
      {:ok, activity = %Activity{}} = TwitterAPI.create_status(user, input)
 +    object = Object.normalize(activity.data["object"])
  
      expected_text =
-       "Hello again, <span><a class='mention' href='shp'>@<span>shp</span></a></span>.&lt;script&gt;&lt;/script&gt;<br>This is on another :moominmamma: line. <a href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a> <a href='http://localhost:4001/tag/epic' rel='tag'>#epic</a> <a href='http://localhost:4001/tag/phantasmagoric' rel='tag'>#phantasmagoric</a><br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>"
+       "Hello again, <span class='h-card'><a data-user='#{mentioned_user.id}' class='u-url mention' href='shp'>@<span>shp</span></a></span>.&lt;script&gt;&lt;/script&gt;<br>This is on another :moominmamma: line. <a class='hashtag' data-tag='2hu' href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a> <a class='hashtag' data-tag='epic' href='http://localhost:4001/tag/epic' rel='tag'>#epic</a> <a class='hashtag' data-tag='phantasmagoric' href='http://localhost:4001/tag/phantasmagoric' rel='tag'>#phantasmagoric</a><br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>"
  
 -    assert get_in(activity.data, ["object", "content"]) == expected_text
 -    assert get_in(activity.data, ["object", "type"]) == "Note"
 -    assert get_in(activity.data, ["object", "actor"]) == user.ap_id
 +    assert get_in(object.data, ["content"]) == expected_text
 +    assert get_in(object.data, ["type"]) == "Note"
 +    assert get_in(object.data, ["actor"]) == user.ap_id
      assert get_in(activity.data, ["actor"]) == user.ap_id
      assert Enum.member?(get_in(activity.data, ["cc"]), User.ap_followers(user))
  
      assert activity.local == true
  
      assert %{"moominmamma" => "http://localhost:4001/finmoji/128px/moominmamma-128.png"} =
 -             activity.data["object"]["emoji"]
 +             object.data["emoji"]
  
      # hashtags
 -    assert activity.data["object"]["tag"] == ["2hu", "epic", "phantasmagoric"]
 +    assert object.data["tag"] == ["2hu", "epic", "phantasmagoric"]
  
      # Add a context
      assert is_binary(get_in(activity.data, ["context"]))
 -    assert is_binary(get_in(activity.data, ["object", "context"]))
 +    assert is_binary(get_in(object.data, ["context"]))
  
 -    assert is_list(activity.data["object"]["attachment"])
 +    assert is_list(object.data["attachment"])
  
 -    assert activity.data["object"] == Object.get_by_ap_id(activity.data["object"]["id"]).data
 +    assert activity.data["object"] == object.data["id"]
  
      user = User.get_by_ap_id(user.ap_id)
  
@@@ -79,7 -91,6 +92,7 @@@
      }
  
      {:ok, activity = %Activity{}} = TwitterAPI.create_status(user, input)
 +    object = Object.normalize(activity.data["object"])
  
      input = %{
        "status" => "Here's your (you).",
      }
  
      {:ok, reply = %Activity{}} = TwitterAPI.create_status(user, input)
 +    reply_object = Object.normalize(reply.data["object"])
  
      assert get_in(reply.data, ["context"]) == get_in(activity.data, ["context"])
  
 -    assert get_in(reply.data, ["object", "context"]) ==
 -             get_in(activity.data, ["object", "context"])
 +    assert get_in(reply_object.data, ["context"]) == get_in(object.data, ["context"])
  
 -    assert get_in(reply.data, ["object", "inReplyTo"]) == get_in(activity.data, ["object", "id"])
 +    assert get_in(reply_object.data, ["inReplyTo"]) == get_in(activity.data, ["object"])
-     assert get_in(reply_object.data, ["inReplyToStatusId"]) == activity.id
+     assert Activity.get_in_reply_to_activity(reply).id == activity.id
    end
  
    test "Follow another user using user_id" do
    end
  
    test "upload a file" do
+     user = insert(:user)
      file = %Plug.Upload{
        content_type: "image/jpg",
        path: Path.absname("test/fixtures/image.jpg"),
        filename: "an_image.jpg"
      }
  
-     response = TwitterAPI.upload(file)
+     response = TwitterAPI.upload(file, user)
  
      assert is_binary(response)
    end
  
    test "it favorites a status, returns the updated activity" do
      user = insert(:user)
+     other_user = insert(:user)
      note_activity = insert(:note_activity)
  
      {:ok, status} = TwitterAPI.fav(user, note_activity.id)
      updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
+     assert ActivityView.render("activity.json", %{activity: updated_activity})["fave_num"] == 1
+     object = Object.normalize(note_activity.data["object"])
+     assert object.data["like_count"] == 1
  
      assert status == updated_activity
+     {:ok, _status} = TwitterAPI.fav(other_user, note_activity.id)
+     object = Object.normalize(note_activity.data["object"])
+     assert object.data["like_count"] == 2
+     updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
+     assert ActivityView.render("activity.json", %{activity: updated_activity})["fave_num"] == 2
    end
  
    test "it unfavorites a status, returns the updated activity" do
        "nickname" => "lain",
        "email" => "lain@wired.jp",
        "fullname" => "lain iwakura",
-       "bio" => "close the world.",
        "password" => "bear",
        "confirm" => "bear"
      }
  
      {:ok, user} = TwitterAPI.register_user(data)
  
-     fetched_user = Repo.get_by(User, nickname: "lain")
+     fetched_user = User.get_by_nickname("lain")
  
      assert UserView.render("show.json", %{user: user}) ==
               UserView.render("show.json", %{user: fetched_user})
    end
  
+   test "it registers a new user with empty string in bio and returns the user." do
+     data = %{
+       "nickname" => "lain",
+       "email" => "lain@wired.jp",
+       "fullname" => "lain iwakura",
+       "bio" => "",
+       "password" => "bear",
+       "confirm" => "bear"
+     }
+     {:ok, user} = TwitterAPI.register_user(data)
+     fetched_user = User.get_by_nickname("lain")
+     assert UserView.render("show.json", %{user: user}) ==
+              UserView.render("show.json", %{user: fetched_user})
+   end
+   test "it sends confirmation email if :account_activation_required is specified in instance config" do
+     setting = Pleroma.Config.get([:instance, :account_activation_required])
+     unless setting do
+       Pleroma.Config.put([:instance, :account_activation_required], true)
+       on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end)
+     end
+     data = %{
+       "nickname" => "lain",
+       "email" => "lain@wired.jp",
+       "fullname" => "lain iwakura",
+       "bio" => "",
+       "password" => "bear",
+       "confirm" => "bear"
+     }
+     {:ok, user} = TwitterAPI.register_user(data)
+     assert user.info.confirmation_pending
+     email = Pleroma.Emails.UserEmail.account_confirmation_email(user)
+     notify_email = Pleroma.Config.get([:instance, :notify_email])
+     instance_name = Pleroma.Config.get([:instance, :name])
+     Swoosh.TestAssertions.assert_email_sent(
+       from: {instance_name, notify_email},
+       to: {user.name, user.email},
+       html_body: email.html_body
+     )
+   end
    test "it registers a new user and parses mentions in the bio" do
      data1 = %{
        "nickname" => "john",
      {:ok, user2} = TwitterAPI.register_user(data2)
  
      expected_text =
-       "<span><a class='mention' href='#{user1.ap_id}'>@<span>john</span></a></span> test"
+       "<span class='h-card'><a data-user='#{user1.id}' class='u-url mention' href='#{user1.ap_id}'>@<span>john</span></a></span> test"
  
      assert user2.bio == expected_text
    end
  
-   @moduletag skip: "needs 'registrations_open: false' in config"
-   test "it registers a new user via invite token and returns the user." do
-     {:ok, token} = UserInviteToken.create_token()
+   describe "register with one time token" do
+     setup do
+       setting = Pleroma.Config.get([:instance, :registrations_open])
  
-     data = %{
-       "nickname" => "vinny",
-       "email" => "pasta@pizza.vs",
-       "fullname" => "Vinny Vinesauce",
-       "bio" => "streamer",
-       "password" => "hiptofbees",
-       "confirm" => "hiptofbees",
-       "token" => token.token
-     }
+       if setting do
+         Pleroma.Config.put([:instance, :registrations_open], false)
+         on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end)
+       end
  
-     {:ok, user} = TwitterAPI.register_user(data)
+       :ok
+     end
  
-     fetched_user = Repo.get_by(User, nickname: "vinny")
-     token = Repo.get_by(UserInviteToken, token: token.token)
+     test "returns user on success" do
+       {:ok, invite} = UserInviteToken.create_invite()
  
-     assert token.used == true
+       data = %{
+         "nickname" => "vinny",
+         "email" => "pasta@pizza.vs",
+         "fullname" => "Vinny Vinesauce",
+         "bio" => "streamer",
+         "password" => "hiptofbees",
+         "confirm" => "hiptofbees",
+         "token" => invite.token
+       }
  
-     assert UserView.render("show.json", %{user: user}) ==
-              UserView.render("show.json", %{user: fetched_user})
+       {:ok, user} = TwitterAPI.register_user(data)
+       fetched_user = User.get_by_nickname("vinny")
+       invite = Repo.get_by(UserInviteToken, token: invite.token)
+       assert invite.used == true
+       assert UserView.render("show.json", %{user: user}) ==
+                UserView.render("show.json", %{user: fetched_user})
+     end
+     test "returns error on invalid token" do
+       data = %{
+         "nickname" => "GrimReaper",
+         "email" => "death@reapers.afterlife",
+         "fullname" => "Reaper Grim",
+         "bio" => "Your time has come",
+         "password" => "scythe",
+         "confirm" => "scythe",
+         "token" => "DudeLetMeInImAFairy"
+       }
+       {:error, msg} = TwitterAPI.register_user(data)
+       assert msg == "Invalid token"
+       refute User.get_by_nickname("GrimReaper")
+     end
+     test "returns error on expired token" do
+       {:ok, invite} = UserInviteToken.create_invite()
+       UserInviteToken.update_invite!(invite, used: true)
+       data = %{
+         "nickname" => "GrimReaper",
+         "email" => "death@reapers.afterlife",
+         "fullname" => "Reaper Grim",
+         "bio" => "Your time has come",
+         "password" => "scythe",
+         "confirm" => "scythe",
+         "token" => invite.token
+       }
+       {:error, msg} = TwitterAPI.register_user(data)
+       assert msg == "Expired token"
+       refute User.get_by_nickname("GrimReaper")
+     end
    end
  
-   @moduletag skip: "needs 'registrations_open: false' in config"
-   test "it returns an error if invalid token submitted" do
-     data = %{
-       "nickname" => "GrimReaper",
-       "email" => "death@reapers.afterlife",
-       "fullname" => "Reaper Grim",
-       "bio" => "Your time has come",
-       "password" => "scythe",
-       "confirm" => "scythe",
-       "token" => "DudeLetMeInImAFairy"
-     }
+   describe "registers with date limited token" do
+     setup do
+       setting = Pleroma.Config.get([:instance, :registrations_open])
+       if setting do
+         Pleroma.Config.put([:instance, :registrations_open], false)
+         on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end)
+       end
+       data = %{
+         "nickname" => "vinny",
+         "email" => "pasta@pizza.vs",
+         "fullname" => "Vinny Vinesauce",
+         "bio" => "streamer",
+         "password" => "hiptofbees",
+         "confirm" => "hiptofbees"
+       }
+       check_fn = fn invite ->
+         data = Map.put(data, "token", invite.token)
+         {:ok, user} = TwitterAPI.register_user(data)
+         fetched_user = User.get_by_nickname("vinny")
+         assert UserView.render("show.json", %{user: user}) ==
+                  UserView.render("show.json", %{user: fetched_user})
+       end
+       {:ok, data: data, check_fn: check_fn}
+     end
+     test "returns user on success", %{check_fn: check_fn} do
+       {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today()})
+       check_fn.(invite)
+       invite = Repo.get_by(UserInviteToken, token: invite.token)
+       refute invite.used
+     end
+     test "returns user on token which expired tomorrow", %{check_fn: check_fn} do
+       {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), 1)})
+       check_fn.(invite)
+       invite = Repo.get_by(UserInviteToken, token: invite.token)
+       refute invite.used
+     end
+     test "returns an error on overdue date", %{data: data} do
+       {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1)})
  
-     {:error, msg} = TwitterAPI.register_user(data)
+       data = Map.put(data, "token", invite.token)
  
-     assert msg == "Invalid token"
-     refute Repo.get_by(User, nickname: "GrimReaper")
+       {:error, msg} = TwitterAPI.register_user(data)
+       assert msg == "Expired token"
+       refute User.get_by_nickname("vinny")
+       invite = Repo.get_by(UserInviteToken, token: invite.token)
+       refute invite.used
+     end
    end
  
-   @moduletag skip: "needs 'registrations_open: false' in config"
-   test "it returns an error if expired token submitted" do
-     {:ok, token} = UserInviteToken.create_token()
-     UserInviteToken.mark_as_used(token.token)
+   describe "registers with reusable token" do
+     setup do
+       setting = Pleroma.Config.get([:instance, :registrations_open])
  
-     data = %{
-       "nickname" => "GrimReaper",
-       "email" => "death@reapers.afterlife",
-       "fullname" => "Reaper Grim",
-       "bio" => "Your time has come",
-       "password" => "scythe",
-       "confirm" => "scythe",
-       "token" => token.token
-     }
+       if setting do
+         Pleroma.Config.put([:instance, :registrations_open], false)
+         on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end)
+       end
+       :ok
+     end
+     test "returns user on success, after him registration fails" do
+       {:ok, invite} = UserInviteToken.create_invite(%{max_use: 100})
+       UserInviteToken.update_invite!(invite, uses: 99)
+       data = %{
+         "nickname" => "vinny",
+         "email" => "pasta@pizza.vs",
+         "fullname" => "Vinny Vinesauce",
+         "bio" => "streamer",
+         "password" => "hiptofbees",
+         "confirm" => "hiptofbees",
+         "token" => invite.token
+       }
+       {:ok, user} = TwitterAPI.register_user(data)
+       fetched_user = User.get_by_nickname("vinny")
+       invite = Repo.get_by(UserInviteToken, token: invite.token)
+       assert invite.used == true
+       assert UserView.render("show.json", %{user: user}) ==
+                UserView.render("show.json", %{user: fetched_user})
+       data = %{
+         "nickname" => "GrimReaper",
+         "email" => "death@reapers.afterlife",
+         "fullname" => "Reaper Grim",
+         "bio" => "Your time has come",
+         "password" => "scythe",
+         "confirm" => "scythe",
+         "token" => invite.token
+       }
+       {:error, msg} = TwitterAPI.register_user(data)
  
-     {:error, msg} = TwitterAPI.register_user(data)
+       assert msg == "Expired token"
+       refute User.get_by_nickname("GrimReaper")
+     end
+   end
+   describe "registers with reusable date limited token" do
+     setup do
+       setting = Pleroma.Config.get([:instance, :registrations_open])
+       if setting do
+         Pleroma.Config.put([:instance, :registrations_open], false)
+         on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end)
+       end
+       :ok
+     end
+     test "returns user on success" do
+       {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100})
+       data = %{
+         "nickname" => "vinny",
+         "email" => "pasta@pizza.vs",
+         "fullname" => "Vinny Vinesauce",
+         "bio" => "streamer",
+         "password" => "hiptofbees",
+         "confirm" => "hiptofbees",
+         "token" => invite.token
+       }
+       {:ok, user} = TwitterAPI.register_user(data)
+       fetched_user = User.get_by_nickname("vinny")
+       invite = Repo.get_by(UserInviteToken, token: invite.token)
+       refute invite.used
+       assert UserView.render("show.json", %{user: user}) ==
+                UserView.render("show.json", %{user: fetched_user})
+     end
+     test "error after max uses" do
+       {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100})
+       UserInviteToken.update_invite!(invite, uses: 99)
+       data = %{
+         "nickname" => "vinny",
+         "email" => "pasta@pizza.vs",
+         "fullname" => "Vinny Vinesauce",
+         "bio" => "streamer",
+         "password" => "hiptofbees",
+         "confirm" => "hiptofbees",
+         "token" => invite.token
+       }
+       {:ok, user} = TwitterAPI.register_user(data)
+       fetched_user = User.get_by_nickname("vinny")
+       invite = Repo.get_by(UserInviteToken, token: invite.token)
+       assert invite.used == true
+       assert UserView.render("show.json", %{user: user}) ==
+                UserView.render("show.json", %{user: fetched_user})
+       data = %{
+         "nickname" => "GrimReaper",
+         "email" => "death@reapers.afterlife",
+         "fullname" => "Reaper Grim",
+         "bio" => "Your time has come",
+         "password" => "scythe",
+         "confirm" => "scythe",
+         "token" => invite.token
+       }
+       {:error, msg} = TwitterAPI.register_user(data)
+       assert msg == "Expired token"
+       refute User.get_by_nickname("GrimReaper")
+     end
+     test "returns error on overdue date" do
+       {:ok, invite} =
+         UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100})
+       data = %{
+         "nickname" => "GrimReaper",
+         "email" => "death@reapers.afterlife",
+         "fullname" => "Reaper Grim",
+         "bio" => "Your time has come",
+         "password" => "scythe",
+         "confirm" => "scythe",
+         "token" => invite.token
+       }
  
-     assert msg == "Expired token"
-     refute Repo.get_by(User, nickname: "GrimReaper")
+       {:error, msg} = TwitterAPI.register_user(data)
+       assert msg == "Expired token"
+       refute User.get_by_nickname("GrimReaper")
+     end
+     test "returns error on with overdue date and after max" do
+       {:ok, invite} =
+         UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100})
+       UserInviteToken.update_invite!(invite, uses: 100)
+       data = %{
+         "nickname" => "GrimReaper",
+         "email" => "death@reapers.afterlife",
+         "fullname" => "Reaper Grim",
+         "bio" => "Your time has come",
+         "password" => "scythe",
+         "confirm" => "scythe",
+         "token" => invite.token
+       }
+       {:error, msg} = TwitterAPI.register_user(data)
+       assert msg == "Expired token"
+       refute User.get_by_nickname("GrimReaper")
+     end
    end
  
    test "it returns the error on registration problems" do
      {:error, error_object} = TwitterAPI.register_user(data)
  
      assert is_binary(error_object[:error])
-     refute Repo.get_by(User, nickname: "lain")
+     refute User.get_by_nickname("lain")
    end
  
    test "it assigns an integer conversation_id" do
      :ok
    end
  
-   describe "context_to_conversation_id" do
-     test "creates a mapping object" do
-       conversation_id = TwitterAPI.context_to_conversation_id("random context")
-       object = Object.get_by_ap_id("random context")
-       assert conversation_id == object.id
-     end
-     test "returns an existing mapping for an existing object" do
-       {:ok, object} = Object.context_mapping("random context") |> Repo.insert()
-       conversation_id = TwitterAPI.context_to_conversation_id("random context")
-       assert conversation_id == object.id
-     end
-   end
    describe "fetching a user by uri" do
      test "fetches a user by uri" do
        id = "https://mastodon.social/users/lambadalambda"
        assert represented["id"] == UserView.render("show.json", %{user: remote, for: user})["id"]
  
        # Also fetches the feed.
-       # assert Activity.get_create_activity_by_object_ap_id("tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status")
+       # assert Activity.get_create_by_object_ap_id("tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status")
+       # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
      end
    end
  end
index f4741cf243e00508db1ca1b7dced704a17ba6731,ee9a0c834ed6eef0f5105d488a289b1203edd3cb..7ef0270cc01583124057b4d94dffdc9927d7f528
+ # Pleroma: A lightweight social networking server
+ # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+ # SPDX-License-Identifier: AGPL-3.0-only
  defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do
    use Pleroma.DataCase
  
+   alias Pleroma.Activity
++  alias Pleroma.Object
+   alias Pleroma.Repo
+   alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.CommonAPI
    alias Pleroma.Web.CommonAPI.Utils
    alias Pleroma.Web.TwitterAPI.ActivityView
    alias Pleroma.Web.TwitterAPI.UserView
-   alias Pleroma.Web.TwitterAPI.TwitterAPI
-   alias Pleroma.Repo
-   alias Pleroma.{Activity, Object}
-   alias Pleroma.User
-   alias Pleroma.Web.ActivityPub.ActivityPub
  
    import Pleroma.Factory
+   import Tesla.Mock
+   setup do
+     mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+     :ok
+   end
    import Mock
  
+   test "returns a temporary ap_id based user for activities missing db users" do
+     user = insert(:user)
+     {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
+     Repo.delete(user)
+     Cachex.clear(:user_cache)
+     %{"user" => tw_user} = ActivityView.render("activity.json", activity: activity)
+     assert tw_user["screen_name"] == "erroruser@example.com"
+     assert tw_user["name"] == user.ap_id
+     assert tw_user["statusnet_profile_url"] == user.ap_id
+   end
+   test "tries to get a user by nickname if fetching by ap_id doesn't work" do
+     user = insert(:user)
+     {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
+     {:ok, user} =
+       user
+       |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"})
+       |> Repo.update()
+     Cachex.clear(:user_cache)
+     result = ActivityView.render("activity.json", activity: activity)
+     assert result["user"]["id"] == user.id
+   end
+   test "tells if the message is muted for some reason" do
+     user = insert(:user)
+     other_user = insert(:user)
+     {:ok, user} = User.mute(user, other_user)
+     {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
+     status = ActivityView.render("activity.json", %{activity: activity})
+     assert status["muted"] == false
+     status = ActivityView.render("activity.json", %{activity: activity, for: user})
+     assert status["muted"] == true
+   end
+   test "a create activity with a html status" do
+     text = """
+     #Bike log - Commute Tuesday\nhttps://pla.bike/posts/20181211/\n#cycling #CHScycling #commute\nMVIMG_20181211_054020.jpg
+     """
+     {:ok, activity} = CommonAPI.post(insert(:user), %{"status" => text})
+     result = ActivityView.render("activity.json", activity: activity)
+     assert result["statusnet_html"] ==
+              "<a class=\"hashtag\" data-tag=\"bike\" href=\"http://localhost:4001/tag/bike\" rel=\"tag\">#Bike</a> log - Commute Tuesday<br /><a href=\"https://pla.bike/posts/20181211/\">https://pla.bike/posts/20181211/</a><br /><a class=\"hashtag\" data-tag=\"cycling\" href=\"http://localhost:4001/tag/cycling\" rel=\"tag\">#cycling</a> <a class=\"hashtag\" data-tag=\"chscycling\" href=\"http://localhost:4001/tag/chscycling\" rel=\"tag\">#CHScycling</a> <a class=\"hashtag\" data-tag=\"commute\" href=\"http://localhost:4001/tag/commute\" rel=\"tag\">#commute</a><br />MVIMG_20181211_054020.jpg"
+     assert result["text"] ==
+              "#Bike log - Commute Tuesday\nhttps://pla.bike/posts/20181211/\n#cycling #CHScycling #commute\nMVIMG_20181211_054020.jpg"
+   end
+   test "a create activity with a summary containing emoji" do
+     {:ok, activity} =
+       CommonAPI.post(insert(:user), %{
+         "spoiler_text" => ":woollysocks: meow",
+         "status" => "."
+       })
+     result = ActivityView.render("activity.json", activity: activity)
+     expected = ":woollysocks: meow"
+     expected_html =
+       "<img height=\"32px\" width=\"32px\" alt=\"woollysocks\" title=\"woollysocks\" src=\"http://localhost:4001/finmoji/128px/woollysocks-128.png\" /> meow"
+     assert result["summary"] == expected
+     assert result["summary_html"] == expected_html
+   end
+   test "a create activity with a summary containing invalid HTML" do
+     {:ok, activity} =
+       CommonAPI.post(insert(:user), %{
+         "spoiler_text" => "<span style=\"color: magenta; font-size: 32px;\">meow</span>",
+         "status" => "."
+       })
+     result = ActivityView.render("activity.json", activity: activity)
+     expected = "meow"
+     assert result["summary"] == expected
+     assert result["summary_html"] == expected
+   end
    test "a create activity with a note" do
      user = insert(:user)
      other_user = insert(:user, %{nickname: "shp"})
  
      {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
 +    object = Object.normalize(activity.data["object"])
  
      result = ActivityView.render("activity.json", activity: activity)
  
-     convo_id = TwitterAPI.context_to_conversation_id(object.data["context"])
 -    convo_id = Utils.context_to_conversation_id(activity.data["object"]["context"])
++    convo_id = Utils.context_to_conversation_id(object.data["context"])
  
      expected = %{
        "activity_type" => "post",
        "attentions" => [
          UserView.render("show.json", %{user: other_user})
        ],
 -      "created_at" => activity.data["object"]["published"] |> Utils.date_to_asctime(),
 -      "external_url" => activity.data["object"]["id"],
 +      "created_at" => object.data["published"] |> Utils.date_to_asctime(),
 +      "external_url" => object.data["id"],
        "fave_num" => 0,
        "favorited" => false,
        "id" => activity.id,
        "possibly_sensitive" => false,
        "repeat_num" => 0,
        "repeated" => false,
+       "pinned" => false,
        "statusnet_conversation_id" => convo_id,
+       "summary" => "",
+       "summary_html" => "",
        "statusnet_html" =>
-         "Hey <span><a href=\"#{other_user.ap_id}\">@<span>shp</span></a></span>!",
+         "Hey <span class=\"h-card\"><a data-user=\"#{other_user.id}\" class=\"u-url mention\" href=\"#{
+           other_user.ap_id
+         }\">@<span>shp</span></a></span>!",
        "tags" => [],
        "text" => "Hey @shp!",
 -      "uri" => activity.data["object"]["id"],
 +      "uri" => object.data["id"],
        "user" => UserView.render("show.json", %{user: user}),
        "visibility" => "direct",
-       "summary" => nil
+       "card" => nil,
+       "muted" => false
      }
  
      assert result == expected
      user = insert(:user)
      other_user = insert(:user, %{nickname: "shp"})
      {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
 +    object = Object.normalize(activity.data["object"])
  
-     convo_id = TwitterAPI.context_to_conversation_id(object.data["context"])
 -    convo_id = Utils.context_to_conversation_id(activity.data["object"]["context"])
++    convo_id = Utils.context_to_conversation_id(object.data["context"])
  
      mocks = [
        {
-         TwitterAPI,
-         [],
+         Utils,
+         [:passthrough],
          [context_to_conversation_id: fn _ -> false end]
        },
        {
  
        assert result["statusnet_conversation_id"] == convo_id
        assert result["user"]
-       refute called(TwitterAPI.context_to_conversation_id(:_))
+       refute called(Utils.context_to_conversation_id(:_))
        refute called(User.get_cached_by_ap_id(user.ap_id))
        refute called(User.get_cached_by_ap_id(other_user.ap_id))
      end
      {:ok, like, _object} = CommonAPI.favorite(activity.id, other_user)
  
      result = ActivityView.render("activity.json", activity: like)
+     activity = Pleroma.Activity.get_by_ap_id(activity.data["id"])
  
      expected = %{
        "activity_type" => "like",
        "in_reply_to_status_id" => activity.id,
        "is_local" => true,
        "is_post_verb" => false,
+       "favorited_status" => ActivityView.render("activity.json", activity: activity),
        "statusnet_html" => "shp favorited a status.",
        "text" => "shp favorited a status.",
        "uri" => "tag:#{like.data["id"]}:objectType=Favourite",
        "in_reply_to_status_id" => nil,
        "is_local" => true,
        "is_post_verb" => false,
+       "favorited_status" => nil,
        "statusnet_html" => "shp favorited a status.",
        "text" => "shp favorited a status.",
        "uri" => "tag:#{like.data["id"]}:objectType=Favourite",
      other_user = insert(:user, %{nickname: "shp"})
  
      {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
 -    {:ok, announce, _object} = CommonAPI.repeat(activity.id, other_user)
 +    {:ok, announce, object} = CommonAPI.repeat(activity.id, other_user)
  
-     convo_id = TwitterAPI.context_to_conversation_id(object.data["context"])
 -    convo_id = Utils.context_to_conversation_id(activity.data["object"]["context"])
++    convo_id = Utils.context_to_conversation_id(object.data["context"])
  
-     activity = Repo.get(Activity, activity.id)
+     activity = Activity.get_by_id(activity.id)
  
      result = ActivityView.render("activity.json", activity: announce)
  
  
      assert result == expected
    end
+   test "a peertube video" do
+     {:ok, object} =
+       ActivityPub.fetch_object_from_id(
+         "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
+       )
+     %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
+     result = ActivityView.render("activity.json", activity: activity)
+     assert length(result["attachments"]) == 1
+     assert result["summary"] == "Friday Night"
+   end
  end