add Markers /api/v1/markers
authorMaksim Pechnikov <parallel588@gmail.com>
Thu, 17 Oct 2019 12:26:59 +0000 (15:26 +0300)
committerMaksim Pechnikov <parallel588@gmail.com>
Thu, 17 Oct 2019 12:26:59 +0000 (15:26 +0300)
lib/pleroma/marker.ex [new file with mode: 0644]
lib/pleroma/web/mastodon_api/controllers/marker_controller.ex [new file with mode: 0644]
lib/pleroma/web/mastodon_api/views/marker_view.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
priv/repo/migrations/20191014181019_create_markers.exs [new file with mode: 0644]
test/marker_test.exs [new file with mode: 0644]
test/support/factory.ex
test/web/mastodon_api/controllers/marker_controller_test.exs [new file with mode: 0644]
test/web/mastodon_api/views/marker_view_test.exs [new file with mode: 0644]

diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex
new file mode 100644 (file)
index 0000000..7f87c86
--- /dev/null
@@ -0,0 +1,74 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Marker do
+  use Ecto.Schema
+
+  import Ecto.Changeset
+  import Ecto.Query
+
+  alias Ecto.Multi
+  alias Pleroma.Repo
+  alias Pleroma.User
+
+  @timelines ["notifications"]
+
+  schema "markers" do
+    field(:last_read_id, :string, default: "")
+    field(:timeline, :string, default: "")
+    field(:lock_version, :integer, default: 0)
+
+    belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+    timestamps()
+  end
+
+  def get_markers(user, timelines \\ []) do
+    Repo.all(get_query(user, timelines))
+  end
+
+  def upsert(%User{} = user, attrs) do
+    attrs
+    |> Map.take(@timelines)
+    |> Enum.reduce(Multi.new(), fn {timeline, timeline_attrs}, multi ->
+      marker =
+        user
+        |> get_marker(timeline)
+        |> changeset(timeline_attrs)
+
+      Multi.insert(multi, timeline, marker,
+        returning: true,
+        on_conflict: {:replace, [:last_read_id]},
+        conflict_target: [:user_id, :timeline]
+      )
+    end)
+    |> Repo.transaction()
+  end
+
+  defp get_marker(user, timeline) do
+    case Repo.find_resource(get_query(user, timeline)) do
+      {:ok, marker} -> %__MODULE__{marker | user: user}
+      _ -> %__MODULE__{timeline: timeline, user_id: user.id}
+    end
+  end
+
+  @doc false
+  defp changeset(marker, attrs) do
+    marker
+    |> cast(attrs, [:last_read_id])
+    |> validate_required([:user_id, :timeline, :last_read_id])
+    |> validate_inclusion(:timeline, @timelines)
+  end
+
+  defp by_timeline(query, timeline) do
+    from(m in query, where: m.timeline in ^List.wrap(timeline))
+  end
+
+  defp by_user_id(query, id), do: from(m in query, where: m.user_id == ^id)
+
+  defp get_query(user, timelines) do
+    __MODULE__
+    |> by_user_id(user.id)
+    |> by_timeline(timelines)
+  end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
new file mode 100644 (file)
index 0000000..ce02562
--- /dev/null
@@ -0,0 +1,32 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MarkerController do
+  use Pleroma.Web, :controller
+  alias Pleroma.Plugs.OAuthScopesPlug
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:statuses"]}
+    when action == :index
+  )
+
+  plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert)
+  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+  action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+  # GET /api/v1/markers
+  def index(%{assigns: %{user: user}} = conn, params) do
+    markers = Pleroma.Marker.get_markers(user, params["timeline"])
+    render(conn, "markers.json", %{markers: markers})
+  end
+
+  # POST /api/v1/markers
+  def upsert(%{assigns: %{user: user}} = conn, params) do
+    with {:ok, result} <- Pleroma.Marker.upsert(user, params),
+         markers <- Map.values(result) do
+      render(conn, "markers.json", %{markers: markers})
+    end
+  end
+end
diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex
new file mode 100644 (file)
index 0000000..38fbeed
--- /dev/null
@@ -0,0 +1,17 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MarkerView do
+  use Pleroma.Web, :view
+
+  def render("markers.json", %{markers: markers}) do
+    Enum.reduce(markers, %{}, fn m, acc ->
+      Map.put_new(acc, m.timeline, %{
+        last_read_id: m.last_read_id,
+        version: m.lock_version,
+        updated_at: NaiveDateTime.to_iso8601(m.updated_at)
+      })
+    end)
+  end
+end
index ae799b8ac36650112f23e7756d0cf475754557a0..45684284cd7f8b0a0cf9be06a1eaa7ff8c6921b9 100644 (file)
@@ -394,6 +394,9 @@ defmodule Pleroma.Web.Router do
     get("/push/subscription", SubscriptionController, :get)
     put("/push/subscription", SubscriptionController, :update)
     delete("/push/subscription", SubscriptionController, :delete)
