[#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: `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 | - 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 | - 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 | ### Changed | ||||
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7) | - **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) | get("/push/subscription", SubscriptionController, :get) | ||||
put("/push/subscription", SubscriptionController, :update) | put("/push/subscription", SubscriptionController, :update) | ||||
delete("/push/subscription", SubscriptionController, :delete) | delete("/push/subscription", SubscriptionController, :delete) | ||||
get("/markers", MarkerController, :index) | |||||
post("/markers", MarkerController, :upsert) | |||||
end | end | ||||
scope "/api/web", Pleroma.Web do | 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 | end | ||||
def marker_factory do | |||||
%Pleroma.Marker{ | |||||
user: build(:user), | |||||
timeline: "notifications", | |||||
lock_version: 0, | |||||
last_read_id: "1" | |||||
} | |||||
end | |||||
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 |