Implement announcement read relationships

This commit is contained in:
Tusooa Zhu 2022-03-08 12:07:21 -05:00
parent c867d23250
commit 5169ad8f14
No known key found for this signature in database
GPG key ID: 7B467EDE43A08224
8 changed files with 316 additions and 3 deletions

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Announcement do
import Ecto.Changeset, only: [cast: 3, validate_required: 2] import Ecto.Changeset, only: [cast: 3, validate_required: 2]
alias Pleroma.AnnouncementReadRelationship
alias Pleroma.Repo alias Pleroma.Repo
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@ -49,15 +50,20 @@ def delete_by_id(id) do
end end
end end
def read_by?(_announcement, _user) do def read_by?(announcement, user) do
false AnnouncementReadRelationship.exists?(user, announcement)
end
def mark_read_by(announcement, user) do
AnnouncementReadRelationship.mark_read(user, announcement)
end end
def render_json(announcement, opts \\ []) do def render_json(announcement, opts \\ []) do
extra_params = extra_params =
case Keyword.fetch(opts, :for) do case Keyword.fetch(opts, :for) do
{:ok, user} -> {:ok, user} when not is_nil(user) ->
%{read: read_by?(announcement, user)} %{read: read_by?(announcement, user)}
_ -> _ ->
%{} %{}
end end

View file

@ -0,0 +1,55 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.AnnouncementReadRelationship do
use Ecto.Schema
import Ecto.Changeset
alias FlakeId.Ecto.CompatType
alias Pleroma.Announcement
alias Pleroma.Repo
alias Pleroma.User
@type t :: %__MODULE__{}
schema "announcement_read_relationships" do
belongs_to(:user, User, type: CompatType)
belongs_to(:announcement, Announcement, type: CompatType)
timestamps(updated_at: false)
end
def mark_read(user, announcement) do
%__MODULE__{}
|> cast(%{user_id: user.id, announcement_id: announcement.id}, [:user_id, :announcement_id])
|> validate_required([:user_id, :announcement_id])
|> foreign_key_constraint(:user_id)
|> foreign_key_constraint(:announcement_id)
|> unique_constraint([:user_id, :announcement_id])
|> Repo.insert()
end
def mark_unread(user, announcement) do
with relationship <- get(user, announcement),
{:exists, true} <- {:exists, not is_nil(relationship)},
{:ok, _} <- Repo.delete(relationship) do
:ok
else
{:exists, false} ->
:ok
_ ->
:error
end
end
def get(user, announcement) do
Repo.get_by(__MODULE__, user_id: user.id, announcement_id: announcement.id)
end
def exists?(user, announcement) do
not is_nil(get(user, announcement))
end
end

View file

@ -0,0 +1,77 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.AnnouncementOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Announcement
alias Pleroma.Web.ApiSpec.Schemas.ApiError
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["Announcement"],
summary: "Retrieve a list of announcements",
operationId: "MastodonAPI.AnnouncementController.index",
responses: %{
200 => Operation.response("Response", "application/json", list_of_announcements()),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def show_operation do
%Operation{
tags: ["Announcement"],
summary: "Display one announcement",
operationId: "MastodonAPI.AnnouncementController.show",
parameters: [
Operation.parameter(
:id,
:path,
:string,
"announcement id"
)
],
responses: %{
200 => Operation.response("Response", "application/json", Announcement),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def mark_read_operation do
%Operation{
tags: ["Announcement"],
summary: "Mark one announcement as read",
operationId: "MastodonAPI.AnnouncementController.mark_read",
security: [%{"oAuth" => ["write:accounts"]}],
parameters: [
Operation.parameter(
:id,
:path,
:string,
"announcement id"
)
],
responses: %{
200 => Operation.response("Response", "application/json", Announcement),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def list_of_announcements do
%Schema{
type: :array,
items: Announcement
}
end
end

View file

@ -0,0 +1,61 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AnnouncementController do
use Pleroma.Web, :controller
# import Pleroma.Web.ControllerHelper,
# only: [
# json_response: 3
# ]
alias Pleroma.Announcement
alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
# MastodonAPI specs do not have oauth requirements for showing
# announcements, but we have "private instance" options. When that
# is set, require read:accounts scope, symmetric to write:accounts
# for `mark_read`.
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
when action in [:show, :index]
)
# Same as in MastodonAPI specs
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["write:accounts"]}
when action in [:mark_read]
)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AnnouncementOperation
@doc "GET /api/v1/announcements"
def index(%{assigns: %{user: user}} = conn, _params) do
render(conn, "index.json", announcements: all_visible(), user: user)
end
def index(conn, _params) do
render(conn, "index.json", announcements: all_visible(), user: nil)
end
defp all_visible do
Announcement.list_all()
end
@doc "POST /api/v1/announcements/:id/dismiss"
def mark_read(_conn, _params) do
{:error, :not_found}
end
@doc "POST /api/v1/announcements/:id"
def show(_conn, _params) do
{:error, :not_found}
end
end

View file

@ -0,0 +1,15 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AnnouncementView do
use Pleroma.Web, :view
def render("index.json", %{announcements: announcements, user: user}) do
render_many(announcements, __MODULE__, "show.json", user: user)
end
def render("show.json", %{announcement: announcement, user: user}) do
Pleroma.Announcement.render_json(announcement, for: user)
end
end

View file

@ -580,6 +580,8 @@ defmodule Pleroma.Web.Router do
get("/timelines/home", TimelineController, :home) get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct) get("/timelines/direct", TimelineController, :direct)
get("/timelines/list/:list_id", TimelineController, :list) get("/timelines/list/:list_id", TimelineController, :list)
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
end end
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do
@ -624,6 +626,9 @@ defmodule Pleroma.Web.Router do
get("/polls/:id", PollController, :show) get("/polls/:id", PollController, :show)
get("/directory", DirectoryController, :index) get("/directory", DirectoryController, :index)
get("/announcements", AnnouncementController, :index)
get("/announcements/:id", AnnouncementController, :show)
end end
scope "/api/v2", Pleroma.Web.MastodonAPI do scope "/api/v2", Pleroma.Web.MastodonAPI do

View file

@ -0,0 +1,40 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.AnnouncementReadRelationshipTest do
alias Pleroma.AnnouncementReadRelationship
use Pleroma.DataCase, async: true
import Pleroma.Factory
setup do
{:ok, user: insert(:user), announcement: insert(:announcement)}
end
describe "mark_read/2" do
test "should insert relationship", %{user: user, announcement: announcement} do
{:ok, _} = AnnouncementReadRelationship.mark_read(user, announcement)
assert AnnouncementReadRelationship.exists?(user, announcement)
end
end
describe "mark_unread/2" do
test "should delete relationship", %{user: user, announcement: announcement} do
{:ok, _} = AnnouncementReadRelationship.mark_read(user, announcement)
assert :ok = AnnouncementReadRelationship.mark_unread(user, announcement)
refute AnnouncementReadRelationship.exists?(user, announcement)
end
test "should not fail if relationship does not exist", %{
user: user,
announcement: announcement
} do
assert :ok = AnnouncementReadRelationship.mark_unread(user, announcement)
refute AnnouncementReadRelationship.exists?(user, announcement)
end
end
end

View file

@ -0,0 +1,54 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
alias Pleroma.AnnouncementReadRelationship
describe "GET /api/v1/announcements" do
test "it lists all announcements" do
%{id: id} = insert(:announcement)
response =
build_conn()
|> get("/api/v1/announcements")
|> json_response_and_validate_schema(:ok)
assert [%{"id" => ^id}] = response
refute Map.has_key?(Enum.at(response, 0), "read")
end
test "when authenticated, also expose read property" do
%{id: id} = insert(:announcement)
%{conn: conn} = oauth_access(["read:accounts"])
response =
conn
|> get("/api/v1/announcements")
|> json_response_and_validate_schema(:ok)
assert [%{"id" => ^id, "read" => false}] = response
end
test "when authenticated and announcement is read by user" do
%{id: id} = announcement = insert(:announcement)
user = insert(:user)
AnnouncementReadRelationship.mark_read(user, announcement)
%{conn: conn} = oauth_access(["read:accounts"], user: user)
response =
conn
|> get("/api/v1/announcements")
|> json_response_and_validate_schema(:ok)
assert [%{"id" => ^id, "read" => true}] = response
end
end
end