Implement announcement read relationships
This commit is contained in:
parent
c867d23250
commit
5169ad8f14
8 changed files with 316 additions and 3 deletions
|
@ -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 @@ defmodule Pleroma.Announcement 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
|
||||||
|
|
55
lib/pleroma/announcement_read_relationship.ex
Normal file
55
lib/pleroma/announcement_read_relationship.ex
Normal 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
|
|
@ -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
|
|
@ -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
|
15
lib/pleroma/web/mastodon_api/views/announcement_view.ex
Normal file
15
lib/pleroma/web/mastodon_api/views/announcement_view.ex
Normal 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
|
|
@ -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
|
||||||
|
|
40
test/pleroma/announcement_read_relationship_test.exs
Normal file
40
test/pleroma/announcement_read_relationship_test.exs
Normal 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
|
|
@ -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
|
Loading…
Reference in a new issue