forked from AkkomaGang/akkoma
Merge branch 'split-masto-api/polls' into 'develop'
Extract poll actions from `MastodonAPIController` to `PollController` See merge request pleroma/pleroma!1755
This commit is contained in:
commit
f0b4ba1bf9
11 changed files with 454 additions and 419 deletions
|
@ -75,4 +75,16 @@ def assign_account_by_id(%{params: %{"id" => id}} = conn, _) do
|
||||||
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
|
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def try_render(conn, target, params)
|
||||||
|
when is_binary(target) do
|
||||||
|
case render(conn, target, params) do
|
||||||
|
nil -> render_error(conn, :not_implemented, "Can't display this activity")
|
||||||
|
res -> res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def try_render(conn, _, _) do
|
||||||
|
render_error(conn, :not_implemented, "Can't display this activity")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
||||||
|
|
||||||
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
|
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
|
||||||
|
|
||||||
alias Pleroma.Activity
|
|
||||||
alias Pleroma.Bookmark
|
alias Pleroma.Bookmark
|
||||||
alias Pleroma.Config
|
alias Pleroma.Config
|
||||||
alias Pleroma.HTTP
|
alias Pleroma.HTTP
|
||||||
|
@ -19,7 +18,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web
|
alias Pleroma.Web
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.MastodonAPI.AccountView
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
alias Pleroma.Web.MastodonAPI.AppView
|
alias Pleroma.Web.MastodonAPI.AppView
|
||||||
|
@ -117,56 +115,6 @@ def custom_emojis(conn, _params) do
|
||||||
json(conn, mastodon_emoji)
|
json(conn, mastodon_emoji)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
|
||||||
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
|
|
||||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
|
||||||
true <- Visibility.visible_for_user?(activity, user) do
|
|
||||||
conn
|
|
||||||
|> put_view(StatusView)
|
|
||||||
|> try_render("poll.json", %{object: object, for: user})
|
|
||||||
else
|
|
||||||
error when is_nil(error) or error == false ->
|
|
||||||
render_error(conn, :not_found, "Record not found")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_cached_vote_or_vote(user, object, choices) do
|
|
||||||
idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
|
|
||||||
|
|
||||||
{_, res} =
|
|
||||||
Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
|
|
||||||
case CommonAPI.vote(user, object, choices) do
|
|
||||||
{:error, _message} = res -> {:ignore, res}
|
|
||||||
res -> {:commit, res}
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
res
|
|
||||||
end
|
|
||||||
|
|
||||||
def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
|
|
||||||
with %Object{} = object <- Object.get_by_id(id),
|
|
||||||
true <- object.data["type"] == "Question",
|
|
||||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
|
||||||
true <- Visibility.visible_for_user?(activity, user),
|
|
||||||
{:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
|
|
||||||
conn
|
|
||||||
|> put_view(StatusView)
|
|
||||||
|> try_render("poll.json", %{object: object, for: user})
|
|
||||||
else
|
|
||||||
nil ->
|
|
||||||
render_error(conn, :not_found, "Record not found")
|
|
||||||
|
|
||||||
false ->
|
|
||||||
render_error(conn, :not_found, "Record not found")
|
|
||||||
|
|
||||||
{:error, message} ->
|
|
||||||
conn
|
|
||||||
|> put_status(:unprocessable_entity)
|
|
||||||
|> json(%{error: message})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_media(
|
def update_media(
|
||||||
%{assigns: %{user: user}} = conn,
|
%{assigns: %{user: user}} = conn,
|
||||||
%{"id" => id, "description" => description} = _
|
%{"id" => id, "description" => description} = _
|
||||||
|
@ -511,18 +459,6 @@ def password_reset(conn, params) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def try_render(conn, target, params)
|
|
||||||
when is_binary(target) do
|
|
||||||
case render(conn, target, params) do
|
|
||||||
nil -> render_error(conn, :not_implemented, "Can't display this activity")
|
|
||||||
res -> res
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def try_render(conn, _, _) do
|
|
||||||
render_error(conn, :not_implemented, "Can't display this activity")
|
|
||||||
end
|
|
||||||
|
|
||||||
defp present?(nil), do: false
|
defp present?(nil), do: false
|
||||||
defp present?(false), do: false
|
defp present?(false), do: false
|
||||||
defp present?(_), do: true
|
defp present?(_), do: true
|
||||||
|
|
53
lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
Normal file
53
lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# 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.PollController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
import Pleroma.Web.ControllerHelper, only: [try_render: 3, json_response: 3]
|
||||||
|
|
||||||
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
|
@doc "GET /api/v1/polls/:id"
|
||||||
|
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
|
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
|
||||||
|
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
||||||
|
true <- Visibility.visible_for_user?(activity, user) do
|
||||||
|
try_render(conn, "show.json", %{object: object, for: user})
|
||||||
|
else
|
||||||
|
error when is_nil(error) or error == false ->
|
||||||
|
render_error(conn, :not_found, "Record not found")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "POST /api/v1/polls/:id/votes"
|
||||||
|
def vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
|
||||||
|
with %Object{data: %{"type" => "Question"}} = object <- Object.get_by_id(id),
|
||||||
|
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
||||||
|
true <- Visibility.visible_for_user?(activity, user),
|
||||||
|
{:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
|
||||||
|
try_render(conn, "show.json", %{object: object, for: user})
|
||||||
|
else
|
||||||
|
nil -> render_error(conn, :not_found, "Record not found")
|
||||||
|
false -> render_error(conn, :not_found, "Record not found")
|
||||||
|
{:error, message} -> json_response(conn, :unprocessable_entity, %{error: message})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_cached_vote_or_vote(user, object, choices) do
|
||||||
|
idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
|
||||||
|
|
||||||
|
Cachex.fetch!(:idempotency_cache, idempotency_key, fn ->
|
||||||
|
case CommonAPI.vote(user, object, choices) do
|
||||||
|
{:error, _message} = res -> {:ignore, res}
|
||||||
|
res -> {:commit, res}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,7 +5,7 @@
|
||||||
defmodule Pleroma.Web.MastodonAPI.StatusController do
|
defmodule Pleroma.Web.MastodonAPI.StatusController do
|
||||||
use Pleroma.Web, :controller
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
import Pleroma.Web.MastodonAPI.MastodonAPIController, only: [try_render: 3]
|
import Pleroma.Web.ControllerHelper, only: [try_render: 3]
|
||||||
|
|
||||||
require Ecto.Query
|
require Ecto.Query
|
||||||
|
|
||||||
|
|
74
lib/pleroma/web/mastodon_api/views/poll_view.ex
Normal file
74
lib/pleroma/web/mastodon_api/views/poll_view.ex
Normal file
|
@ -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.Web.MastodonAPI.PollView do
|
||||||
|
use Pleroma.Web, :view
|
||||||
|
|
||||||
|
alias Pleroma.HTML
|
||||||
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
|
|
||||||
|
def render("show.json", %{object: object, multiple: multiple, options: options} = params) do
|
||||||
|
{end_time, expired} = end_time_and_expired(object)
|
||||||
|
{options, votes_count} = options_and_votes_count(options)
|
||||||
|
|
||||||
|
%{
|
||||||
|
# Mastodon uses separate ids for polls, but an object can't have
|
||||||
|
# more than one poll embedded so object id is fine
|
||||||
|
id: to_string(object.id),
|
||||||
|
expires_at: end_time,
|
||||||
|
expired: expired,
|
||||||
|
multiple: multiple,
|
||||||
|
votes_count: votes_count,
|
||||||
|
options: options,
|
||||||
|
voted: voted?(params),
|
||||||
|
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def render("show.json", %{object: object} = params) do
|
||||||
|
case object.data do
|
||||||
|
%{"anyOf" => options} when is_list(options) ->
|
||||||
|
render(__MODULE__, "show.json", Map.merge(params, %{multiple: true, options: options}))
|
||||||
|
|
||||||
|
%{"oneOf" => options} when is_list(options) ->
|
||||||
|
render(__MODULE__, "show.json", Map.merge(params, %{multiple: false, options: options}))
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp end_time_and_expired(object) do
|
||||||
|
case object.data["closed"] || object.data["endTime"] do
|
||||||
|
end_time when is_binary(end_time) ->
|
||||||
|
end_time = NaiveDateTime.from_iso8601!(end_time)
|
||||||
|
expired = NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) == :lt
|
||||||
|
|
||||||
|
{Utils.to_masto_date(end_time), expired}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{nil, false}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp options_and_votes_count(options) do
|
||||||
|
Enum.map_reduce(options, 0, fn %{"name" => name} = option, count ->
|
||||||
|
current_count = option["replies"]["totalItems"] || 0
|
||||||
|
|
||||||
|
{%{
|
||||||
|
title: HTML.strip_tags(name),
|
||||||
|
votes_count: current_count
|
||||||
|
}, current_count + count}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp voted?(%{object: object} = opts) do
|
||||||
|
if opts[:for] do
|
||||||
|
existing_votes = Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
|
||||||
|
existing_votes != [] or opts[:for].ap_id == object.data["actor"]
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -18,6 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.CommonAPI.Utils
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
alias Pleroma.Web.MastodonAPI.AccountView
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
|
alias Pleroma.Web.MastodonAPI.PollView
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
alias Pleroma.Web.MediaProxy
|
alias Pleroma.Web.MediaProxy
|
||||||
|
|
||||||
|
@ -277,7 +278,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
||||||
spoiler_text: summary_html,
|
spoiler_text: summary_html,
|
||||||
visibility: get_visibility(object),
|
visibility: get_visibility(object),
|
||||||
media_attachments: attachments,
|
media_attachments: attachments,
|
||||||
poll: render("poll.json", %{object: object, for: opts[:for]}),
|
poll: render(PollView, "show.json", object: object, for: opts[:for]),
|
||||||
mentions: mentions,
|
mentions: mentions,
|
||||||
tags: build_tags(tags),
|
tags: build_tags(tags),
|
||||||
application: %{
|
application: %{
|
||||||
|
@ -389,75 +390,6 @@ def render("listens.json", opts) do
|
||||||
safe_render_many(opts.activities, StatusView, "listen.json", opts)
|
safe_render_many(opts.activities, StatusView, "listen.json", opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("poll.json", %{object: object} = opts) do
|
|
||||||
{multiple, options} =
|
|
||||||
case object.data do
|
|
||||||
%{"anyOf" => options} when is_list(options) -> {true, options}
|
|
||||||
%{"oneOf" => options} when is_list(options) -> {false, options}
|
|
||||||
_ -> {nil, nil}
|
|
||||||
end
|
|
||||||
|
|
||||||
if options do
|
|
||||||
{end_time, expired} =
|
|
||||||
case object.data["closed"] || object.data["endTime"] do
|
|
||||||
end_time when is_binary(end_time) ->
|
|
||||||
end_time =
|
|
||||||
(object.data["closed"] || object.data["endTime"])
|
|
||||||
|> NaiveDateTime.from_iso8601!()
|
|
||||||
|
|
||||||
expired =
|
|
||||||
end_time
|
|
||||||
|> NaiveDateTime.compare(NaiveDateTime.utc_now())
|
|
||||||
|> case do
|
|
||||||
:lt -> true
|
|
||||||
_ -> false
|
|
||||||
end
|
|
||||||
|
|
||||||
end_time = Utils.to_masto_date(end_time)
|
|
||||||
|
|
||||||
{end_time, expired}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
{nil, false}
|
|
||||||
end
|
|
||||||
|
|
||||||
voted =
|
|
||||||
if opts[:for] do
|
|
||||||
existing_votes =
|
|
||||||
Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
|
|
||||||
|
|
||||||
existing_votes != [] or opts[:for].ap_id == object.data["actor"]
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
{options, votes_count} =
|
|
||||||
Enum.map_reduce(options, 0, fn %{"name" => name} = option, count ->
|
|
||||||
current_count = option["replies"]["totalItems"] || 0
|
|
||||||
|
|
||||||
{%{
|
|
||||||
title: HTML.strip_tags(name),
|
|
||||||
votes_count: current_count
|
|
||||||
}, current_count + count}
|
|
||||||
end)
|
|
||||||
|
|
||||||
%{
|
|
||||||
# Mastodon uses separate ids for polls, but an object can't have
|
|
||||||
# more than one poll embedded so object id is fine
|
|
||||||
id: to_string(object.id),
|
|
||||||
expires_at: end_time,
|
|
||||||
expired: expired,
|
|
||||||
multiple: multiple,
|
|
||||||
votes_count: votes_count,
|
|
||||||
options: options,
|
|
||||||
voted: voted,
|
|
||||||
emojis: build_emojis(object.data["emoji"])
|
|
||||||
}
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def render("context.json", %{activity: activity, activities: activities, user: user}) do
|
def render("context.json", %{activity: activity, activities: activities, user: user}) do
|
||||||
%{ancestors: ancestors, descendants: descendants} =
|
%{ancestors: ancestors, descendants: descendants} =
|
||||||
activities
|
activities
|
||||||
|
|
|
@ -403,7 +403,7 @@ defmodule Pleroma.Web.Router do
|
||||||
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
|
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
|
||||||
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
|
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
|
||||||
|
|
||||||
post("/polls/:id/votes", MastodonAPIController, :poll_vote)
|
post("/polls/:id/votes", PollController, :vote)
|
||||||
|
|
||||||
post("/media", MastodonAPIController, :upload)
|
post("/media", MastodonAPIController, :upload)
|
||||||
put("/media/:id", MastodonAPIController, :update_media)
|
put("/media/:id", MastodonAPIController, :update_media)
|
||||||
|
@ -488,7 +488,7 @@ defmodule Pleroma.Web.Router do
|
||||||
get("/statuses/:id", StatusController, :show)
|
get("/statuses/:id", StatusController, :show)
|
||||||
get("/statuses/:id/context", StatusController, :context)
|
get("/statuses/:id/context", StatusController, :context)
|
||||||
|
|
||||||
get("/polls/:id", MastodonAPIController, :get_poll)
|
get("/polls/:id", PollController, :show)
|
||||||
|
|
||||||
get("/accounts/:id/statuses", AccountController, :statuses)
|
get("/accounts/:id/statuses", AccountController, :statuses)
|
||||||
get("/accounts/:id/followers", AccountController, :followers)
|
get("/accounts/:id/followers", AccountController, :followers)
|
||||||
|
|
184
test/web/mastodon_api/controllers/poll_controller_test.exs
Normal file
184
test/web/mastodon_api/controllers/poll_controller_test.exs
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
# 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.PollControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
describe "GET /api/v1/polls/:id" do
|
||||||
|
test "returns poll entity for object id", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Pleroma does",
|
||||||
|
"poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> get("/api/v1/polls/#{object.id}")
|
||||||
|
|
||||||
|
response = json_response(conn, 200)
|
||||||
|
id = to_string(object.id)
|
||||||
|
assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not expose polls for private statuses", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Pleroma does",
|
||||||
|
"poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20},
|
||||||
|
"visibility" => "private"
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, other_user)
|
||||||
|
|> get("/api/v1/polls/#{object.id}")
|
||||||
|
|
||||||
|
assert json_response(conn, 404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /api/v1/polls/:id/votes" do
|
||||||
|
test "votes are added to the poll", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "A very delicious sandwich",
|
||||||
|
"poll" => %{
|
||||||
|
"options" => ["Lettuce", "Grilled Bacon", "Tomato"],
|
||||||
|
"expires_in" => 20,
|
||||||
|
"multiple" => true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, other_user)
|
||||||
|
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
|
||||||
|
|
||||||
|
assert json_response(conn, 200)
|
||||||
|
object = Object.get_by_id(object.id)
|
||||||
|
|
||||||
|
assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
|
||||||
|
total_items == 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "author can't vote", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Am I cute?",
|
||||||
|
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
assert conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
|
||||||
|
|> json_response(422) == %{"error" => "Poll's author can't vote"}
|
||||||
|
|
||||||
|
object = Object.get_by_id(object.id)
|
||||||
|
|
||||||
|
refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not allow multiple choices on a single-choice question", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "The glass is",
|
||||||
|
"poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
assert conn
|
||||||
|
|> assign(:user, other_user)
|
||||||
|
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]})
|
||||||
|
|> json_response(422) == %{"error" => "Too many choices"}
|
||||||
|
|
||||||
|
object = Object.get_by_id(object.id)
|
||||||
|
|
||||||
|
refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
|
||||||
|
total_items == 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not allow choice index to be greater than options count", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Am I cute?",
|
||||||
|
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, other_user)
|
||||||
|
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
|
||||||
|
|
||||||
|
assert json_response(conn, 422) == %{"error" => "Invalid indices"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 404 error when object is not exist", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> post("/api/v1/polls/1/votes", %{"choices" => [0]})
|
||||||
|
|
||||||
|
assert json_response(conn, 404) == %{"error" => "Record not found"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 404 when poll is private and not available for user", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Am I cute?",
|
||||||
|
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20},
|
||||||
|
"visibility" => "private"
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> assign(:user, other_user)
|
||||||
|
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
|
||||||
|
|
||||||
|
assert json_response(conn, 404) == %{"error" => "Record not found"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -417,178 +417,6 @@ test "redirects to the getting-started page when referer is not present", %{conn
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /api/v1/polls/:id" do
|
|
||||||
test "returns poll entity for object id", %{conn: conn} do
|
|
||||||
user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
"status" => "Pleroma does",
|
|
||||||
"poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20}
|
|
||||||
})
|
|
||||||
|
|
||||||
object = Object.normalize(activity)
|
|
||||||
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> assign(:user, user)
|
|
||||||
|> get("/api/v1/polls/#{object.id}")
|
|
||||||
|
|
||||||
response = json_response(conn, 200)
|
|
||||||
id = to_string(object.id)
|
|
||||||
assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not expose polls for private statuses", %{conn: conn} do
|
|
||||||
user = insert(:user)
|
|
||||||
other_user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
"status" => "Pleroma does",
|
|
||||||
"poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20},
|
|
||||||
"visibility" => "private"
|
|
||||||
})
|
|
||||||
|
|
||||||
object = Object.normalize(activity)
|
|
||||||
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> assign(:user, other_user)
|
|
||||||
|> get("/api/v1/polls/#{object.id}")
|
|
||||||
|
|
||||||
assert json_response(conn, 404)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "POST /api/v1/polls/:id/votes" do
|
|
||||||
test "votes are added to the poll", %{conn: conn} do
|
|
||||||
user = insert(:user)
|
|
||||||
other_user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
"status" => "A very delicious sandwich",
|
|
||||||
"poll" => %{
|
|
||||||
"options" => ["Lettuce", "Grilled Bacon", "Tomato"],
|
|
||||||
"expires_in" => 20,
|
|
||||||
"multiple" => true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
object = Object.normalize(activity)
|
|
||||||
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> assign(:user, other_user)
|
|
||||||
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
|
|
||||||
|
|
||||||
assert json_response(conn, 200)
|
|
||||||
object = Object.get_by_id(object.id)
|
|
||||||
|
|
||||||
assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
|
|
||||||
total_items == 1
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "author can't vote", %{conn: conn} do
|
|
||||||
user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
"status" => "Am I cute?",
|
|
||||||
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
|
|
||||||
})
|
|
||||||
|
|
||||||
object = Object.normalize(activity)
|
|
||||||
|
|
||||||
assert conn
|
|
||||||
|> assign(:user, user)
|
|
||||||
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
|
|
||||||
|> json_response(422) == %{"error" => "Poll's author can't vote"}
|
|
||||||
|
|
||||||
object = Object.get_by_id(object.id)
|
|
||||||
|
|
||||||
refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not allow multiple choices on a single-choice question", %{conn: conn} do
|
|
||||||
user = insert(:user)
|
|
||||||
other_user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
"status" => "The glass is",
|
|
||||||
"poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20}
|
|
||||||
})
|
|
||||||
|
|
||||||
object = Object.normalize(activity)
|
|
||||||
|
|
||||||
assert conn
|
|
||||||
|> assign(:user, other_user)
|
|
||||||
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]})
|
|
||||||
|> json_response(422) == %{"error" => "Too many choices"}
|
|
||||||
|
|
||||||
object = Object.get_by_id(object.id)
|
|
||||||
|
|
||||||
refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
|
|
||||||
total_items == 1
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not allow choice index to be greater than options count", %{conn: conn} do
|
|
||||||
user = insert(:user)
|
|
||||||
other_user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
"status" => "Am I cute?",
|
|
||||||
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
|
|
||||||
})
|
|
||||||
|
|
||||||
object = Object.normalize(activity)
|
|
||||||
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> assign(:user, other_user)
|
|
||||||
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
|
|
||||||
|
|
||||||
assert json_response(conn, 422) == %{"error" => "Invalid indices"}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns 404 error when object is not exist", %{conn: conn} do
|
|
||||||
user = insert(:user)
|
|
||||||
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> assign(:user, user)
|
|
||||||
|> post("/api/v1/polls/1/votes", %{"choices" => [0]})
|
|
||||||
|
|
||||||
assert json_response(conn, 404) == %{"error" => "Record not found"}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns 404 when poll is private and not available for user", %{conn: conn} do
|
|
||||||
user = insert(:user)
|
|
||||||
other_user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
"status" => "Am I cute?",
|
|
||||||
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20},
|
|
||||||
"visibility" => "private"
|
|
||||||
})
|
|
||||||
|
|
||||||
object = Object.normalize(activity)
|
|
||||||
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> assign(:user, other_user)
|
|
||||||
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
|
|
||||||
|
|
||||||
assert json_response(conn, 404) == %{"error" => "Record not found"}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "POST /auth/password, with valid parameters" do
|
describe "POST /auth/password, with valid parameters" do
|
||||||
setup %{conn: conn} do
|
setup %{conn: conn} do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
|
126
test/web/mastodon_api/views/poll_view_test.exs
Normal file
126
test/web/mastodon_api/views/poll_view_test.exs
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
# 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.PollViewTest do
|
||||||
|
use Pleroma.DataCase
|
||||||
|
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
alias Pleroma.Web.MastodonAPI.PollView
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
import Tesla.Mock
|
||||||
|
|
||||||
|
setup do
|
||||||
|
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders a poll" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Is Tenshi eating a corndog cute?",
|
||||||
|
"poll" => %{
|
||||||
|
"options" => ["absolutely!", "sure", "yes", "why are you even asking?"],
|
||||||
|
"expires_in" => 20
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
expected = %{
|
||||||
|
emojis: [],
|
||||||
|
expired: false,
|
||||||
|
id: to_string(object.id),
|
||||||
|
multiple: false,
|
||||||
|
options: [
|
||||||
|
%{title: "absolutely!", votes_count: 0},
|
||||||
|
%{title: "sure", votes_count: 0},
|
||||||
|
%{title: "yes", votes_count: 0},
|
||||||
|
%{title: "why are you even asking?", votes_count: 0}
|
||||||
|
],
|
||||||
|
voted: false,
|
||||||
|
votes_count: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result = PollView.render("show.json", %{object: object})
|
||||||
|
expires_at = result.expires_at
|
||||||
|
result = Map.delete(result, :expires_at)
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
expires_at = NaiveDateTime.from_iso8601!(expires_at)
|
||||||
|
assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects if it is multiple choice" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Which Mastodon developer is your favourite?",
|
||||||
|
"poll" => %{
|
||||||
|
"options" => ["Gargron", "Eugen"],
|
||||||
|
"expires_in" => 20,
|
||||||
|
"multiple" => true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
assert %{multiple: true} = PollView.render("show.json", %{object: object})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects emoji" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "What's with the smug face?",
|
||||||
|
"poll" => %{
|
||||||
|
"options" => [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"],
|
||||||
|
"expires_in" => 20
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
assert %{emojis: [%{shortcode: "blank"}]} = PollView.render("show.json", %{object: object})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "detects vote status" do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "Which input devices do you use?",
|
||||||
|
"poll" => %{
|
||||||
|
"options" => ["mouse", "trackball", "trackpoint"],
|
||||||
|
"multiple" => true,
|
||||||
|
"expires_in" => 20
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
object = Object.normalize(activity)
|
||||||
|
|
||||||
|
{:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2])
|
||||||
|
|
||||||
|
result = PollView.render("show.json", %{object: object, for: other_user})
|
||||||
|
|
||||||
|
assert result[:voted] == true
|
||||||
|
assert Enum.at(result[:options], 1)[:votes_count] == 1
|
||||||
|
assert Enum.at(result[:options], 2)[:votes_count] == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not crash on polls with no end date" do
|
||||||
|
object = Object.normalize("https://skippers-bin.com/notes/7x9tmrp97i")
|
||||||
|
result = PollView.render("show.json", %{object: object})
|
||||||
|
|
||||||
|
assert result[:expires_at] == nil
|
||||||
|
assert result[:expired] == false
|
||||||
|
end
|
||||||
|
end
|
|
@ -451,116 +451,6 @@ test "a rich media card with all relevant data renders correctly" do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "poll view" do
|
|
||||||
test "renders a poll" do
|
|
||||||
user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
"status" => "Is Tenshi eating a corndog cute?",
|
|
||||||
"poll" => %{
|
|
||||||
"options" => ["absolutely!", "sure", "yes", "why are you even asking?"],
|
|
||||||
"expires_in" => 20
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
object = Object.normalize(activity)
|
|
||||||
|
|
||||||
expected = %{
|
|
||||||
emojis: [],
|
|
||||||
expired: false,
|
|
||||||
id: to_string(object.id),
|
|
||||||
multiple: false,
|
|
||||||
options: [
|
|
||||||
%{title: "absolutely!", votes_count: 0},
|
|
||||||
%{title: "sure", votes_count: 0},
|
|
||||||
%{title: "yes", votes_count: 0},
|
|
||||||
%{title: "why are you even asking?", votes_count: 0}
|
|
||||||
],
|
|
||||||
voted: false,
|
|
||||||
votes_count: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
result = StatusView.render("poll.json", %{object: object})
|
|
||||||
expires_at = result.expires_at
|
|
||||||
result = Map.delete(result, :expires_at)
|
|
||||||
|
|
||||||
assert result == expected
|
|
||||||
|
|
||||||
expires_at = NaiveDateTime.from_iso8601!(expires_at)
|
|
||||||
assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20
|
|
||||||
end
|
|
||||||
|
|
||||||
test "detects if it is multiple choice" do
|
|
||||||
user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
"status" => "Which Mastodon developer is your favourite?",
|
|
||||||
"poll" => %{
|
|
||||||
"options" => ["Gargron", "Eugen"],
|
|
||||||
"expires_in" => 20,
|
|
||||||
"multiple" => true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
object = Object.normalize(activity)
|
|
||||||
|
|
||||||
assert %{multiple: true} = StatusView.render("poll.json", %{object: object})
|
|
||||||
end
|
|
||||||
|
|
||||||
test "detects emoji" do
|
|
||||||
user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
"status" => "What's with the smug face?",
|
|
||||||
"poll" => %{
|
|
||||||
"options" => [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"],
|
|
||||||
"expires_in" => 20
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
object = Object.normalize(activity)
|
|
||||||
|
|
||||||
assert %{emojis: [%{shortcode: "blank"}]} =
|
|
||||||
StatusView.render("poll.json", %{object: object})
|
|
||||||
end
|
|
||||||
|
|
||||||
test "detects vote status" do
|
|
||||||
user = insert(:user)
|
|
||||||
other_user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
"status" => "Which input devices do you use?",
|
|
||||||
"poll" => %{
|
|
||||||
"options" => ["mouse", "trackball", "trackpoint"],
|
|
||||||
"multiple" => true,
|
|
||||||
"expires_in" => 20
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
object = Object.normalize(activity)
|
|
||||||
|
|
||||||
{:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2])
|
|
||||||
|
|
||||||
result = StatusView.render("poll.json", %{object: object, for: other_user})
|
|
||||||
|
|
||||||
assert result[:voted] == true
|
|
||||||
assert Enum.at(result[:options], 1)[:votes_count] == 1
|
|
||||||
assert Enum.at(result[:options], 2)[:votes_count] == 1
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not crash on polls with no end date" do
|
|
||||||
object = Object.normalize("https://skippers-bin.com/notes/7x9tmrp97i")
|
|
||||||
result = StatusView.render("poll.json", %{object: object})
|
|
||||||
|
|
||||||
assert result[:expires_at] == nil
|
|
||||||
assert result[:expired] == false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "embeds a relationship in the account" do
|
test "embeds a relationship in the account" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
other_user = insert(:user)
|
other_user = insert(:user)
|
||||||
|
|
Loading…
Reference in a new issue