logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma
commit: ce750dbab5de905af1e0ba6ba6a5a08b41600ac1
parent: 2ebe8c416a72b512feaba87040982da5bcf865cf
Author: kaniini <ariadne@dereferenced.org>
Date:   Mon, 21 Oct 2019 09:26:17 +0000

Merge branch 'feature/masto_api_markers' into 'develop'

[#1275] Markers /api/v1/markers

See merge request pleroma/pleroma!1852

Diffstat:

MCHANGELOG.md1+
Alib/pleroma/marker.ex74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/mastodon_api/controllers/marker_controller.ex32++++++++++++++++++++++++++++++++
Alib/pleroma/web/mastodon_api/views/marker_view.ex17+++++++++++++++++
Mlib/pleroma/web/router.ex3+++
Apriv/repo/migrations/20191014181019_create_markers.exs15+++++++++++++++
Atest/marker_test.exs51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/support/factory.ex9+++++++++
Atest/web/mastodon_api/controllers/marker_controller_test.exs124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/web/mastodon_api/views/marker_view_test.exs27+++++++++++++++++++++++++++
10 files changed, 353 insertions(+), 0 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -51,6 +51,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Admin API: `POST/DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST/DELETE /api/pleroma/admin/users/permission_group/:permission_group` (both accept `nicknames` array), `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body). - Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays - Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read +- Mastodon API: Add `/api/v1/markers` for managing timeline read markers ### Changed - **Breaking:** Elixir >=1.8 is now required (was >= 1.7) diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex @@ -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 @@ -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 @@ -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 diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex @@ -405,6 +405,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 @@ -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 @@ -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 diff --git a/test/support/factory.ex b/test/support/factory.ex @@ -377,4 +377,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 @@ -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 @@ -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