Implement mastodon api for editing status
This commit is contained in:
parent
393b508846
commit
b613a9ec6b
8 changed files with 271 additions and 19 deletions
|
@ -27,4 +27,28 @@ defmodule Pleroma.Constants do
|
||||||
do:
|
do:
|
||||||
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
|
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const(status_updatable_fields,
|
||||||
|
do: [
|
||||||
|
"source",
|
||||||
|
"tag",
|
||||||
|
"updated",
|
||||||
|
"emoji",
|
||||||
|
"content",
|
||||||
|
"summary",
|
||||||
|
"sensitive",
|
||||||
|
"attachment",
|
||||||
|
"generator"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const(actor_types,
|
||||||
|
do: [
|
||||||
|
"Application",
|
||||||
|
"Group",
|
||||||
|
"Organization",
|
||||||
|
"Person",
|
||||||
|
"Service"
|
||||||
|
]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -218,10 +218,16 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Retricted to user updates for now, always public
|
|
||||||
@spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
@spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
||||||
def update(actor, object) do
|
def update(actor, object) do
|
||||||
to = [Pleroma.Constants.as_public(), actor.follower_address]
|
{to, cc} =
|
||||||
|
if object["type"] in Pleroma.Constants.actor_types() do
|
||||||
|
# User updates, always public
|
||||||
|
{[Pleroma.Constants.as_public(), actor.follower_address], []}
|
||||||
|
else
|
||||||
|
# Status updates, follow the recipients in the object
|
||||||
|
{object["to"] || [], object["cc"] || []}
|
||||||
|
end
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
|
@ -229,7 +235,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
||||||
"type" => "Update",
|
"type" => "Update",
|
||||||
"actor" => actor.ap_id,
|
"actor" => actor.ap_id,
|
||||||
"object" => object,
|
"object" => object,
|
||||||
"to" => to
|
"to" => to,
|
||||||
|
"cc" => cc
|
||||||
}, []}
|
}, []}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
||||||
alias Pleroma.Web.Streamer
|
alias Pleroma.Web.Streamer
|
||||||
alias Pleroma.Workers.PollWorker
|
alias Pleroma.Workers.PollWorker
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||||
|
@ -411,20 +412,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
||||||
end
|
end
|
||||||
|
|
||||||
@updatable_object_types ["Note", "Question"]
|
@updatable_object_types ["Note", "Question"]
|
||||||
# We do not allow poll options to be changed, but the poll description can be.
|
|
||||||
@updatable_fields [
|
|
||||||
"source",
|
|
||||||
"tag",
|
|
||||||
"updated",
|
|
||||||
"emoji",
|
|
||||||
"content",
|
|
||||||
"summary",
|
|
||||||
"sensitive",
|
|
||||||
"attachment",
|
|
||||||
"generator"
|
|
||||||
]
|
|
||||||
defp update_content_fields(orig_object_data, updated_object) do
|
defp update_content_fields(orig_object_data, updated_object) do
|
||||||
@updatable_fields
|
Pleroma.Constants.status_updatable_fields()
|
||||||
|> Enum.reduce(
|
|> Enum.reduce(
|
||||||
%{data: orig_object_data, updated: false},
|
%{data: orig_object_data, updated: false},
|
||||||
fn field, %{data: data, updated: updated} ->
|
fn field, %{data: data, updated: updated} ->
|
||||||
|
@ -502,6 +491,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
||||||
|> maybe_update_poll(updated_object)
|
|> maybe_update_poll(updated_object)
|
||||||
|
|
||||||
orig_object
|
orig_object
|
||||||
|
|> Repo.preload(:hashtags)
|
||||||
|> Object.change(%{data: updated_object_data})
|
|> Object.change(%{data: updated_object_data})
|
||||||
|> Object.update_and_set_cache()
|
|> Object.update_and_set_cache()
|
||||||
end
|
end
|
||||||
|
|
|
@ -473,6 +473,22 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_operation do
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Update status"],
|
||||||
|
summary: "Update status",
|
||||||
|
description: "Change the content of a status",
|
||||||
|
operationId: "StatusController.update",
|
||||||
|
security: [%{"oAuth" => ["write:statuses"]}],
|
||||||
|
parameters: [
|
||||||
|
id_param()
|
||||||
|
],
|
||||||
|
requestBody: request_body("Parameters", update_request(), required: true),
|
||||||
|
responses: %{
|
||||||
|
200 => status_response(),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def array_of_statuses do
|
def array_of_statuses do
|
||||||
|
@ -578,6 +594,60 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp update_request do
|
||||||
|
%Schema{
|
||||||
|
title: "StatusUpdateRequest",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
status: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
"Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
|
||||||
|
},
|
||||||
|
media_ids: %Schema{
|
||||||
|
nullable: true,
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :string},
|
||||||
|
description: "Array of Attachment ids to be attached as media."
|
||||||
|
},
|
||||||
|
poll: poll_params(),
|
||||||
|
sensitive: %Schema{
|
||||||
|
allOf: [BooleanLike],
|
||||||
|
nullable: true,
|
||||||
|
description: "Mark status and attached media as sensitive?"
|
||||||
|
},
|
||||||
|
spoiler_text: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
"Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
|
||||||
|
},
|
||||||
|
content_type: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
"The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint."
|
||||||
|
},
|
||||||
|
to: %Schema{
|
||||||
|
type: :array,
|
||||||
|
nullable: true,
|
||||||
|
items: %Schema{type: :string},
|
||||||
|
description:
|
||||||
|
"A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"status" => "What time is it?",
|
||||||
|
"sensitive" => "false",
|
||||||
|
"poll" => %{
|
||||||
|
"options" => ["Cofe", "Adventure"],
|
||||||
|
"expires_in" => 420
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def poll_params do
|
def poll_params do
|
||||||
%Schema{
|
%Schema{
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
@ -690,7 +760,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
||||||
spoiler_text: %Schema{
|
spoiler_text: %Schema{
|
||||||
type: :string,
|
type: :string,
|
||||||
description:
|
description:
|
||||||
"Subject or summary line, below which status content is collapsed until expanded"
|
"Subject or summary line, below which status content is collapsed until expanded"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -402,6 +402,42 @@ defmodule Pleroma.Web.CommonAPI do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update(user, orig_activity, changes) do
|
||||||
|
with orig_object <- Object.normalize(orig_activity),
|
||||||
|
{:ok, new_object} <- make_update_data(user, orig_object, changes),
|
||||||
|
{:ok, update_data, _} <- Builder.update(user, new_object),
|
||||||
|
{:ok, update, _} <- Pipeline.common_pipeline(update_data, local: true) do
|
||||||
|
{:ok, update}
|
||||||
|
else
|
||||||
|
_ -> {:error, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp make_update_data(user, orig_object, changes) do
|
||||||
|
kept_params = %{
|
||||||
|
visibility: Visibility.get_visibility(orig_object)
|
||||||
|
}
|
||||||
|
|
||||||
|
params = Map.merge(changes, kept_params)
|
||||||
|
|
||||||
|
with {:ok, draft} <- ActivityDraft.create(user, params) do
|
||||||
|
change =
|
||||||
|
Pleroma.Constants.status_updatable_fields()
|
||||||
|
|> Enum.reduce(orig_object.data, fn key, acc ->
|
||||||
|
if Map.has_key?(draft.object, key) do
|
||||||
|
acc |> Map.put(key, Map.get(draft.object, key))
|
||||||
|
else
|
||||||
|
acc |> Map.drop([key])
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Map.put("updated", Utils.make_date())
|
||||||
|
|
||||||
|
{:ok, change}
|
||||||
|
else
|
||||||
|
_ -> {:error, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
|
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
|
||||||
def pin(id, %User{} = user) do
|
def pin(id, %User{} = user) do
|
||||||
with %Activity{} = activity <- create_activity_by_id(id),
|
with %Activity{} = activity <- create_activity_by_id(id),
|
||||||
|
|
|
@ -223,7 +223,26 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "PUT /api/v1/statuses/:id"
|
@doc "PUT /api/v1/statuses/:id"
|
||||||
def update(%{assigns: %{user: _user}} = _conn, %{id: _id} = _params) do
|
def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do
|
||||||
|
with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
|
||||||
|
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
|
||||||
|
{_, true} <- {:is_create, activity.data["type"] == "Create"},
|
||||||
|
actor <- Activity.user_actor(activity),
|
||||||
|
{_, true} <- {:own_status, actor.id == user.id},
|
||||||
|
changes <- body_params |> put_application(conn),
|
||||||
|
{_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
|
||||||
|
{_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
|
||||||
|
try_render(conn, "show.json",
|
||||||
|
activity: activity,
|
||||||
|
for: user,
|
||||||
|
with_direct_conversation_id: true,
|
||||||
|
with_muted: Map.get(params, :with_muted, false)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
{:own_status, _} -> {:error, :forbidden}
|
||||||
|
{:pipeline, _} -> {:error, :internal_server_error}
|
||||||
|
_ -> {:error, :not_found}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "GET /api/v1/statuses/:id"
|
@doc "GET /api/v1/statuses/:id"
|
||||||
|
|
|
@ -1541,4 +1541,33 @@ defmodule Pleroma.Web.CommonAPITest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "update/3" do
|
||||||
|
test "updates a post" do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1"})
|
||||||
|
|
||||||
|
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
|
||||||
|
|
||||||
|
updated_object = Object.normalize(updated)
|
||||||
|
assert updated_object.data["content"] == "updated 2"
|
||||||
|
assert Map.get(updated_object.data, "summary", "") == ""
|
||||||
|
assert Map.has_key?(updated_object.data, "updated")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not change visibility" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1", visibility: "private"})
|
||||||
|
|
||||||
|
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
|
||||||
|
|
||||||
|
updated_object = Object.normalize(updated)
|
||||||
|
assert updated_object.data["content"] == "updated 2"
|
||||||
|
assert Map.get(updated_object.data, "summary", "") == ""
|
||||||
|
assert Visibility.get_visibility(updated_object) == "private"
|
||||||
|
assert Visibility.get_visibility(updated) == "private"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2051,7 +2051,84 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|
||||||
|
|
||||||
id = activity.id
|
id = activity.id
|
||||||
|
|
||||||
assert %{"id" => ^id, "text" => "mew mew #abc", "spoiler_text" => "#def"} = json_response_and_validate_schema(conn, 200)
|
assert %{"id" => ^id, "text" => "mew mew #abc", "spoiler_text" => "#def"} =
|
||||||
|
json_response_and_validate_schema(conn, 200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update status" do
|
||||||
|
setup do
|
||||||
|
oauth_access(["write:statuses"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it updates the status", %{conn: conn, user: user} do
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put("/api/v1/statuses/#{activity.id}", %{
|
||||||
|
"status" => "edited",
|
||||||
|
"spoiler_text" => "lol"
|
||||||
|
})
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert response["content"] == "edited"
|
||||||
|
assert response["spoiler_text"] == "lol"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it does not update visibility", %{conn: conn, user: user} do
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
status: "mew mew #abc",
|
||||||
|
spoiler_text: "#def",
|
||||||
|
visibility: "private"
|
||||||
|
})
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put("/api/v1/statuses/#{activity.id}", %{
|
||||||
|
"status" => "edited",
|
||||||
|
"spoiler_text" => "lol"
|
||||||
|
})
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert response["visibility"] == "private"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it refuses to update when original post is not by the user", %{conn: conn} do
|
||||||
|
another_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(another_user, %{status: "mew mew #abc", spoiler_text: "#def"})
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put("/api/v1/statuses/#{activity.id}", %{
|
||||||
|
"status" => "edited",
|
||||||
|
"spoiler_text" => "lol"
|
||||||
|
})
|
||||||
|
|> json_response_and_validate_schema(:forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns 404 if the user cannot see the post", %{conn: conn} do
|
||||||
|
another_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(another_user, %{
|
||||||
|
status: "mew mew #abc",
|
||||||
|
spoiler_text: "#def",
|
||||||
|
visibility: "private"
|
||||||
|
})
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put("/api/v1/statuses/#{activity.id}", %{
|
||||||
|
"status" => "edited",
|
||||||
|
"spoiler_text" => "lol"
|
||||||
|
})
|
||||||
|
|> json_response_and_validate_schema(:not_found)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue