[#1275] Markers /api/v1/markers See merge request pleroma/pleroma!1852merge-requests/1875/head
@@ -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) | |||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 | |||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |