add pinned posts
authorEgor Kislitsyn <egor@kislitsyn.com>
Mon, 7 Jan 2019 13:45:33 +0000 (20:45 +0700)
committerEgor Kislitsyn <egor@kislitsyn.com>
Mon, 7 Jan 2019 13:45:33 +0000 (20:45 +0700)
lib/pleroma/user/info.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
lib/pleroma/web/router.ex
test/web/activity_pub/activity_pub_test.exs
test/web/common_api/common_api_test.exs
test/web/mastodon_api/mastodon_api_controller_test.exs

index 2f419a5a26066a73072bbffa6dacca7e9cda58b7..ffb800177de59daf2127f004bc2f5a132248870d 100644 (file)
@@ -31,6 +31,7 @@ defmodule Pleroma.User.Info do
     field(:hub, :string, default: nil)
     field(:salmon, :string, default: nil)
     field(:hide_network, :boolean, default: false)
+    field(:pinned_activities, {:array, :integer}, default: [])
 
     # Found in the wild
     # ap_id -> Where is this used?
@@ -198,4 +199,26 @@ defmodule Pleroma.User.Info do
       :is_admin
     ])
   end
+
+  def add_pinnned_activity(info, %Pleroma.Activity{id: id}) do
+    if id not in info.pinned_activities do
+      max_pinned_posts = Pleroma.Config.get([:instance, :max_pinned_posts], 0)
+      params = %{pinned_activities: info.pinned_activities ++ [id]}
+
+      info
+      |> cast(params, [:pinned_activities])
+      |> validate_length(:pinned_activities,
+        max: max_pinned_posts,
+        message: "You have already pinned the maximum number of toots"
+      )
+    else
+      change(info)
+    end
+  end
+
+  def remove_pinnned_activity(info, %Pleroma.Activity{id: id}) do
+    params = %{pinned_activities: List.delete(info.pinned_activities, id)}
+
+    cast(info, params, [:pinned_activities])
+  end
 end
index 4685f6d95ad796f6e8cca791c6426361b60b90ee..c5f62c4f8c5702156e6efca2059bfb577d95607f 100644 (file)
@@ -394,6 +394,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
       |> 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
@@ -552,6 +553,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     )
   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
+
   def fetch_activities_query(recipients, opts \\ %{}) do
     base_query =
       from(
@@ -576,6 +583,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> restrict_visibility(opts)
     |> restrict_replies(opts)
     |> restrict_reblogs(opts)
+    |> restrict_pinned(opts)
   end
 
   def fetch_activities(recipients, opts \\ %{}) do
index bb3c38f006f0715b8801cc79640c5b3a22c1a954..6d22813b20d57a63bfe0b096e1665b7a6afa390c 100644 (file)
@@ -164,4 +164,38 @@ defmodule Pleroma.Web.CommonAPI do
       object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
     })
   end
