forked from AkkomaGang/akkoma
Implement mastodon api for editing status
This commit is contained in:
parent
f269e910ef
commit
e6ab68c624
8 changed files with 271 additions and 19 deletions
|
@ -27,4 +27,28 @@ defmodule Pleroma.Constants 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)
|
||||
)
|
||||
|
||||
const(status_updatable_fields,
|
||||
do: [
|
||||
"source",
|
||||
"tag",
|
||||
"updated",
|
||||
"emoji",
|
||||
"content",
|
||||
"summary",
|
||||
"sensitive",
|
||||
"attachment",
|
||||
"generator"
|
||||
]
|
||||
)
|
||||
|
||||
const(actor_types,
|
||||
do: [
|
||||
"Application",
|
||||
"Group",
|
||||
"Organization",
|
||||
"Person",
|
||||
"Service"
|
||||
]
|
||||
)
|
||||
end
|
||||
|
|
|
@ -231,10 +231,16 @@ def like(actor, object) do
|
|||
end
|
||||
end
|
||||
|
||||
# Retricted to user updates for now, always public
|
||||
@spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
||||
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,
|
||||
%{
|
||||
|
@ -242,7 +248,8 @@ def update(actor, object) do
|
|||
"type" => "Update",
|
||||
"actor" => actor.ap_id,
|
||||
"object" => object,
|
||||
"to" => to
|
||||
"to" => to,
|
||||
"cc" => cc
|
||||
}, []}
|
||||
end
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
alias Pleroma.Web.Streamer
|
||||
alias Pleroma.Workers.PollWorker
|
||||
|
||||
require Pleroma.Constants
|
||||
require Logger
|
||||
|
||||
@logger Pleroma.Config.get([:side_effects, :logger], Logger)
|
||||
|
@ -416,20 +417,8 @@ defp handle_update_user(
|
|||
end
|
||||
|
||||
@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
|
||||
@updatable_fields
|
||||
Pleroma.Constants.status_updatable_fields()
|
||||
|> Enum.reduce(
|
||||
%{data: orig_object_data, updated: false},
|
||||
fn field, %{data: data, updated: updated} ->
|
||||
|
@ -507,6 +496,7 @@ defp handle_update_object(
|
|||
|> maybe_update_poll(updated_object)
|
||||
|
||||
orig_object
|
||||
|> Repo.preload(:hashtags)
|
||||
|> Object.change(%{data: updated_object_data})
|
||||
|> Object.update_and_set_cache()
|
||||
end
|
||||
|
|
|
@ -445,6 +445,22 @@ def show_source_operation do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def array_of_statuses do
|
||||
|
@ -555,6 +571,60 @@ defp create_request do
|
|||
}
|
||||
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
|
||||
%Schema{
|
||||
nullable: true,
|
||||
|
@ -667,7 +737,7 @@ defp status_source_response do
|
|||
spoiler_text: %Schema{
|
||||
type: :string,
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -346,6 +346,42 @@ def post(user, %{status: _} = data) do
|
|||
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()}
|
||||
def pin(id, %User{} = user) do
|
||||
with %Activity{} = activity <- create_activity_by_id(id),
|
||||
|
|
|
@ -222,7 +222,26 @@ def show_source(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
|
|||
end
|
||||
|
||||
@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
|
||||
|
||||
@doc "GET /api/v1/statuses/:id"
|
||||
|
|
|
@ -1315,4 +1315,33 @@ test "unreact_with_emoji" do
|
|||
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
|
||||
|
|
|
@ -2103,7 +2103,84 @@ test "it returns the source", %{conn: conn} do
|
|||
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue