Merge branch 'develop' into 'feature/2164-unify-api-arguments'
authorfeld <feld@feld.me>
Tue, 2 Feb 2021 17:11:38 +0000 (17:11 +0000)
committerfeld <feld@feld.me>
Tue, 2 Feb 2021 17:11:38 +0000 (17:11 +0000)
# Conflicts:
#   CHANGELOG.md

CHANGELOG.md
docs/development/API/differences_in_mastoapi_responses.md
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/api_spec/operations/account_operation.ex
lib/pleroma/web/api_spec/operations/timeline_operation.ex
lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs
test/support/factory.ex

index 905e35f266f31ea215c37fffb5cba7213a4e95a1..0a24a6a97d54a0e0ed605009ebe7dd900dd63809 100644 (file)
@@ -48,8 +48,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mastodon API: User and conversation mutes can now auto-expire if `expires_in` parameter was given while adding the mute.
 - Admin API: An endpoint to manage frontends.
 - Streaming API: Add follow relationships updates.
-- WebPush: Introduce `pleroma:chat_mention` and `pleroma:emoji_reaction` notification types
-- Mastodon API: Add monthly active users to `/api/v1/instance` (`pleroma.stats.mau`)
+- WebPush: Introduce `pleroma:chat_mention` and `pleroma:emoji_reaction` notification types.
+- Mastodon API: Add monthly active users to `/api/v1/instance` (`pleroma.stats.mau`).
+- Mastodon API: Home, public, hashtag & list timelines accept `only_media`, `remote` & `local` parameters for filtration.
 </details>
 
 ### Fixed
index 84430408b4c3e9fca74f05e476e02eb5622c248e..c83be2faac38659053e9aaf9160c48d5b844db09 100644 (file)
@@ -16,6 +16,12 @@ Adding the parameter `reply_visibility` to the public and home timelines queries
 
 Adding the parameter `instance=lain.com` to the public timeline will show only statuses originating from `lain.com` (or any remote instance).
 
+Home, public, hashtag & list timelines accept these parameters:
+
+- `only_media`: show only statuses with media attached
+- `local`: show only local statuses
+- `remote`: show only remote statuses
+
 ## Statuses
 
 - `visibility`: has additional possible values `list` and `local` (for local-only statuses)
@@ -54,6 +60,16 @@ The `id` parameter can also be the `nickname` of the user. This only works in th
 - `/api/v1/accounts/:id`
 - `/api/v1/accounts/:id/statuses`
 
+`/api/v1/accounts/:id/statuses` endpoint accepts these parameters:
+
+- `pinned`: include only pinned statuses
+- `tagged`: with tag
+- `only_media`: include only statuses with media attached
+- `with_muted`: include statuses/reactions from muted accounts
+- `exclude_reblogs`: exclude reblogs
+- `exclude_replies`: exclude replies
+- `exclude_visibilities`: exclude visibilities
+
 Has these additional fields under the `pleroma` object:
 
 - `ap_id`: nullable URL string, ActivityPub id of the user
index d0bb07aab8f2327a2acab59901bf1f7a671a2a70..98051032aca1b10cb23353be274017556eed7449 100644 (file)
@@ -735,6 +735,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_local(query, _), do: query
 
+  defp restrict_remote(query, %{remote: true}) do
+    from(activity in query, where: activity.local == false)
+  end
+
+  defp restrict_remote(query, _), do: query
+
   defp restrict_actor(query, %{actor_id: actor_id}) do
     from(activity in query, where: activity.actor == ^actor_id)
   end
@@ -1111,6 +1117,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> restrict_tag_all(opts)
     |> restrict_since(opts)
     |> restrict_local(opts)
+    |> restrict_remote(opts)
     |> restrict_actor(opts)
     |> restrict_type(opts)
     |> restrict_state(opts)
index 80acee2f7a21fb689ad62a538105adffac8c438f..a301ce090fa45c0cd55473e3c5f532710f4f514c 100644 (file)
@@ -130,7 +130,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
             :with_muted,
             :query,
             BooleanLike,
-            "Include statuses from muted acccounts."
+            "Include statuses from muted accounts."
           ),
           Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
           Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"),
@@ -144,7 +144,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
             :with_muted,
             :query,
             BooleanLike,
-            "Include reactions from muted acccounts."
+            "Include reactions from muted accounts."
           )
         ] ++ pagination_params(),
       responses: %{
index e1ebdab389c781b643d4ce05ab0c0d907393340a..01396642c6776d60ac1c2db5d3d5feabcc457471 100644 (file)
@@ -25,6 +25,8 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
       security: [%{"oAuth" => ["read:statuses"]}],
       parameters: [
         local_param(),
+        remote_param(),
+        only_media_param(),
         with_muted_param(),
         exclude_visibilities_param(),
         reply_visibility_param() | pagination_params()
@@ -61,6 +63,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
         local_param(),
         instance_param(),
         only_media_param(),
+        remote_param(),
         with_muted_param(),
         exclude_visibilities_param(),
         reply_visibility_param() | pagination_params()
@@ -107,6 +110,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
         ),
         local_param(),
         only_media_param(),
+        remote_param(),
         with_muted_param(),
         exclude_visibilities_param() | pagination_params()
       ],
@@ -132,6 +136,9 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
           required: true
         ),
         with_muted_param(),
+        local_param(),
+        remote_param(),
+        only_media_param(),
         exclude_visibilities_param() | pagination_params()
       ],
       operationId: "TimelineController.list",
@@ -198,4 +205,13 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
       "Show only statuses with media attached?"
     )
   end
+
+  defp remote_param do
+    Operation.parameter(
+      :remote,
+      :query,
+      %Schema{allOf: [BooleanLike], default: false},
+      "Show only remote statuses?"
+    )
+  end
 end
index 08e6f23b98cf8eb8fa8a261c030795359e37a7da..cef299aa49a1579719b3f017aade8edfced9076a 100644 (file)
@@ -51,6 +51,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
       |> Map.put(:reply_filtering_user, user)
       |> Map.put(:announce_filtering_user, user)
       |> Map.put(:user, user)
+      |> Map.put(:local_only, params[:local])
+      |> Map.delete(:local)
 
     activities =
       [user.ap_id | User.following(user)]
@@ -190,6 +192,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
         |> Map.put(:blocking_user, user)
         |> Map.put(:user, user)
         |> Map.put(:muting_user, user)
+        |> Map.put(:local_only, params[:local])
 
       # 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).
index 664375fef162a362091f3fee4437f9b12e7b5d83..cc409451c1a4c398a0db6980017811c9038ea7fb 100644 (file)
@@ -90,6 +90,65 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
                }
              ] = result
     end
+
+    test "filtering", %{conn: conn, user: user} do
+      local_user = insert(:user)
+      {:ok, user, local_user} = User.follow(user, local_user)
+      {:ok, local_activity} = CommonAPI.post(local_user, %{status: "Status"})
+      with_media = create_with_media_activity(local_user)
+
+      remote_user = insert(:user, local: false)
+      {:ok, _user, remote_user} = User.follow(user, remote_user)
+      remote_activity = create_remote_activity(remote_user)
+
+      without_filter_ids =
+        conn
+        |> get("/api/v1/timelines/home")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["id"])
+
+      assert local_activity.id in without_filter_ids
+      assert remote_activity.id in without_filter_ids
+      assert with_media.id in without_filter_ids
+
+      only_local_ids =
+        conn
+        |> get("/api/v1/timelines/home?local=true")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["id"])
+
+      assert local_activity.id in only_local_ids
+      refute remote_activity.id in only_local_ids
+      assert with_media.id in only_local_ids
+
+      only_local_media_ids =
+        conn
+        |> get("/api/v1/timelines/home?local=true&only_media=true")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["id"])
+
+      refute local_activity.id in only_local_media_ids
+      refute remote_activity.id in only_local_media_ids
+      assert with_media.id in only_local_media_ids
+
+      remote_ids =
+        conn
+        |> get("/api/v1/timelines/home?remote=true")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["id"])
+
+      refute local_activity.id in remote_ids
+      assert remote_activity.id in remote_ids
+      refute with_media.id in remote_ids
+
+      assert conn
+             |> get("/api/v1/timelines/home?remote=true&only_media=true")
+             |> json_response_and_validate_schema(200) == []
+
+      assert conn
+             |> get("/api/v1/timelines/home?remote=true&local=true")
+             |> json_response_and_validate_schema(200) == []
+    end
   end
 
   describe "public" do
