Merge branch 'develop' into issue/1276-2

This commit is contained in:
Maksim Pechnikov 2020-05-06 05:55:39 +03:00
commit fb38b7339c
40 changed files with 751 additions and 225 deletions

View file

@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint. - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
- Mastodon API: Add support for filtering replies in public and home timelines - Mastodon API: Add support for filtering replies in public and home timelines
- Admin API: endpoints for create/update/delete OAuth Apps. - Admin API: endpoints for create/update/delete OAuth Apps.
- Admin API: endpoint for status view.
</details> </details>
### Fixed ### Fixed

View file

@ -653,6 +653,8 @@
profiles: %{local: false, remote: false}, profiles: %{local: false, remote: false},
activities: %{local: false, remote: false} activities: %{local: false, remote: false}
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

View file

@ -2247,6 +2247,7 @@
children: [ children: [
%{ %{
key: :active, key: :active,
label: "Enabled",
type: :boolean, type: :boolean,
description: "Globally enable or disable digest emails" description: "Globally enable or disable digest emails"
}, },
@ -3194,5 +3195,19 @@
] ]
} }
] ]
},
%{
group: :pleroma,
key: Pleroma.Web.ApiSpec.CastAndValidate,
type: :group,
children: [
%{
key: :strict,
type: :boolean,
description:
"Enables strict input validation (useful in development, not recommended in production)",
suggestions: [false]
}
]
} }
] ]

View file

@ -52,6 +52,8 @@
hostname: "localhost", hostname: "localhost",
pool_size: 10 pool_size: 10
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
if File.exists?("./config/dev.secret.exs") do if File.exists?("./config/dev.secret.exs") do
import_config "dev.secret.exs" import_config "dev.secret.exs"
else else

View file

@ -96,6 +96,8 @@
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
if File.exists?("./config/test.secret.exs") do if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs" import_config "test.secret.exs"
else else

View file

@ -755,6 +755,17 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 400 Bad Request `"Invalid parameters"` when `status` is missing - 400 Bad Request `"Invalid parameters"` when `status` is missing
- On success: `204`, empty response - On success: `204`, empty response
## `GET /api/pleroma/admin/statuses/:id`
### Show status by id
- Params:
- `id`: required, status id
- Response:
- On failure:
- 404 Not Found `"Not Found"`
- On success: JSON, Mastodon Status entity
## `PUT /api/pleroma/admin/statuses/:id` ## `PUT /api/pleroma/admin/statuses/:id`
### Change the scope of an individual reported status ### Change the scope of an individual reported status

View file

@ -924,4 +924,8 @@ Restrict access for unauthenticated users to timelines (public and federate), us
* `remote` * `remote`
* `activities` - statuses * `activities` - statuses
* `local` * `local`
* `remote` * `remote`
## Pleroma.Web.ApiSpec.CastAndValidate
* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`.

View file

@ -113,7 +113,6 @@ defmodule Pleroma.User do
field(:is_admin, :boolean, default: false) field(:is_admin, :boolean, default: false)
field(:show_role, :boolean, default: true) field(:show_role, :boolean, default: true)
field(:settings, :map, default: nil) field(:settings, :map, default: nil)
field(:magic_key, :string, default: nil)
field(:uri, Types.Uri, default: nil) field(:uri, Types.Uri, default: nil)
field(:hide_followers_count, :boolean, default: false) field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false) field(:hide_follows_count, :boolean, default: false)
@ -387,7 +386,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
:banner, :banner,
:locked, :locked,
:last_refreshed_at, :last_refreshed_at,
:magic_key,
:uri, :uri,
:follower_address, :follower_address,
:following_address, :following_address,

View file

@ -1530,21 +1530,34 @@ def fetch_follow_information_for_user(user) do
defp normalize_counter(counter) when is_integer(counter), do: counter defp normalize_counter(counter) when is_integer(counter), do: counter
defp normalize_counter(_), do: 0 defp normalize_counter(_), do: 0
defp maybe_update_follow_information(data) do def maybe_update_follow_information(user_data) do
with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])}, with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
{:ok, info} <- fetch_follow_information_for_user(data) do {_, true} <- {:user_type_check, user_data[:type] in ["Person", "Service"]},
info = Map.merge(data[:info] || %{}, info) {_, true} <-
Map.put(data, :info, info) {:collections_available,
!!(user_data[:following_address] && user_data[:follower_address])},
{:ok, info} <-
fetch_follow_information_for_user(user_data) do
info = Map.merge(user_data[:info] || %{}, info)
user_data
|> Map.put(:info, info)
else else
{:user_type_check, false} ->
user_data
{:collections_available, false} ->
user_data
{:enabled, false} -> {:enabled, false} ->
data user_data
e -> e ->
Logger.error( Logger.error(
"Follower/Following counter update for #{data.ap_id} failed.\n" <> inspect(e) "Follower/Following counter update for #{user_data.ap_id} failed.\n" <> inspect(e)
) )
data user_data
end end
end end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
@ -19,8 +20,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
field(:object, Types.ObjectID) field(:object, Types.ObjectID)
field(:actor, Types.ObjectID) field(:actor, Types.ObjectID)
field(:context, :string) field(:context, :string)
field(:to, {:array, :string}) field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}) field(:cc, {:array, :string}, default: [])
end end
def cast_and_validate(data) do def cast_and_validate(data) do
@ -31,7 +32,48 @@ def cast_and_validate(data) do
def cast_data(data) do def cast_data(data) do
%__MODULE__{} %__MODULE__{}
|> cast(data, [:id, :type, :object, :actor, :context, :to, :cc]) |> changeset(data)
end
def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields))
|> fix_after_cast()
end
def fix_after_cast(cng) do
cng
|> fix_recipients()
|> fix_context()
end
def fix_context(cng) do
object = get_field(cng, :object)
with nil <- get_field(cng, :context),
%Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do
cng
|> put_change(:context, context)
else
_ ->
cng
end
end
def fix_recipients(cng) do
to = get_field(cng, :to)
cc = get_field(cng, :cc)
object = get_field(cng, :object)
with {[], []} <- {to, cc},
%Object{data: %{"actor" => actor}} <- Object.get_cached_by_ap_id(object),
{:ok, actor} <- Types.ObjectID.cast(actor) do
cng
|> put_change(:to, [actor])
else
_ ->
cng
end
end end
def validate_data(data_cng) do def validate_data(data_cng) do

View file

@ -4,20 +4,33 @@
defmodule Pleroma.Web.ActivityPub.Pipeline do defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.SideEffects alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
@spec common_pipeline(map(), keyword()) :: {:ok, Activity.t(), keyword()} | {:error, any()} @spec common_pipeline(map(), keyword()) ::
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do
{:ok, value} ->
value
{:error, e} ->
{:error, e}
end
end
def do_common_pipeline(object, meta) do
with {_, {:ok, validated_object, meta}} <- with {_, {:ok, validated_object, meta}} <-
{:validate_object, ObjectValidator.validate(object, meta)}, {:validate_object, ObjectValidator.validate(object, meta)},
{_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)}, {_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)},
{_, {:ok, %Activity{} = activity, meta}} <- {_, {:ok, activity, meta}} <-
{:persist_object, ActivityPub.persist(mrfd_object, meta)}, {:persist_object, ActivityPub.persist(mrfd_object, meta)},
{_, {:ok, %Activity{} = activity, meta}} <- {_, {:ok, activity, meta}} <-
{:execute_side_effects, SideEffects.handle(activity, meta)}, {:execute_side_effects, SideEffects.handle(activity, meta)},
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do {_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta} {:ok, activity, meta}
@ -27,7 +40,9 @@ def common_pipeline(object, meta) do
end end
end end
defp maybe_federate(activity, meta) do defp maybe_federate(%Object{}, _), do: {:ok, :not_federated}
defp maybe_federate(%Activity{} = activity, meta) do
with {:ok, local} <- Keyword.fetch(meta, :local) do with {:ok, local} <- Keyword.fetch(meta, :local) do
if local do if local do
Federator.publish(activity) Federator.publish(activity)

View file

@ -15,17 +15,12 @@ def handle(object, meta \\ [])
# - Add like to object # - Add like to object
# - Set up notification # - Set up notification
def handle(%{data: %{"type" => "Like"}} = object, meta) do def handle(%{data: %{"type" => "Like"}} = object, meta) do
{:ok, result} = liked_object = Object.get_by_ap_id(object.data["object"])
Pleroma.Repo.transaction(fn -> Utils.add_like_to_object(object, liked_object)
liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object)
Notification.create_notifications(object) Notification.create_notifications(object)
{:ok, object, meta} {:ok, object, meta}
end)
result
end end
# Nothing to do # Nothing to do

View file

@ -15,7 +15,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
@ -658,16 +657,9 @@ def handle_incoming(
end end
def handle_incoming(%{"type" => "Like"} = data, _options) do def handle_incoming(%{"type" => "Like"} = data, _options) do
with {_, {:ok, cast_data_sym}} <- with :ok <- ObjectValidator.fetch_actor_and_object(data),
{:casting_data, {:ok, activity, _meta} <-
data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)}, Pipeline.common_pipeline(data, local: false) do
cast_data = ObjectValidator.stringify_keys(Map.from_struct(cast_data_sym)),
:ok <- ObjectValidator.fetch_actor_and_object(cast_data),
{_, {:ok, cast_data}} <- {:ensure_context_presence, ensure_context_presence(cast_data)},
{_, {:ok, cast_data}} <-
{:ensure_recipients_presence, ensure_recipients_presence(cast_data)},
{_, {:ok, activity, _meta}} <-
{:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
{:ok, activity} {:ok, activity}
else else
e -> {:error, e} e -> {:error, e}
@ -1296,45 +1288,4 @@ def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
def maybe_fix_user_url(data), do: data def maybe_fix_user_url(data), do: data
def maybe_fix_user_object(data), do: maybe_fix_user_url(data) def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
defp ensure_context_presence(%{"context" => context} = data) when is_binary(context),
do: {:ok, data}
defp ensure_context_presence(%{"object" => object} = data) when is_binary(object) do
with %{data: %{"context" => context}} when is_binary(context) <- Object.normalize(object) do
{:ok, Map.put(data, "context", context)}
else
_ ->
{:error, :no_context}
end
end
defp ensure_context_presence(_) do
{:error, :no_context}
end
defp ensure_recipients_presence(%{"to" => [_ | _], "cc" => [_ | _]} = data),
do: {:ok, data}
defp ensure_recipients_presence(%{"object" => object} = data) do
case Object.normalize(object) do
%{data: %{"actor" => actor}} ->
data =
data
|> Map.put("to", [actor])
|> Map.put("cc", data["cc"] || [])
{:ok, data}
nil ->
{:error, :no_object}
_ ->
{:error, :no_actor}
end
end
defp ensure_recipients_presence(_) do
{:error, :no_object}
end
end end

View file

@ -93,7 +93,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true} %{scopes: ["read:statuses"], admin: true}
when action in [:list_statuses, :list_user_statuses, :list_instance_statuses] when action in [:list_statuses, :list_user_statuses, :list_instance_statuses, :status_show]
) )
plug( plug(
@ -837,6 +837,16 @@ def list_statuses(%{assigns: %{user: _admin}} = conn, params) do
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false}) |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
end end
def status_show(conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id) do
conn
|> put_view(StatusView)
|> render("show.json", %{activity: activity})
else
_ -> errors(conn, {:error, :not_found})
end
end
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
{:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"]) {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])

View file

@ -0,0 +1,139 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.CastAndValidate do
@moduledoc """
This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
(https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
The main difference is ignoring unexpected query params instead of throwing
an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
to disable this behavior. Also, the default rendering error module
is `Pleroma.Web.ApiSpec.RenderError`.
"""
@behaviour Plug
alias Plug.Conn
@impl Plug
def init(opts) do
opts
|> Map.new()
|> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
end
@impl Plug
def call(%{private: %{open_api_spex: private_data}} = conn, %{
operation_id: operation_id,
render_error: render_error
}) do
spec = private_data.spec
operation = private_data.operation_lookup[operation_id]
content_type =
case Conn.get_req_header(conn, "content-type") do
[header_value | _] ->
header_value
|> String.split(";")
|> List.first()
_ ->
nil
end
private_data = Map.put(private_data, :operation_id, operation_id)
conn = Conn.put_private(conn, :open_api_spex, private_data)
case cast_and_validate(spec, operation, conn, content_type, strict?()) do
{:ok, conn} ->
conn
{:error, reason} ->
opts = render_error.init(reason)
conn
|> render_error.call(opts)
|> Plug.Conn.halt()
end
end
def call(
%{
private: %{
phoenix_controller: controller,
phoenix_action: action,
open_api_spex: private_data
}
} = conn,
opts
) do
operation =
case private_data.operation_lookup[{controller, action}] do
nil ->
operation_id = controller.open_api_operation(action).operationId
operation = private_data.operation_lookup[operation_id]
operation_lookup =
private_data.operation_lookup
|> Map.put({controller, action}, operation)
OpenApiSpex.Plug.Cache.adapter().put(
private_data.spec_module,
{private_data.spec, operation_lookup}
)
operation
operation ->
operation
end
if operation.operationId do
call(conn, Map.put(opts, :operation_id, operation.operationId))
else
raise "operationId was not found in action API spec"
end
end
def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
end
defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
{:ok, conn} ->
{:ok, conn}
# Remove unexpected query params and cast/validate again
{:error, errors} ->
query_params =
Enum.reduce(errors, conn.query_params, fn
%{reason: :unexpected_field, name: name, path: [name]}, params ->
Map.delete(params, name)
%{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
update_in(params, path, &List.delete(&1, value))
_, params ->
params
end)
conn = %Conn{conn | query_params: query_params}
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
end
end
defp list_items_to_string(list) do
Enum.map(list, fn
i when is_atom(i) -> to_string(i)
i -> i
end)
end
defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
end

View file

@ -0,0 +1,96 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["Scheduled Statuses"],
summary: "View scheduled statuses",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: pagination_params(),
operationId: "ScheduledActivity.index",
responses: %{
200 =>
Operation.response("Array of ScheduledStatus", "application/json", %Schema{
type: :array,
items: ScheduledStatus
})
}
}
end
def show_operation do
%Operation{
tags: ["Scheduled Statuses"],
summary: "View a single scheduled status",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [id_param()],
operationId: "ScheduledActivity.show",
responses: %{
200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
tags: ["Scheduled Statuses"],
summary: "Schedule a status",
operationId: "ScheduledActivity.update",
security: [%{"oAuth" => ["write:statuses"]}],
parameters: [id_param()],
requestBody:
request_body("Parameters", %Schema{
type: :object,
properties: %{
scheduled_at: %Schema{
type: :string,
format: :"date-time",
description:
"ISO 8601 Datetime at which the status will be published. Must be at least 5 minutes into the future."
}
}
}),
responses: %{
200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def delete_operation do
%Operation{
tags: ["Scheduled Statuses"],
summary: "Cancel a scheduled status",
security: [%{"oAuth" => ["write:statuses"]}],
parameters: [id_param()],
operationId: "ScheduledActivity.delete",
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
defp id_param do
Operation.parameter(:id, :path, FlakeID, "Poll ID",
example: "123",
required: true
)
end
end

View file

@ -17,6 +17,9 @@ def init(opts), do: opts
def call(conn, errors) do def call(conn, errors) do
errors = errors =
Enum.map(errors, fn Enum.map(errors, fn
%{name: nil, reason: :invalid_enum} = err ->
%OpenApiSpex.Cast.Error{err | name: err.value}
%{name: nil} = err -> %{name: nil} = err ->
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)} %OpenApiSpex.Cast.Error{err | name: List.last(err.path)}

View file

@ -0,0 +1,68 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Attachment",
description: "Represents a file or media attachment that can be added to a status.",
type: :object,
requried: [:id, :url, :preview_url],
properties: %{
id: %Schema{type: :string},
url: %Schema{
type: :string,
format: :uri,
description: "The location of the original full-size attachment"
},
remote_url: %Schema{
type: :string,
format: :uri,
description:
"The location of the full-size original attachment on the remote website. String (URL), or null if the attachment is local",
nullable: true
},
preview_url: %Schema{
type: :string,
format: :uri,
description: "The location of a scaled-down preview of the attachment"
},
text_url: %Schema{
type: :string,
format: :uri,
description: "A shorter URL for the attachment"
},
description: %Schema{
type: :string,
nullable: true,
description:
"Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load"
},
type: %Schema{
type: :string,
enum: ["image", "video", "audio", "unknown"],
description: "The type of the attachment"
},
pleroma: %Schema{
type: :object,
properties: %{
mime_type: %Schema{type: :string, description: "mime type of the attachment"}
}
}
},
example: %{
id: "1638338801",
type: "image",
url: "someurl",
remote_url: "someurl",
preview_url: "someurl",
text_url: "someurl",
description: nil,
pleroma: %{mime_type: "image/png"}
}
})
end

View file

@ -0,0 +1,54 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Attachment
alias Pleroma.Web.ApiSpec.Schemas.Poll
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
require OpenApiSpex
OpenApiSpex.schema(%{
title: "ScheduledStatus",
description: "Represents a status that will be published at a future scheduled date.",
type: :object,
required: [:id, :scheduled_at, :params],
properties: %{
id: %Schema{type: :string},
scheduled_at: %Schema{type: :string, format: :"date-time"},
media_attachments: %Schema{type: :array, items: Attachment},
params: %Schema{
type: :object,
required: [:text, :visibility],
properties: %{
text: %Schema{type: :string, nullable: true},
media_ids: %Schema{type: :array, nullable: true, items: %Schema{type: :string}},
sensitive: %Schema{type: :boolean, nullable: true},
spoiler_text: %Schema{type: :string, nullable: true},
visibility: %Schema{type: VisibilityScope, nullable: true},
scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true},
poll: %Schema{type: Poll, nullable: true},
in_reply_to_id: %Schema{type: :string, nullable: true}
}
}
},
example: %{
id: "3221",
scheduled_at: "2019-12-05T12:33:01.000Z",
params: %{
text: "test content",
media_ids: nil,
sensitive: nil,
spoiler_text: nil,
visibility: nil,
scheduled_at: nil,
poll: nil,
idempotency: nil,
in_reply_to_id: nil
},
media_attachments: [Attachment.schema().example]
}
})
end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ApiSpec.Schemas.Status do defmodule Pleroma.Web.ApiSpec.Schemas.Status do
alias OpenApiSpex.Schema alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.Attachment
alias Pleroma.Web.ApiSpec.Schemas.Emoji alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Poll alias Pleroma.Web.ApiSpec.Schemas.Poll
@ -50,22 +51,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
language: %Schema{type: :string, nullable: true}, language: %Schema{type: :string, nullable: true},
media_attachments: %Schema{ media_attachments: %Schema{
type: :array, type: :array,
items: %Schema{ items: Attachment
type: :object,
properties: %{
id: %Schema{type: :string},
url: %Schema{type: :string, format: :uri},
remote_url: %Schema{type: :string, format: :uri},
preview_url: %Schema{type: :string, format: :uri},
text_url: %Schema{type: :string, format: :uri},
description: %Schema{type: :string},
type: %Schema{type: :string, enum: ["image", "video", "audio", "unknown"]},
pleroma: %Schema{
type: :object,
properties: %{mime_type: %Schema{type: :string}}
}
}
}
}, },
mentions: %Schema{ mentions: %Schema{
type: :array, type: :array,

View file

@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.TwitterAPI
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError) plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create) plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)

View file

@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials) plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
plug(OpenApiSpex.Plug.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate)
@local_mastodon_name "Mastodon-Local" @local_mastodon_name "Mastodon-Local"

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
plug(OpenApiSpex.Plug.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug( plug(
:skip_plug, :skip_plug,

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User alias Pleroma.User
plug(OpenApiSpex.Plug.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
plug( plug(

View file

@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
@oauth_read_actions [:show, :index] @oauth_read_actions [:show, :index]
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError) plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,

View file

@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError) plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create) plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation

View file

@ -11,17 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
alias Pleroma.ScheduledActivity alias Pleroma.ScheduledActivity
alias Pleroma.Web.MastodonAPI.MastodonAPI alias Pleroma.Web.MastodonAPI.MastodonAPI
plug(:assign_scheduled_activity when action != :index)
@oauth_read_actions [:show, :index] @oauth_read_actions [:show, :index]
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions) plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions) plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
plug(:assign_scheduled_activity when action != :index)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ScheduledActivityOperation
@doc "GET /api/v1/scheduled_statuses" @doc "GET /api/v1/scheduled_statuses"
def index(%{assigns: %{user: user}} = conn, params) do def index(%{assigns: %{user: user}} = conn, params) do
params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
conn conn
|> add_link_headers(scheduled_activities) |> add_link_headers(scheduled_activities)
@ -35,7 +39,7 @@ def show(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params)
end end
@doc "PUT /api/v1/scheduled_statuses/:id" @doc "PUT /api/v1/scheduled_statuses/:id"
def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do def update(%{assigns: %{scheduled_activity: scheduled_activity}, body_params: params} = conn, _) do
with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
render(conn, "show.json", scheduled_activity: scheduled_activity) render(conn, "show.json", scheduled_activity: scheduled_activity)
end end
@ -48,7 +52,7 @@ def delete(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params
end end
end end
defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
case ScheduledActivity.get(user, id) do case ScheduledActivity.get(user, id) do
%ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity) %ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt() nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()

View file

@ -188,6 +188,7 @@ defmodule Pleroma.Web.Router do
post("/reports/:id/notes", AdminAPIController, :report_notes_create) post("/reports/:id/notes", AdminAPIController, :report_notes_create)
delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete) delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete)
get("/statuses/:id", AdminAPIController, :status_show)
put("/statuses/:id", AdminAPIController, :status_update) put("/statuses/:id", AdminAPIController, :status_update)
delete("/statuses/:id", AdminAPIController, :status_delete) delete("/statuses/:id", AdminAPIController, :status_delete)
get("/statuses", AdminAPIController, :list_statuses) get("/statuses", AdminAPIController, :list_statuses)

View file

@ -86,54 +86,24 @@ def represent_user(user, "XML") do
|> XmlBuilder.to_doc() |> XmlBuilder.to_doc()
end end
defp get_magic_key("data:application/magic-public-key," <> magic_key) do
{:ok, magic_key}
end
defp get_magic_key(nil) do
Logger.debug("Undefined magic key.")
{:ok, nil}
end
defp get_magic_key(_) do
{:error, "Missing magic key data."}
end
defp webfinger_from_xml(doc) do defp webfinger_from_xml(doc) do
with magic_key <- XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc), subject = XML.string_from_xpath("//Subject", doc)
{:ok, magic_key} <- get_magic_key(magic_key),
topic <-
XML.string_from_xpath(
~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href},
doc
),
subject <- XML.string_from_xpath("//Subject", doc),
subscribe_address <-
XML.string_from_xpath(
~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
doc
),
ap_id <-
XML.string_from_xpath(
~s{//Link[@rel="self" and @type="application/activity+json"]/@href},
doc
) do
data = %{
"magic_key" => magic_key,
"topic" => topic,
"subject" => subject,
"subscribe_address" => subscribe_address,
"ap_id" => ap_id
}
{:ok, data} subscribe_address =
else ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}
{:error, e} -> |> XML.string_from_xpath(doc)
{:error, e}
e -> ap_id =
{:error, e} ~s{//Link[@rel="self" and @type="application/activity+json"]/@href}
end |> XML.string_from_xpath(doc)
data = %{
"subject" => subject,
"subscribe_address" => subscribe_address,
"ap_id" => ap_id
}
{:ok, data}
end end
defp webfinger_from_json(doc) do defp webfinger_from_json(doc) do
@ -146,9 +116,6 @@ defp webfinger_from_json(doc) do
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} -> {"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
Map.put(data, "ap_id", link["href"]) Map.put(data, "ap_id", link["href"])
{_, "http://ostatus.org/schema/1.0/subscribe"} ->
Map.put(data, "subscribe_address", link["template"])
_ -> _ ->
Logger.debug("Unhandled type: #{inspect(link["type"])}") Logger.debug("Unhandled type: #{inspect(link["type"])}")
data data

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.RemoveMagicKeyField do
use Ecto.Migration
def change do
alter table(:users) do
remove(:magic_key, :string)
end
end
end

View file

@ -32,6 +32,7 @@ def user_factory do
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: sequence(:bio, &"Tester Number #{&1}"), bio: sequence(:bio, &"Tester Number #{&1}"),
last_digest_emailed_at: NaiveDateTime.utc_now(), last_digest_emailed_at: NaiveDateTime.utc_now(),
last_refreshed_at: NaiveDateTime.utc_now(),
notification_settings: %Pleroma.User.NotificationSetting{} notification_settings: %Pleroma.User.NotificationSetting{}
} }

View file

@ -40,12 +40,18 @@ defmacro __using__(_opts) do
clear_config: 2 clear_config: 2
] ]
def to_datetime(naive_datetime) do def to_datetime(%NaiveDateTime{} = naive_datetime) do
naive_datetime naive_datetime
|> DateTime.from_naive!("Etc/UTC") |> DateTime.from_naive!("Etc/UTC")
|> DateTime.truncate(:second) |> DateTime.truncate(:second)
end end
def to_datetime(datetime) when is_binary(datetime) do
datetime
|> NaiveDateTime.from_iso8601!()
|> to_datetime()
end
def collect_ids(collection) do def collect_ids(collection) do
collection collection
|> Enum.map(& &1.id) |> Enum.map(& &1.id)

View file

@ -18,9 +18,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
import ExUnit.CaptureLog
import Mock
import Pleroma.Factory import Pleroma.Factory
import Tesla.Mock import Tesla.Mock
import Mock
setup do setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end) mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@ -2403,4 +2404,51 @@ defp private_messages(_) do
u3: %{r1: r3_1.id, r2: r3_2.id}, u3: %{r1: r3_1.id, r2: r3_2.id},
u4: %{r1: r4_1.id}} u4: %{r1: r4_1.id}}
end end
describe "maybe_update_follow_information/1" do
setup do
clear_config([:instance, :external_user_synchronization], true)
user = %{
local: false,
ap_id: "https://gensokyo.2hu/users/raymoo",
following_address: "https://gensokyo.2hu/users/following",
follower_address: "https://gensokyo.2hu/users/followers",
type: "Person"
}
%{user: user}
end
test "logs an error when it can't fetch the info", %{user: user} do
assert capture_log(fn ->
ActivityPub.maybe_update_follow_information(user)
end) =~ "Follower/Following counter update for #{user.ap_id} failed"
end
test "just returns the input if the user type is Application", %{
user: user
} do
user =
user
|> Map.put(:type, "Application")
refute capture_log(fn ->
assert ^user = ActivityPub.maybe_update_follow_information(user)
end) =~ "Follower/Following counter update for #{user.ap_id} failed"
end
test "it just returns the input if the user has no following/follower addresses", %{
user: user
} do
user =
user
|> Map.put(:following_address, nil)
|> Map.put(:follower_address, nil)
refute capture_log(fn ->
assert ^user = ActivityPub.maybe_update_follow_information(user)
end) =~ "Follower/Following counter update for #{user.ap_id} failed"
end
end
end end

View file

@ -36,6 +36,32 @@ test "is valid for a valid object", %{valid_like: valid_like} do
assert LikeValidator.cast_and_validate(valid_like).valid? assert LikeValidator.cast_and_validate(valid_like).valid?
end end
test "sets the 'to' field to the object actor if no recipients are given", %{
valid_like: valid_like,
user: user
} do
without_recipients =
valid_like
|> Map.delete("to")
{:ok, object, _meta} = ObjectValidator.validate(without_recipients, [])
assert object["to"] == [user.ap_id]
end
test "sets the context field to the context of the object if no context is given", %{
valid_like: valid_like,
post_activity: post_activity
} do
without_context =
valid_like
|> Map.delete("context")
{:ok, object, _meta} = ObjectValidator.validate(without_context, [])
assert object["context"] == post_activity.data["context"]
end
test "it errors when the actor is missing or not known", %{valid_like: valid_like} do test "it errors when the actor is missing or not known", %{valid_like: valid_like} do
without_actor = Map.delete(valid_like, "actor") without_actor = Map.delete(valid_like, "actor")

View file

@ -0,0 +1,78 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
test "it works for incoming likes" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/mastodon-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
_actor = insert(:user, ap_id: data["actor"], local: false)
{:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
refute Enum.empty?(activity.recipients)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Like"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2"
assert data["object"] == activity.data["object"]
end
test "it works for incoming misskey likes, turning them into EmojiReacts" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/misskey-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
_actor = insert(:user, ap_id: data["actor"], local: false)
{:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data)
assert activity_data["actor"] == data["actor"]
assert activity_data["type"] == "EmojiReact"
assert activity_data["id"] == data["id"]
assert activity_data["object"] == activity.data["object"]
assert activity_data["content"] == "🍮"
end
test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/misskey-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
|> Map.put("_misskey_reaction", "")
_actor = insert(:user, ap_id: data["actor"], local: false)
{:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data)
assert activity_data["actor"] == data["actor"]
assert activity_data["type"] == "EmojiReact"
assert activity_data["id"] == data["id"]
assert activity_data["object"] == activity.data["object"]
assert activity_data["content"] == ""
end
end

View file

@ -325,62 +325,6 @@ test "it cleans up incoming notices which are not really DMs" do
assert object_data["cc"] == to assert object_data["cc"] == to
end end
test "it works for incoming likes" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/mastodon-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
refute Enum.empty?(activity.recipients)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Like"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2"
assert data["object"] == activity.data["object"]
end
test "it works for incoming misskey likes, turning them into EmojiReacts" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/misskey-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == data["actor"]
assert data["type"] == "EmojiReact"
assert data["id"] == data["id"]
assert data["object"] == activity.data["object"]
assert data["content"] == "🍮"
end
test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/misskey-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
|> Map.put("_misskey_reaction", "")
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == data["actor"]
assert data["type"] == "EmojiReact"
assert data["id"] == data["id"]
assert data["object"] == activity.data["object"]
assert data["content"] == ""
end
test "it works for incoming emoji reactions" do test "it works for incoming emoji reactions" do
user = insert(:user) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})

View file

@ -1620,6 +1620,25 @@ test "returns 403 when requested by anonymous" do
end end
end end
describe "GET /api/pleroma/admin/statuses/:id" do
test "not found", %{conn: conn} do
assert conn
|> get("/api/pleroma/admin/statuses/not_found")
|> json_response(:not_found)
end
test "shows activity", %{conn: conn} do
activity = insert(:note_activity)
response =
conn
|> get("/api/pleroma/admin/statuses/#{activity.id}")
|> json_response(200)
assert response["id"] == activity.id
end
end
describe "PUT /api/pleroma/admin/statuses/:id" do describe "PUT /api/pleroma/admin/statuses/:id" do
setup do setup do
activity = insert(:note_activity) activity = insert(:note_activity)

View file

@ -24,19 +24,19 @@ test "shows scheduled activities" do
# min_id # min_id
conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}") conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}")
result = json_response(conn_res, 200) result = json_response_and_validate_schema(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
# since_id # since_id
conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}") conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}")
result = json_response(conn_res, 200) result = json_response_and_validate_schema(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result
# max_id # max_id
conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}") conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}")
result = json_response(conn_res, 200) result = json_response_and_validate_schema(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
end end
@ -46,12 +46,12 @@ test "shows a scheduled activity" do
res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}") res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}")
assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200) assert %{"id" => scheduled_activity_id} = json_response_and_validate_schema(res_conn, 200)
assert scheduled_activity_id == scheduled_activity.id |> to_string() assert scheduled_activity_id == scheduled_activity.id |> to_string()
res_conn = get(conn, "/api/v1/scheduled_statuses/404") res_conn = get(conn, "/api/v1/scheduled_statuses/404")
assert %{"error" => "Record not found"} = json_response(res_conn, 404) assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404)
end end
test "updates a scheduled activity" do test "updates a scheduled activity" do
@ -74,22 +74,32 @@ test "updates a scheduled activity" do
assert job.args == %{"activity_id" => scheduled_activity.id} assert job.args == %{"activity_id" => scheduled_activity.id}
assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(scheduled_at) assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(scheduled_at)
new_scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 120) new_scheduled_at =
NaiveDateTime.utc_now()
|> Timex.shift(minutes: 120)
|> Timex.format!("%Y-%m-%dT%H:%M:%S.%fZ", :strftime)
res_conn = res_conn =
put(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{ conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
scheduled_at: new_scheduled_at scheduled_at: new_scheduled_at
}) })
assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200) assert %{"scheduled_at" => expected_scheduled_at} =
json_response_and_validate_schema(res_conn, 200)
assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at) assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at)
job = refresh_record(job) job = refresh_record(job)
assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(new_scheduled_at) assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(new_scheduled_at)
res_conn = put(conn, "/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at}) res_conn =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
assert %{"error" => "Record not found"} = json_response(res_conn, 404) assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404)
end end
test "deletes a scheduled activity" do test "deletes a scheduled activity" do
@ -115,7 +125,7 @@ test "deletes a scheduled activity" do
|> assign(:user, user) |> assign(:user, user)
|> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
assert %{} = json_response(res_conn, 200) assert %{} = json_response_and_validate_schema(res_conn, 200)
refute Repo.get(ScheduledActivity, scheduled_activity.id) refute Repo.get(ScheduledActivity, scheduled_activity.id)
refute Repo.get(Oban.Job, job.id) refute Repo.get(Oban.Job, job.id)
@ -124,6 +134,6 @@ test "deletes a scheduled activity" do
|> assign(:user, user) |> assign(:user, user)
|> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
assert %{"error" => "Record not found"} = json_response(res_conn, 404) assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404)
end end
end end

View file

@ -402,11 +402,17 @@ test "attachments" do
pleroma: %{mime_type: "image/png"} pleroma: %{mime_type: "image/png"}
} }
api_spec = Pleroma.Web.ApiSpec.spec()
assert expected == StatusView.render("attachment.json", %{attachment: object}) assert expected == StatusView.render("attachment.json", %{attachment: object})
OpenApiSpex.TestAssertions.assert_schema(expected, "Attachment", api_spec)
# If theres a "id", use that instead of the generated one # If theres a "id", use that instead of the generated one
object = Map.put(object, "id", 2) object = Map.put(object, "id", 2)
assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object}) result = StatusView.render("attachment.json", %{attachment: object})
assert %{id: "2"} = result
OpenApiSpex.TestAssertions.assert_schema(result, "Attachment", api_spec)
end end
test "put the url advertised in the Activity in to the url attribute" do test "put the url advertised in the Activity in to the url attribute" do

View file

@ -67,7 +67,7 @@ test "it work for AP-only user" do
assert data["magic_key"] == nil assert data["magic_key"] == nil
assert data["salmon"] == nil assert data["salmon"] == nil
assert data["topic"] == "https://mstdn.jp/users/kPherox.atom" assert data["topic"] == nil
assert data["subject"] == "acct:kPherox@mstdn.jp" assert data["subject"] == "acct:kPherox@mstdn.jp"
assert data["ap_id"] == "https://mstdn.jp/users/kPherox" assert data["ap_id"] == "https://mstdn.jp/users/kPherox"
assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}" assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}"