+
+  def pin(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.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
 end
index 95d0f849c0a34e0a3fac19d02710e06f47617b2e..2fb2943f1b942fd2f7801b3c320d2cf6d1235ceb 100644 (file)
@@ -256,13 +256,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
 
   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
+      activities = ActivityPub.fetch_user_activities(user, reading_user, params)
 
       conn
       |> add_link_headers(:user_statuses, activities, params["id"])
@@ -409,6 +403,27 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
+  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
+
+  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 notifications(%{assigns: %{user: user}} = conn, params) do
     notifications = Notification.for_user(user, params)
 
index 8df45bf4da2344053a835375019b580529ed05e9..ad73d886792dcb764579b7b2fdbd6648f7b7f209 100644 (file)
@@ -188,6 +188,8 @@ defmodule Pleroma.Web.Router do
     post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
     post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
     post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
+    post("/statuses/:id/pin", MastodonAPIController, :pin_status)
+    post("/statuses/:id/unpin", MastodonAPIController, :unpin_status)
 
     post("/notifications/clear", MastodonAPIController, :clear_notifications)
     post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
index 2453998ad8a54f4ed59c0dfce92c55335aa6bf48..7d9febc470e6d0169b5afa1c0f3e0d1c6e7fc149 100644 (file)
@@ -601,6 +601,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     assert object
   end
 
+  test "returned pinned posts" do
+    Pleroma.Config.put([:instance, :max_pinned_posts], 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 = User.get_by_ap_id(user.ap_id)
+    CommonAPI.pin(activity_two.id, user)
+
+    user = User.get_by_ap_id(user.ap_id)
+    CommonAPI.pin(activity_three.id, user)
+
+    user = User.get_by_ap_id(user.ap_id)
+    activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"})
+
+    assert 3 = length(activities)
+  end
+
   def data_uri do
     File.read!("test/fixtures/avatar_data_uri")
   end
index c3674711aee7e7cfd5daa65bd7c53856790ca089..59beb312021b05df1bd454c0083b19cedce6e688 100644 (file)
@@ -96,4 +96,40 @@ defmodule Pleroma.Web.CommonAPI.Test do
       {:error, _} = CommonAPI.favorite(activity.id, user)
     end
   end
+
+  describe "pinned posts" do
+    test "pin post" do
+      Pleroma.Config.put([:instance, :max_pinned_posts], 1)
+      user = insert(:user)
+
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
+
+      assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
+    end
+
+    test "max pinned posts" do
+      Pleroma.Config.put([:instance, :max_pinned_posts], 1)
+      user = insert(:user)
+
+      {:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
+      {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
+
+      assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user)
+
+      user = User.get_by_ap_id(user.ap_id)
+
+      assert {:error, "You have already pinned the maximum number of toots"} =
+               CommonAPI.pin(activity_two.id, user)
+    end
+
+    test "unpin post" do
+      Pleroma.Config.put([:instance, :max_pinned_posts], 1)
+      user = insert(:user)
+
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
+      {:ok, activity} = CommonAPI.pin(activity.id, user)
+
+      assert {:ok, ^activity} = CommonAPI.unpin(activity.id, user)
+    end
+  end
 end
index 0136acf8cdfcdfe5e12152c1075d5152be6ce020..7f6c9fb88c6f6ba0ded890a0c51230da9cb01c28 100644 (file)
@@ -1453,4 +1453,99 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     user = User.get_cached_by_ap_id(user.ap_id)
     assert user.info.settings == %{"programming" => "socks"}
   end
+
+  describe "pinned posts" do
+    test "returns pinned posts", %{conn: conn} do
+      Pleroma.Config.put([:instance, :max_pinned_posts], 1)
+      user = insert(:user)
+
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
+      {:ok, _} = CommonAPI.pin(activity.id, user)
+
+      result =
+        conn
+        |> assign(:user, user)
+        |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
+        |> Map.get(:resp_body)
+        |> Jason.decode!()
+
+      id_str = Integer.to_string(activity.id)
+
+      assert [%{"id" => ^id_str}] = result
+    end
+
+    test "pin post", %{conn: conn} do
+      Pleroma.Config.put([:instance, :max_pinned_posts], 1)
+      user = insert(:user)
+
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
+      id_str = Integer.to_string(activity.id)
+
+      assert %{"id" => ^id_str} =
+               conn
+               |> assign(:user, user)
+               |> post("/api/v1/statuses/#{activity.id}/pin")
+               |> Map.get(:resp_body)
+               |> Jason.decode!()
+
+      assert [%{"id" => ^id_str}] =
+               conn
+               |> assign(:user, user)
+               |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
+               |> Map.get(:resp_body)
+               |> Jason.decode!()
+    end
+
+    test "unpin post", %{conn: conn} do
+      Pleroma.Config.put([:instance, :max_pinned_posts], 1)
+      user = insert(:user)
+
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
+      {:ok, _} = CommonAPI.pin(activity.id, user)
+
+      id_str = Integer.to_string(activity.id)
+      user = User.get_by_ap_id(user.ap_id)
+
+      assert %{"id" => ^id_str} =
+               conn
+               |> assign(:user, user)
+               |> post("/api/v1/statuses/#{activity.id}/unpin")
+               |> Map.get(:resp_body)
+               |> Jason.decode!()
+
+      assert [] =
+               conn
+               |> assign(:user, user)
+               |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
+               |> Map.get(:resp_body)
+               |> Jason.decode!()
+    end
+
+    test "max pinned posts", %{conn: conn} do
+      Pleroma.Config.put([:instance, :max_pinned_posts], 1)
+
+      user = insert(:user)
+
+      {:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
+      {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
+
+      id_str_one = Integer.to_string(activity_one.id)
+
+      assert %{"id" => ^id_str_one} =
+               conn
+               |> assign(:user, user)
+               |> post("/api/v1/statuses/#{id_str_one}/pin")
+               |> Map.get(:resp_body)
+               |> Jason.decode!()
+
+      user = User.get_by_ap_id(user.ap_id)
+
+      assert %{"error" => "You have already pinned the maximum number of toots"} =
+               conn
+               |> assign(:user, user)
+               |> post("/api/v1/statuses/#{activity_two.id}/pin")
+               |> Map.get(:resp_body)
+               |> Jason.decode!()
+    end
+  end
 end