@@ -98,27 +157,80 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
       user = insert(:user)
 
       {:ok, activity} = CommonAPI.post(user, %{status: "test"})
+      with_media = create_with_media_activity(user)
 
-      _activity = insert(:note_activity, local: false)
+      remote = insert(:note_activity, local: false)
 
-      conn = get(conn, "/api/v1/timelines/public?local=False")
+      assert conn
+             |> get("/api/v1/timelines/public?local=False")
+             |> json_response_and_validate_schema(:ok)
+             |> length == 3
 
-      assert length(json_response_and_validate_schema(conn, :ok)) == 2
+      local_ids =
+        conn
+        |> get("/api/v1/timelines/public?local=True")
+        |> json_response_and_validate_schema(:ok)
+        |> Enum.map(& &1["id"])
 
-      conn = get(build_conn(), "/api/v1/timelines/public?local=True")
+      assert activity.id in local_ids
+      assert with_media.id in local_ids
+      refute remote.id in local_ids
 
-      assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok)
+      local_ids =
+        conn
+        |> get("/api/v1/timelines/public?local=True")
+        |> json_response_and_validate_schema(:ok)
+        |> Enum.map(& &1["id"])
+
+      assert activity.id in local_ids
+      assert with_media.id in local_ids
+      refute remote.id in local_ids
+
+      local_ids =
+        conn
+        |> get("/api/v1/timelines/public?local=True&only_media=true")
+        |> json_response_and_validate_schema(:ok)
+        |> Enum.map(& &1["id"])
+
+      refute activity.id in local_ids
+      assert with_media.id in local_ids
+      refute remote.id in local_ids
+
+      local_ids =
+        conn
+        |> get("/api/v1/timelines/public?local=1")
+        |> json_response_and_validate_schema(:ok)
+        |> Enum.map(& &1["id"])
+
+      assert activity.id in local_ids
+      assert with_media.id in local_ids
+      refute remote.id in local_ids
+
+      remote_id = remote.id
+
+      assert [%{"id" => ^remote_id}] =
+               conn
+               |> get("/api/v1/timelines/public?remote=true")
+               |> json_response_and_validate_schema(:ok)
 
-      conn = get(build_conn(), "/api/v1/timelines/public?local=1")
+      with_media_id = with_media.id
 
-      assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok)
+      assert [%{"id" => ^with_media_id}] =
+               conn
+               |> get("/api/v1/timelines/public?only_media=true")
+               |> json_response_and_validate_schema(:ok)
+
+      assert conn
+             |> get("/api/v1/timelines/public?remote=true&only_media=true")
+             |> json_response_and_validate_schema(:ok) == []
 
       # does not contain repeats
       {:ok, _} = CommonAPI.repeat(activity.id, user)
 
-      conn = get(build_conn(), "/api/v1/timelines/public?local=true")
-
-      assert [_] = json_response_and_validate_schema(conn, :ok)
+      assert [_, _] =
+               conn
+               |> get("/api/v1/timelines/public?local=true")
+               |> json_response_and_validate_schema(:ok)
     end
 
     test "the public timeline includes only public statuses for an authenticated user" do
@@ -544,6 +656,77 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
                }
              ] = result
     end
+
+    test "filtering", %{user: user, conn: conn} do
+      {:ok, list} = Pleroma.List.create("name", user)
+
+      local_user = insert(:user)
+      {:ok, local_activity} = CommonAPI.post(local_user, %{status: "Marisa is stupid."})
+      with_media = create_with_media_activity(local_user)
+      {:ok, list} = Pleroma.List.follow(list, local_user)
+
+      remote_user = insert(:user, local: false)
+      remote_activity = create_remote_activity(remote_user)
+      {:ok, list} = Pleroma.List.follow(list, remote_user)
+
+      all_ids =
+        conn
+        |> get("/api/v1/timelines/list/#{list.id}")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["id"])
+
+      assert local_activity.id in all_ids
+      assert with_media.id in all_ids
+      assert remote_activity.id in all_ids
+
+      only_local_ids =
+        conn
+        |> get("/api/v1/timelines/list/#{list.id}?local=true")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["id"])
+
+      assert local_activity.id in only_local_ids
+      assert with_media.id in only_local_ids
+      refute remote_activity.id in only_local_ids
+
+      only_local_media_ids =
+        conn
+        |> get("/api/v1/timelines/list/#{list.id}?local=true&only_media=true")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["id"])
+
+      refute local_activity.id in only_local_media_ids
+      assert with_media.id in only_local_media_ids
+      refute remote_activity.id in only_local_media_ids
+
+      remote_ids =
+        conn
+        |> get("/api/v1/timelines/list/#{list.id}?remote=true")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["id"])
+
+      refute local_activity.id in remote_ids
+      refute with_media.id in remote_ids
+      assert remote_activity.id in remote_ids
+
+      assert conn
+             |> get("/api/v1/timelines/list/#{list.id}?remote=true&only_media=true")
+             |> json_response_and_validate_schema(200) == []
+
+      only_media_ids =
+        conn
+        |> get("/api/v1/timelines/list/#{list.id}?only_media=true")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["id"])
+
+      refute local_activity.id in only_media_ids
+      assert with_media.id in only_media_ids
+      refute remote_activity.id in only_media_ids
+
+      assert conn
+             |> get("/api/v1/timelines/list/#{list.id}?only_media=true&local=true&remote=true")
+             |> json_response_and_validate_schema(200) == []
+    end
   end
 
   describe "hashtag" do
@@ -554,19 +737,85 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
       following = insert(:user)
 
       {:ok, activity} = CommonAPI.post(following, %{status: "test #2hu"})
+      with_media = create_with_media_activity(following)
 
-      nconn = get(conn, "/api/v1/timelines/tag/2hu")
+      remote = insert(:user, local: false)
+      remote_activity = create_remote_activity(remote)
 
-      assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok)
+      all_ids =
+        conn
+        |> get("/api/v1/timelines/tag/2hu")
+        |> json_response_and_validate_schema(:ok)
+        |> Enum.map(& &1["id"])
 
-      assert id == to_string(activity.id)
+      assert activity.id in all_ids
+      assert with_media.id in all_ids
+      assert remote_activity.id in all_ids
 
       # works for different capitalization too
-      nconn = get(conn, "/api/v1/timelines/tag/2HU")
+      all_ids =
+        conn
+        |> get("/api/v1/timelines/tag/2HU")
+        |> json_response_and_validate_schema(:ok)
+        |> Enum.map(& &1["id"])
 
-      assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok)
+      assert activity.id in all_ids
+      assert with_media.id in all_ids
+      assert remote_activity.id in all_ids
 
-      assert id == to_string(activity.id)
+      local_ids =
+        conn
+        |> get("/api/v1/timelines/tag/2hu?local=true")
+        |> json_response_and_validate_schema(:ok)
+        |> Enum.map(& &1["id"])
+
+      assert activity.id in local_ids
+      assert with_media.id in local_ids
+      refute remote_activity.id in local_ids
+
+      remote_ids =
+        conn
+        |> get("/api/v1/timelines/tag/2hu?remote=true")
+        |> json_response_and_validate_schema(:ok)
+        |> Enum.map(& &1["id"])
+
+      refute activity.id in remote_ids
+      refute with_media.id in remote_ids
+      assert remote_activity.id in remote_ids
+
+      media_ids =
+        conn
+        |> get("/api/v1/timelines/tag/2hu?only_media=true")
+        |> json_response_and_validate_schema(:ok)
+        |> Enum.map(& &1["id"])
+
+      refute activity.id in media_ids
+      assert with_media.id in media_ids
+      refute remote_activity.id in media_ids
+
+      media_local_ids =
+        conn
+        |> get("/api/v1/timelines/tag/2hu?only_media=true&local=true")
+        |> json_response_and_validate_schema(:ok)
+        |> Enum.map(& &1["id"])
+
+      refute activity.id in media_local_ids
+      assert with_media.id in media_local_ids
+      refute remote_activity.id in media_local_ids
+
+      ids =
+        conn
+        |> get("/api/v1/timelines/tag/2hu?only_media=true&local=true&remote=true")
+        |> json_response_and_validate_schema(:ok)
+        |> Enum.map(& &1["id"])
+
+      refute activity.id in ids
+      refute with_media.id in ids
+      refute remote_activity.id in ids
+
+      assert conn
+             |> get("/api/v1/timelines/tag/2hu?only_media=true&remote=true")
+             |> json_response_and_validate_schema(:ok) == []
     end
 
     test "multi-hashtag timeline", %{conn: conn} do
@@ -726,4 +975,37 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
       ensure_authenticated_access(base_uri)
     end
   end
+
+  defp create_remote_activity(user) do
+    obj =
+      insert(:note, %{
+        data: %{
+          "to" => [
+            "https://www.w3.org/ns/activitystreams#Public",
+            User.ap_followers(user)
+          ]
+        },
+        user: user
+      })
+
+    insert(:note_activity, %{
+      note: obj,
+      recipients: [
+        "https://www.w3.org/ns/activitystreams#Public",
+        User.ap_followers(user)
+      ],
+      user: user,
+      local: false
+    })
+  end
+
+  defp create_with_media_activity(user) do
+    obj = insert(:attachment_note, user: user)
+
+    insert(:note_activity, %{
+      note: obj,
+      recipients: ["https://www.w3.org/ns/activitystreams#Public", User.ap_followers(user)],
+      user: user
+    })
+  end
 end
index bf9592064ed7c63eda4d16e508c36f074f0b7e23..436e19409c5ab0f4701bea5aa4dd1f4e32ca4ecb 100644 (file)
@@ -104,6 +104,37 @@ defmodule Pleroma.Factory do
     }
   end
 
+  def attachment_note_factory(attrs \\ %{}) do
+    user = attrs[:user] || insert(:user)
+    {length, attrs} = Map.pop(attrs, :length, 1)
+
+    data = %{
+      "attachment" =>
+        Stream.repeatedly(fn -> attachment_data(user.ap_id, attrs[:href]) end)
+        |> Enum.take(length)
+    }
+
+    build(:note, Map.put(attrs, :data, data))
+  end
+
+  defp attachment_data(ap_id, href) do
+    href = href || sequence(:href, &"#{Pleroma.Web.Endpoint.url()}/media/#{&1}.jpg")
+
+    %{
+      "url" => [
+        %{
+          "href" => href,
+          "type" => "Link",
+          "mediaType" => "image/jpeg"
+        }
+      ],
+      "name" => "some name",
+      "type" => "Document",
+      "actor" => ap_id,
+      "mediaType" => "image/jpeg"
+    }
+  end
+
   def audio_factory(attrs \\ %{}) do
     text = sequence(:text, &"lain radio episode #{&1}")