logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git
commit: b36891d1e6f5d4b0874ef19c4418a46d580c4701
parent 2012800ed1618eeafddaaaaa05df4d3e10a2afff
Author: feld <feld@feld.me>
Date:   Tue,  2 Feb 2021 17:18:07 +0000

Merge branch 'feature/2164-unify-api-arguments' into 'develop'

Unifying timelines api arguments

Closes #2164

See merge request pleroma/pleroma!3281

Diffstat:

MCHANGELOG.md5+++--
Mdocs/development/API/differences_in_mastoapi_responses.md16++++++++++++++++
Mlib/pleroma/web/activity_pub/activity_pub.ex7+++++++
Mlib/pleroma/web/api_spec/operations/account_operation.ex4++--
Mlib/pleroma/web/api_spec/operations/timeline_operation.ex16++++++++++++++++
Mlib/pleroma/web/mastodon_api/controllers/timeline_controller.ex3+++
Mtest/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs314+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mtest/support/factory.ex31+++++++++++++++++++++++++++++++
8 files changed, 376 insertions(+), 20 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -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 diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md @@ -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 diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -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) diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -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: %{ diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -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). diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -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 diff --git a/test/support/factory.ex b/test/support/factory.ex @@ -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}")