+
+    get("/markers", MarkerController, :index)
+    post("/markers", MarkerController, :upsert)
   end
 
   scope "/api/web", Pleroma.Web do
diff --git a/priv/repo/migrations/20191014181019_create_markers.exs b/priv/repo/migrations/20191014181019_create_markers.exs
new file mode 100644 (file)
index 0000000..c717831
--- /dev/null
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.CreateMarkers do
+  use Ecto.Migration
+
+  def change do
+    create_if_not_exists table(:markers) do
+      add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
+      add(:timeline, :string, default: "", null: false)
+      add(:last_read_id, :string, default: "", null: false)
+      add(:lock_version, :integer, default: 0, null: false)
+      timestamps()
+    end
+
+    create_if_not_exists(unique_index(:markers, [:user_id, :timeline]))
+  end
+end
diff --git a/test/marker_test.exs b/test/marker_test.exs
new file mode 100644 (file)
index 0000000..04bd67f
--- /dev/null
@@ -0,0 +1,51 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MarkerTest do
+  use Pleroma.DataCase
+  alias Pleroma.Marker
+
+  import Pleroma.Factory
+
+  describe "get_markers/2" do
+    test "returns user markers" do
+      user = insert(:user)
+      marker = insert(:marker, user: user)
+      insert(:marker, timeline: "home", user: user)
+      assert Marker.get_markers(user, ["notifications"]) == [refresh_record(marker)]
+    end
+  end
+
+  describe "upsert/2" do
+    test "creates a marker" do
+      user = insert(:user)
+
+      {:ok, %{"notifications" => %Marker{} = marker}} =
+        Marker.upsert(
+          user,
+          %{"notifications" => %{"last_read_id" => "34"}}
+        )
+
+      assert marker.timeline == "notifications"
+      assert marker.last_read_id == "34"
+      assert marker.lock_version == 0
+    end
+
+    test "updates exist marker" do
+      user = insert(:user)
+      marker = insert(:marker, user: user, last_read_id: "8909")
+
+      {:ok, %{"notifications" => %Marker{}}} =
+        Marker.upsert(
+          user,
+          %{"notifications" => %{"last_read_id" => "9909"}}
+        )
+
+      marker = refresh_record(marker)
+      assert marker.timeline == "notifications"
+      assert marker.last_read_id == "9909"
+      assert marker.lock_version == 0
+    end
+  end
+end
index b180844cd90acc7fde68ee3d95543bc89c2a1529..4537c458b0efe46b11b4c745a0183c1a827d1a50 100644 (file)
@@ -397,4 +397,13 @@ defmodule Pleroma.Factory do
         )
     }
   end
+
+  def marker_factory do
+    %Pleroma.Marker{
+      user: build(:user),
+      timeline: "notifications",
+      lock_version: 0,
+      last_read_id: "1"
+    }
+  end
 end
diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs
new file mode 100644 (file)
index 0000000..1fcad87
--- /dev/null
@@ -0,0 +1,124 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
+  use Pleroma.Web.ConnCase
+
+  import Pleroma.Factory
+
+  describe "GET /api/v1/markers" do
+    test "gets markers with correct scopes", %{conn: conn} do
+      user = insert(:user)
+      token = insert(:oauth_token, user: user, scopes: ["read:statuses"])
+
+      {:ok, %{"notifications" => marker}} =
+        Pleroma.Marker.upsert(
+          user,
+          %{"notifications" => %{"last_read_id" => "69420"}}
+        )
+
+      response =
+        conn
+        |> assign(:user, user)
+        |> assign(:token, token)
+        |> get("/api/v1/markers", %{timeline: ["notifications"]})
+        |> json_response(200)
+
+      assert response == %{
+               "notifications" => %{
+                 "last_read_id" => "69420",
+                 "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
+                 "version" => 0
+               }
+             }
+    end
+
+    test "gets markers with missed scopes", %{conn: conn} do
+      user = insert(:user)
+      token = insert(:oauth_token, user: user, scopes: [])
+
+      Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => "69420"}})
+
+      response =
+        conn
+        |> assign(:user, user)
+        |> assign(:token, token)
+        |> get("/api/v1/markers", %{timeline: ["notifications"]})
+        |> json_response(403)
+
+      assert response == %{"error" => "Insufficient permissions: read:statuses."}
+    end
+  end
+
+  describe "POST /api/v1/markers" do
+    test "creates a marker with correct scopes", %{conn: conn} do
+      user = insert(:user)
+      token = insert(:oauth_token, user: user, scopes: ["write:statuses"])
+
+      response =
+        conn
+        |> assign(:user, user)
+        |> assign(:token, token)
+        |> post("/api/v1/markers", %{
+          home: %{last_read_id: "777"},
+          notifications: %{"last_read_id" => "69420"}
+        })
+        |> json_response(200)
+
+      assert %{
+               "notifications" => %{
+                 "last_read_id" => "69420",
+                 "updated_at" => _,
+                 "version" => 0
+               }
+             } = response
+    end
+
+    test "updates exist marker", %{conn: conn} do
+      user = insert(:user)
+      token = insert(:oauth_token, user: user, scopes: ["write:statuses"])
+
+      {:ok, %{"notifications" => marker}} =
+        Pleroma.Marker.upsert(
+          user,
+          %{"notifications" => %{"last_read_id" => "69477"}}
+        )
+
+      response =
+        conn
+        |> assign(:user, user)
+        |> assign(:token, token)
+        |> post("/api/v1/markers", %{
+          home: %{last_read_id: "777"},
+          notifications: %{"last_read_id" => "69888"}
+        })
+        |> json_response(200)
+
+      assert response == %{
+               "notifications" => %{
+                 "last_read_id" => "69888",
+                 "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
+                 "version" => 0
+               }
+             }
+    end
+
+    test "creates a marker with missed scopes", %{conn: conn} do
+      user = insert(:user)
+      token = insert(:oauth_token, user: user, scopes: [])
+
+      response =
+        conn
+        |> assign(:user, user)
+        |> assign(:token, token)
+        |> post("/api/v1/markers", %{
+          home: %{last_read_id: "777"},
+          notifications: %{"last_read_id" => "69420"}
+        })
+        |> json_response(403)
+
+      assert response == %{"error" => "Insufficient permissions: write:statuses."}
+    end
+  end
+end
diff --git a/test/web/mastodon_api/views/marker_view_test.exs b/test/web/mastodon_api/views/marker_view_test.exs
new file mode 100644 (file)
index 0000000..8a5c89d
--- /dev/null
@@ -0,0 +1,27 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do
+  use Pleroma.DataCase
+  alias Pleroma.Web.MastodonAPI.MarkerView
+  import Pleroma.Factory
+
+  test "returns markers" do
+    marker1 = insert(:marker, timeline: "notifications", last_read_id: "17")
+    marker2 = insert(:marker, timeline: "home", last_read_id: "42")
+
+    assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{
+             "home" => %{
+               last_read_id: "42",
+               updated_at: NaiveDateTime.to_iso8601(marker2.updated_at),
+               version: 0
+             },
+             "notifications" => %{
+               last_read_id: "17",
+               updated_at: NaiveDateTime.to_iso8601(marker1.updated_at),
+               version: 0
+             }
+           }
+  end
+end