Extend Mastodon API with public endpoint for getting Favorites timeline of any user...
authorSergey Suprunenko <suprunenko.s@gmail.com>
Tue, 23 Apr 2019 02:47:43 +0000 (02:47 +0000)
committerkaniini <nenolod@gmail.com>
Tue, 23 Apr 2019 02:47:43 +0000 (02:47 +0000)
CHANGELOG.md
docs/api/pleroma_api.md
lib/pleroma/user/info.ex
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/twitter_api_controller.ex
test/web/mastodon_api/mastodon_api_controller_test.exs

index dab53f67c9e6204347e83958d77deff7de05b9a8..70381f3827a854b2fa4f74bb4e12aa1fefe1da4c 100644 (file)
@@ -16,12 +16,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Configuration: `link_name` option
 - Configuration: `fetch_initial_posts` option
 - Configuration: `notify_email` option
-- Pleroma API: User subscribtions
+- Pleroma API: User subscriptions
 - Pleroma API: Healthcheck endpoint
 - Admin API: Endpoints for listing/revoking invite tokens
 - Admin API: Endpoints for making users follow/unfollow each other
 - Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/)
 - Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension)
+- Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
 - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
 - ActivityPub C2S: OAuth endpoints
 - Metadata RelMe provider
index 4b8062d378844abbbb330f77d5e635b99c807f71..190846de937c963bf2920cb9f3700b3d911cb55c 100644 (file)
@@ -77,7 +77,7 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
     * `token`: invite token required when the registrations aren't public.
 * Response: JSON. Returns a user object on success, otherwise returns `{"error": "error_msg"}`
 * Example response:
-```
+```json
 {
        "background_image": null,
        "cover_photo": "https://pleroma.soykaf.com/images/banner.png",
@@ -187,6 +187,62 @@ See [Admin-API](Admin-API.md)
 }
 ```
 
+## `/api/v1/pleroma/accounts/:id/favourites`
+### Returns favorites timeline of any user
+* Method `GET`
+* Authentication: not required
+* Params:
+    * `id`: the id of the account for whom to return results
+    * `limit`: optional, the number of records to retrieve
+    * `since_id`: optional, returns results that are more recent than the specified id
+    * `max_id`: optional, returns results that are older than the specified id
+* Response: JSON, returns a list of Mastodon Status entities on success, otherwise returns `{"error": "error_msg"}`
+* Example response:
+```json
+[
+  {
+    "account": {
+      "id": "9hptFmUF3ztxYh3Svg",
+      "url": "https://pleroma.example.org/users/nick2",
+      "username": "nick2",
+      ...
+    },
+    "application": {"name": "Web", "website": null},
+    "bookmarked": false,
+    "card": null,
+    "content": "This is :moominmamma: note 0",
+    "created_at": "2019-04-15T15:42:15.000Z",
+    "emojis": [],
+    "favourited": false,
+    "favourites_count": 1,
+    "id": "9hptFmVJ02khbzYJaS",
+    "in_reply_to_account_id": null,
+    "in_reply_to_id": null,
+    "language": null,
+    "media_attachments": [],
+    "mentions": [],
+    "muted": false,
+    "pinned": false,
+    "pleroma": {
+      "content": {"text/plain": "This is :moominmamma: note 0"},
+      "conversation_id": 13679,
+      "local": true,
+      "spoiler_text": {"text/plain": "2hu"}
+    },
+    "reblog": null,
+    "reblogged": false,
+    "reblogs_count": 0,
+    "replies_count": 0,
+    "sensitive": false,
+    "spoiler_text": "2hu",
+    "tags": [{"name": "2hu", "url": "/tag/2hu"}],
+    "uri": "https://pleroma.example.org/objects/198ed2a1-7912-4482-b559-244a0369e984",
+    "url": "https://pleroma.example.org/notice/9hptFmVJ02khbzYJaS",
+    "visibility": "public"
+  }
+]
+```
+
 ## `/api/pleroma/notification_settings`
 ### Updates user notification settings
 * Method `PUT`
index 5afa7988cee7287750e2f6e1a0169732c12cd486..7f22a45b5c7116b4af4a86201e2c8396137bea1a 100644 (file)
@@ -38,6 +38,7 @@ defmodule Pleroma.User.Info do
     field(:salmon, :string, default: nil)
     field(:hide_followers, :boolean, default: false)
     field(:hide_follows, :boolean, default: false)
+    field(:hide_favorites, :boolean, default: true)
     field(:pinned_activities, {:array, :string}, default: [])
     field(:flavour, :string, default: nil)
 
@@ -202,6 +203,7 @@ defmodule Pleroma.User.Info do
       :banner,
       :hide_follows,
       :hide_followers,
+      :hide_favorites,
       :background,
       :show_role
     ])
index dfc89defad6b8e310ab82640fa1ec83fb548a69a..0ba8d9eea71ea082a59586de39b37ad51c99e19e 100644 (file)
@@ -1087,6 +1087,43 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     |> render("index.json", %{activities: activities, for: user, as: :activity})
   end
 
+  def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
+    with %User{} = user <- User.get_by_id(id),
+         false <- user.info.hide_favorites do
+      params =
+        params
+        |> Map.put("type", "Create")
+        |> Map.put("favorited_by", user.ap_id)
+        |> Map.put("blocking_user", for_user)
+
+      recipients =
+        if for_user do
+          ["https://www.w3.org/ns/activitystreams#Public"] ++
+            [for_user.ap_id | for_user.following]
+        else
+          ["https://www.w3.org/ns/activitystreams#Public"]
+        end
+
+      activities =
+        recipients
+        |> ActivityPub.fetch_activities(params)
+        |> Enum.reverse()
+
+      conn
+      |> add_link_headers(:favourites, activities)
+      |> put_view(StatusView)
+      |> render("index.json", %{activities: activities, for: for_user, as: :activity})
+    else
+      nil ->
+        {:error, :not_found}
+
+      true ->
+        conn
+        |> put_status(403)
+        |> json(%{error: "Can't get favorites"})
+    end
+  end
+
   def bookmarks(%{assigns: %{user: user}} = conn, _) do
     user = User.get_cached_by_id(user.id)
 
index 6228b5868dad3dfe53c0dfa2aa8a171532451080..ff4f08af57a58a16cf7db7d033577bece2349649 100644 (file)
@@ -395,6 +395,8 @@ defmodule Pleroma.Web.Router do
       get("/accounts/:id", MastodonAPIController, :user)
 
       get("/search", MastodonAPIController, :search)
+
+      get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
     end
   end
 
index 851f328fdc803a6838e1ff5789542ade6d22b5e1..79ed9dad206334eb6774ba761205a20bf984759c 100644 (file)
@@ -632,7 +632,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
 
   defp build_info_cng(user, params) do
     info_params =
-      ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
+      ["no_rich_text", "locked", "hide_followers", "hide_follows", "hide_favorites", "show_role"]
       |> Enum.reduce(%{}, fn key, res ->
         if value = params[key] do
           Map.put(res, key, value == "true")
index 6648b93f90928ee78979e5bfd06104aa5a5cc24e..a229440887a5bd3aa487536608091ce285e58ca7 100644 (file)
@@ -1988,6 +1988,199 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     assert [] = json_response(third_conn, 200)
   end
 
+  describe "getting favorites timeline of specified user" do
+    setup do
+      [current_user, user] = insert_pair(:user, %{info: %{hide_favorites: false}})
+      [current_user: current_user, user: user]
+    end
+
+    test "returns list of statuses favorited by specified user", %{
+      conn: conn,
+      current_user: current_user,
+      user: user
+    } do
+      [activity | _] = insert_pair(:note_activity)
+      CommonAPI.favorite(activity.id, user)
+
+      response =
+        conn
+        |> assign(:user, current_user)
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+        |> json_response(:ok)
+
+      [like] = response
+
+      assert length(response) == 1
+      assert like["id"] == activity.id
+    end
+
+    test "returns favorites for specified user_id when user is not logged in", %{
+      conn: conn,
+      user: user
+    } do
+      activity = insert(:note_activity)
+      CommonAPI.favorite(activity.id, user)
+
+      response =
+        conn
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+        |> json_response(:ok)
+
+      assert length(response) == 1
+    end
+
+    test "returns favorited DM only when user is logged in and he is one of recipients", %{
+      conn: conn,
+      current_user: current_user,
+      user: user
+    } do
+      {:ok, direct} =
+        CommonAPI.post(current_user, %{
+          "status" => "Hi @#{user.nickname}!",
+          "visibility" => "direct"
+        })
+
+      CommonAPI.favorite(direct.id, user)
+
+      response =
+        conn
+        |> assign(:user, current_user)
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+        |> json_response(:ok)
+
+      assert length(response) == 1
+
+      anonymous_response =
+        conn
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+        |> json_response(:ok)
+
+      assert length(anonymous_response) == 0
+    end
+
+    test "does not return others' favorited DM when user is not one of recipients", %{
+      conn: conn,
+      current_user: current_user,
+      user: user
+    } do
+      user_two = insert(:user)
+
+      {:ok, direct} =
+        CommonAPI.post(user_two, %{
+          "status" => "Hi @#{user.nickname}!",
+          "visibility" => "direct"
+        })
+
+      CommonAPI.favorite(direct.id, user)
+
+      response =
+        conn
+        |> assign(:user, current_user)
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+        |> json_response(:ok)
+
+      assert length(response) == 0
+    end
+
+    test "paginates favorites using since_id and max_id", %{
+      conn: conn,
+      current_user: current_user,
+      user: user
+    } do
+      activities = insert_list(10, :note_activity)
+
+      Enum.each(activities, fn activity ->
+        CommonAPI.favorite(activity.id, user)
+      end)
+
+      third_activity = Enum.at(activities, 2)
+      seventh_activity = Enum.at(activities, 6)
+
+      response =
+        conn
+        |> assign(:user, current_user)
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{
+          since_id: third_activity.id,
+          max_id: seventh_activity.id
+        })
+        |> json_response(:ok)
+
+      assert length(response) == 3
+      refute third_activity in response
+      refute seventh_activity in response
+    end
+
+    test "limits favorites using limit parameter", %{
+      conn: conn,
+      current_user: current_user,
+      user: user
+    } do
+      7
+      |> insert_list(:note_activity)
+      |> Enum.each(fn activity ->
+        CommonAPI.favorite(activity.id, user)
+      end)
+
+      response =
+        conn
+        |> assign(:user, current_user)
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{limit: "3"})
+        |> json_response(:ok)
+
+      assert length(response) == 3
+    end
+
+    test "returns empty response when user does not have any favorited statuses", %{
+      conn: conn,
+      current_user: current_user,
+      user: user
+    } do
+      response =
+        conn
+        |> assign(:user, current_user)
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+        |> json_response(:ok)
+
+      assert Enum.empty?(response)
+    end
+
+    test "returns 404 error when specified user is not exist", %{conn: conn} do
+      conn = get(conn, "/api/v1/pleroma/accounts/test/favourites")
+
+      assert json_response(conn, 404) == %{"error" => "Record not found"}
+    end
+
+    test "returns 403 error when user has hidden own favorites", %{
+      conn: conn,
+      current_user: current_user
+    } do
+      user = insert(:user, %{info: %{hide_favorites: true}})
+      activity = insert(:note_activity)
+      CommonAPI.favorite(activity.id, user)
+
+      conn =
+        conn
+        |> assign(:user, current_user)
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+
+      assert json_response(conn, 403) == %{"error" => "Can't get favorites"}
+    end
+
+    test "hides favorites for new users by default", %{conn: conn, current_user: current_user} do
+      user = insert(:user)
+      activity = insert(:note_activity)
+      CommonAPI.favorite(activity.id, user)
+
+      conn =
+        conn
+        |> assign(:user, current_user)
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+
+      assert user.info.hide_favorites
+      assert json_response(conn, 403) == %{"error" => "Can't get favorites"}
+    end
+  end
+
   describe "updating credentials" do
     test "updates the user's bio", %{conn: conn} do
       user = insert(:user)