Merge branch 'develop' into 'feature/local-only-scope'
authorminibikini <egor@kislitsyn.com>
Tue, 27 Oct 2020 18:59:19 +0000 (18:59 +0000)
committerminibikini <egor@kislitsyn.com>
Tue, 27 Oct 2020 18:59:19 +0000 (18:59 +0000)
# Conflicts:
#   CHANGELOG.md

1  2 
CHANGELOG.md
lib/pleroma/web/common_api/utils.ex
test/pleroma/web/common_api_test.exs
test/pleroma/web/mastodon_api/controllers/status_controller_test.exs

diff --combined CHANGELOG.md
index f063817603f5449f859949fc8d4a20d0c519e592,ac91d4d9ee9f710990675479eb00f83296dc4281..361ad503887be36bd8a99f290d824078cc66d064
@@@ -12,13 -12,14 +12,16 @@@ The format is based on [Keep a Changelo
  - Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details).
  - Pleroma API: Importing the mutes users from CSV files.
  - Experimental websocket-based federation between Pleroma instances.
 +- Support for local-only statuses
+ - App metrics: ability to restrict access to specified IP whitelist.
 +
  ### Changed
  
  - **Breaking** Requires `libmagic` (or `file`) to guess file types.
  - **Breaking:** Pleroma Admin API: emoji packs and files routes changed.
  - **Breaking:** Sensitive/NSFW statuses no longer disable link previews.
+ - **Breaking:** App metrics endpoint (`/api/pleroma/app_metrics`) is disabled by default, check `docs/API/prometheus.md` on enabling and configuring. 
  - Search: Users are now findable by their urls.
  - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated.
  - Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated.
@@@ -48,6 -49,7 +51,7 @@@ switched to a new configuration mechani
  
  - Add documented-but-missing chat pagination.
  - Allow sending out emails again.
+ - Allow sending chat messages to yourself
  
  ## Unreleased (Patch)
  
index d57ba4209f459ef74df42bc83c30b466b643e133,3b71adf0e577b1fdabaa50c5b7917968c7f02681..abf6c40d5d4af93c6eaa4234b82fe087890722d2
@@@ -16,7 -16,6 +16,7 @@@ defmodule Pleroma.Web.CommonAPI.Utils d
    alias Pleroma.User
    alias Pleroma.Web.ActivityPub.Utils
    alias Pleroma.Web.ActivityPub.Visibility
 +  alias Pleroma.Web.CommonAPI.ActivityDraft
    alias Pleroma.Web.MediaProxy
    alias Pleroma.Web.Plugs.AuthenticationPlug
  
      {_, descs} = Jason.decode(descs_str)
  
      Enum.map(ids, fn media_id ->
 -      case Repo.get(Object, media_id) do
 -        %Object{data: data} ->
 -          Map.put(data, "name", descs[media_id])
 -
 -        _ ->
 -          nil
 +      with %Object{data: data} <- Repo.get(Object, media_id) do
 +        Map.put(data, "name", descs[media_id])
        end
      end)
      |> Enum.reject(&is_nil/1)
    end
  
 -  @spec get_to_and_cc(
 -          User.t(),
 -          list(String.t()),
 -          Activity.t() | nil,
 -          String.t(),
 -          Participation.t() | nil
 -        ) :: {list(String.t()), list(String.t())}
 +  @spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
  
 -  def get_to_and_cc(_, _, _, _, %Participation{} = participation) do
 +  def get_to_and_cc(%{in_reply_to_conversation: %Participation{} = participation}) do
      participation = Repo.preload(participation, :recipients)
      {Enum.map(participation.recipients, & &1.ap_id), []}
    end
  
 -  def get_to_and_cc(user, mentioned_users, inReplyTo, "public", _) do
 -    to = [Pleroma.Constants.as_public() | mentioned_users]
 -    cc = [user.follower_address]
 +  def get_to_and_cc(%{visibility: "public"} = draft) do
 +    to = [public_uri(draft) | draft.mentions]
 +    cc = [draft.user.follower_address]
  
 -    if inReplyTo do
 -      {Enum.uniq([inReplyTo.data["actor"] | to]), cc}
 +    if draft.in_reply_to do
 +      {Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
      else
        {to, cc}
      end
    end
  
 -  def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted", _) do
 -    to = [user.follower_address | mentioned_users]
 -    cc = [Pleroma.Constants.as_public()]
 +  def get_to_and_cc(%{visibility: "unlisted"} = draft) do
 +    to = [draft.user.follower_address | draft.mentions]
 +    cc = [public_uri(draft)]
  
 -    if inReplyTo do
 -      {Enum.uniq([inReplyTo.data["actor"] | to]), cc}
 +    if draft.in_reply_to do
 +      {Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
      else
        {to, cc}
      end
    end
  
 -  def get_to_and_cc(user, mentioned_users, inReplyTo, "private", _) do
 -    {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct", nil)
 -    {[user.follower_address | to], cc}
 +  def get_to_and_cc(%{visibility: "private"} = draft) do
 +    {to, cc} = get_to_and_cc(struct(draft, visibility: "direct"))
 +    {[draft.user.follower_address | to], cc}
    end
  
 -  def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct", _) do
 +  def get_to_and_cc(%{visibility: "direct"} = draft) do
      # If the OP is a DM already, add the implicit actor.
 -    if inReplyTo && Visibility.is_direct?(inReplyTo) do
 -      {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
 +    if draft.in_reply_to && Visibility.is_direct?(draft.in_reply_to) do
 +      {Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]), []}
      else
 -      {mentioned_users, []}
 +      {draft.mentions, []}
      end
    end
  
 -  def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}, _), do: {mentions, []}
 +  def get_to_and_cc(%{visibility: {:list, _}, mentions: mentions}), do: {mentions, []}
 +
 +  defp public_uri(%{params: %{local_only: true}}), do: Pleroma.Constants.as_local_public()
 +  defp public_uri(_), do: Pleroma.Constants.as_public()
  
    def get_addressed_users(_, to) when is_list(to) do
      User.get_ap_ids_by_nicknames(to)
      end
    end
  
 -  def make_content_html(
 -        status,
 -        attachments,
 -        data,
 -        visibility
 -      ) do
 +  def make_content_html(%ActivityDraft{} = draft) do
      attachment_links =
 -      data
 +      draft.params
        |> Map.get("attachment_links", Config.get([:instance, :attachment_links]))
        |> truthy_param?()
  
 -    content_type = get_content_type(data[:content_type])
 +    content_type = get_content_type(draft.params[:content_type])
  
      options =
 -      if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
 +      if draft.visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
          [safe_mention: true]
        else
          []
        end
  
 -    status
 +    draft.status
      |> format_input(content_type, options)
 -    |> maybe_add_attachments(attachments, attachment_links)
 -    |> maybe_add_nsfw_tag(data)
 +    |> maybe_add_attachments(draft.attachments, attachment_links)
 +    |> maybe_add_nsfw_tag(draft.params)
    end
  
    defp get_content_type(content_type) do
    def format_input(text, format, options \\ [])
  
    @doc """
-   Formatting text to plain text.
+   Formatting text to plain text, BBCode, HTML, or Markdown
    """
    def format_input(text, "text/plain", options) do
      text
          end).()
    end
  
-   @doc """
-   Formatting text as BBCode.
-   """
    def format_input(text, "text/bbcode", options) do
      text
      |> String.replace(~r/\r/, "")
      |> Formatter.linkify(options)
    end
  
-   @doc """
-   Formatting text to html.
-   """
    def format_input(text, "text/html", options) do
      text
      |> Formatter.html_escape("text/html")
      |> Formatter.linkify(options)
    end
  
-   @doc """
-   Formatting text to markdown.
-   """
    def format_input(text, "text/markdown", options) do
      text
      |> Formatter.mentions_escape(options)
      |> Formatter.html_escape("text/html")
    end
  
 -  def make_note_data(
 -        actor,
 -        to,
 -        context,
 -        content_html,
 -        attachments,
 -        in_reply_to,
 -        tags,
 -        summary \\ nil,
 -        cc \\ [],
 -        sensitive \\ false,
 -        extra_params \\ %{}
 -      ) do
 +  def make_note_data(%ActivityDraft{} = draft) do
      %{
        "type" => "Note",
 -      "to" => to,
 -      "cc" => cc,
 -      "content" => content_html,
 -      "summary" => summary,
 -      "sensitive" => truthy_param?(sensitive),
 -      "context" => context,
 -      "attachment" => attachments,
 -      "actor" => actor,
 -      "tag" => Keyword.values(tags) |> Enum.uniq()
 +      "to" => draft.to,
 +      "cc" => draft.cc,
 +      "content" => draft.content_html,
 +      "summary" => draft.summary,
 +      "sensitive" => draft.sensitive,
 +      "context" => draft.context,
 +      "attachment" => draft.attachments,
 +      "actor" => draft.user.ap_id,
 +      "tag" => Keyword.values(draft.tags) |> Enum.uniq()
      }
 -    |> add_in_reply_to(in_reply_to)
 -    |> Map.merge(extra_params)
 +    |> add_in_reply_to(draft.in_reply_to)
 +    |> Map.merge(draft.extra)
    end
  
    defp add_in_reply_to(object, nil), do: object
index a5d3955580a7fb3a1fdf374c1cb818f78d48e254,c5b90ad84297b7d569eac29dd92069a6f9125328..e1dddd21aeae73bf4b8abddd3595f532fe785acd
@@@ -95,6 -95,20 +95,20 @@@ defmodule Pleroma.Web.CommonAPITest d
    describe "posting chat messages" do
      setup do: clear_config([:instance, :chat_limit])
  
+     test "it posts a self-chat" do
+       author = insert(:user)
+       recipient = author
+       {:ok, activity} =
+         CommonAPI.post_chat_message(
+           author,
+           recipient,
+           "remember to buy milk when milk truk arive"
+         )
+       assert activity.data["type"] == "Create"
+     end
      test "it posts a chat message without content but with an attachment" do
        author = insert(:user)
        recipient = insert(:user)
        assert {:error, "The status is over the character limit"} =
                 CommonAPI.post(user, %{status: "foobar"})
  
-       assert {:ok, activity} = CommonAPI.post(user, %{status: "12345"})
+       assert {:ok, _activity} = CommonAPI.post(user, %{status: "12345"})
      end
  
      test "it can handle activities that expire" do
               } = CommonAPI.get_user("")
      end
    end
 +
 +  describe "with `local_only` enabled" do
 +    setup do: clear_config([:instance, :federating], true)
 +
 +    test "post" do
 +      user = insert(:user)
 +
 +      with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
 +        {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", local_only: true})
 +
 +        assert Activity.local_only?(activity)
 +        assert_not_called(Pleroma.Web.Federator.publish(activity))
 +      end
 +    end
 +
 +    test "delete" do
 +      user = insert(:user)
 +
 +      {:ok, %Activity{id: activity_id}} =
 +        CommonAPI.post(user, %{status: "#2hu #2HU", local_only: true})
 +
 +      with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
 +        assert {:ok, %Activity{data: %{"deleted_activity_id" => ^activity_id}} = activity} =
 +                 CommonAPI.delete(activity_id, user)
 +
 +        assert Activity.local_only?(activity)
 +        assert_not_called(Pleroma.Web.Federator.publish(activity))
 +      end
 +    end
 +
 +    test "repeat" do
 +      user = insert(:user)
 +      other_user = insert(:user)
 +
 +      {:ok, %Activity{id: activity_id}} =
 +        CommonAPI.post(other_user, %{status: "cofe", local_only: true})
 +
 +      with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
 +        assert {:ok, %Activity{data: %{"type" => "Announce"}} = activity} =
 +                 CommonAPI.repeat(activity_id, user)
 +
 +        assert Activity.local_only?(activity)
 +        refute called(Pleroma.Web.Federator.publish(activity))
 +      end
 +    end
 +
 +    test "unrepeat" do
 +      user = insert(:user)
 +      other_user = insert(:user)
 +
 +      {:ok, %Activity{id: activity_id}} =
 +        CommonAPI.post(other_user, %{status: "cofe", local_only: true})
 +
 +      assert {:ok, _} = CommonAPI.repeat(activity_id, user)
 +
 +      with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
 +        assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
 +                 CommonAPI.unrepeat(activity_id, user)
 +
 +        assert Activity.local_only?(activity)
 +        refute called(Pleroma.Web.Federator.publish(activity))
 +      end
 +    end
 +
 +    test "favorite" do
 +      user = insert(:user)
 +      other_user = insert(:user)
 +
 +      {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", local_only: true})
 +
 +      with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
 +        assert {:ok, %Activity{data: %{"type" => "Like"}} = activity} =
 +                 CommonAPI.favorite(user, activity.id)
 +
 +        assert Activity.local_only?(activity)
 +        refute called(Pleroma.Web.Federator.publish(activity))
 +      end
 +    end
 +
 +    test "unfavorite" do
 +      user = insert(:user)
 +      other_user = insert(:user)
 +
 +      {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", local_only: true})
 +
 +      {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
 +
 +      with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
 +        assert {:ok, activity} = CommonAPI.unfavorite(activity.id, user)
 +        assert Activity.local_only?(activity)
 +        refute called(Pleroma.Web.Federator.publish(activity))
 +      end
 +    end
 +
 +    test "react_with_emoji" do
 +      user = insert(:user)
 +      other_user = insert(:user)
 +      {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", local_only: true})
 +
 +      with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
 +        assert {:ok, %Activity{data: %{"type" => "EmojiReact"}} = activity} =
 +                 CommonAPI.react_with_emoji(activity.id, user, "👍")
 +
 +        assert Activity.local_only?(activity)
 +        refute called(Pleroma.Web.Federator.publish(activity))
 +      end
 +    end
 +
 +    test "unreact_with_emoji" do
 +      user = insert(:user)
 +      other_user = insert(:user)
 +      {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", local_only: true})
 +
 +      {:ok, _reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
 +
 +      with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
 +        assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
 +                 CommonAPI.unreact_with_emoji(activity.id, user, "👍")
 +
 +        assert Activity.local_only?(activity)
 +        refute called(Pleroma.Web.Federator.publish(activity))
 +      end
 +    end
 +  end
  end
index 4acf7a18e2d79096d1110303a8708e3891be1a1c,436608e515094a3733a02d5ec294b25e6cac582d..ddddd0ea06744851c34e9378441ce9b844465194
@@@ -937,7 -937,7 +937,7 @@@ defmodule Pleroma.Web.MastodonAPI.Statu
          |> get("/api/v1/statuses/#{reblog_activity1.id}")
  
        assert %{
-                "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2},
+                "reblog" => %{"id" => _id, "reblogged" => false, "reblogs_count" => 2},
                 "reblogged" => false,
                 "favourited" => false,
                 "bookmarked" => false
               |> get("/api/v1/statuses/#{activity.id}")
               |> json_response_and_validate_schema(:ok)
    end
 +
 +  test "posting a local only status" do
 +    %{user: _user, conn: conn} = oauth_access(["write:statuses"])
 +
 +    conn_one =
 +      conn
 +      |> put_req_header("content-type", "application/json")
 +      |> post("/api/v1/statuses", %{
 +        "status" => "cofe",
 +        "local_only" => "true"
 +      })
 +
 +    local = Pleroma.Constants.as_local_public()
 +
 +    assert %{"content" => "cofe", "id" => id, "pleroma" => %{"local_only" => true}} =
 +             json_response(conn_one, 200)
 +
 +    assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)
 +  end
  end