diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 350ff6cb3..2833911f1 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -34,9 +34,15 @@ defmodule Pleroma.Upload do """ alias Ecto.UUID + alias Pleroma.Repo + alias Pleroma.Object alias Pleroma.Maps + alias Pleroma.Pagination alias Pleroma.Utils.URIEncoding alias Pleroma.Web.ActivityPub.Utils + + import Ecto.Query + require Logger @type source :: @@ -301,4 +307,38 @@ defmodule Pleroma.Upload do public_endpoint || upload_base_url end end + + def list_duplicate_attachments(%{"id" => id, "url" => [%{"href" => href}]}) do + clean_href = %URI{ URI.parse(href) | query: nil } |> URI.to_string() + + same_attachment_count = + from( + o in Object, + where: fragment("STARTS_WITH((?)->'url'->0->>'href', (?))", o.data, ^clean_href) and fragment("(?)->>'id' <> (?)", o.data, ^id), + ) |> Repo.all() + end + + def list_objects_with_attachment(href) do + clean_href = %URI{ URI.parse(href) | query: nil } |> URI.to_string() + + # Ugly as sin, but I could not figure out a better way to do this in ecto + # This tries to find any objects with this attachment + # This DOES NOT handle chat messages and SKIPS them, because they have attachments as an object instead of an array + Repo.query!( + """ + SELECT * FROM + (SELECT objects.data->>'id' AS id, jsonb_array_elements(objects.data->'attachment') as json_attachments FROM objects WHERE jsonb_typeof(objects.data->'attachment') = 'array') + WHERE STARTS_WITH(json_attachments->'url'->0->>'href', $1) + """, + [clean_href] + ).rows |> Enum.map(&List.first(&1)) + end + + def list_uploaded_by(user, offset, limit) do + from(obj in Object, + where: fragment("(?)->>'type' = ?", obj.data, "Document") and fragment("(?)->>'actor' = ?", obj.data, ^user.ap_id), + order_by: [desc: obj.id] + ) + |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => limit}, :offset) + end end diff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex index 588b42e06..2b5ec2585 100644 --- a/lib/pleroma/web/api_spec/operations/media_operation.ex +++ b/lib/pleroma/web/api_spec/operations/media_operation.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.Attachment + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -73,6 +74,20 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do } end + def delete_operation do + %Operation{ + tags: ["Media attachments"], + summary: "Delete attachment", + description: "Deletes an attachment.", + operationId: "MediaController.delete", + security: [%{"oAuth" => ["write:media"]}], + parameters: [id_param()], + responses: %{ + 200 => Operation.response("Media", "application/json", Attachment), + } + } + end + defp update_request do %Schema{ title: "MediaUpdateRequest", @@ -101,7 +116,15 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do tags: ["Media attachments"], summary: "Attachment", operationId: "MediaController.show", - parameters: [id_param()], + parameters: [ + id_param(), + Operation.parameter( + :attachment_usage, + :query, + %Schema{type: :boolean, default: false}, + "Whether to include known usages of attachment" + ) + ], security: [%{"oAuth" => ["read:media"]}], responses: %{ 200 => Operation.response("Media", "application/json", Attachment), diff --git a/lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex index 4bb4f112c..7e1b9c338 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex @@ -393,4 +393,28 @@ defmodule Pleroma.Web.ApiSpec.PleromaUtilOperation do } } end + + def uploaded_media_operation do + %Operation{ + tags: ["Media"], + summary: "List uploaded media", + security: [%{"oAuth" => ["read:accounts"]}], + operationId: "PleromaAPI.UtilController.uploaded_media", + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{ + aliases: %Schema{ + type: :array, + items: %Schema{type: :string}, + example: ["foo@example.org"] + } + } + }), + 400 => Operation.response("Error", "application/json", ApiError), + 403 => Operation.response("Error", "application/json", ApiError) + } + } + end end diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 41056d389..8dd9a62b7 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do alias Pleroma.Object alias Pleroma.User + alias Pleroma.Repo alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.Plugs.OAuthScopesPlug @@ -78,13 +79,51 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do end end + def delete( + %{ + assigns: %{user: user}, + private: %{open_api_spex: %{params: %{id: id}}} + } = conn, + _ + ) do + with %Object{} = object <- Object.get_by_id(id), + :ok <- Object.authorize_access(object, user), + {:ok, _, _} <- Object.delete(object) do + + conn + |> put_resp_header("content-type", "text/plain") + |> send_resp(200, "") + end + end + def update(conn, data), do: show(conn, data) @doc "GET /api/v1/media/:id" - def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do + def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id, attachment_usage: attachment_usage}}}} = conn, _) do with %Object{data: data, id: object_id} = object <- Object.get_by_id(id), :ok <- Object.authorize_access(object, user) do + attachment_data = Map.put(data, "id", object_id) + attachment_data = + if attachment_usage do + href = data["url"] |> List.first |> Map.get("href") + used_in_objects = Pleroma.Upload.list_objects_with_attachment(href) + + attachment_duplicates = + Pleroma.Upload.list_duplicate_attachments(object.data) + |> Enum.group_by(& if &1.data["actor"] == user.ap_id, do: :own, else: :other) + |> Map.new(fn {key, val} -> {key, length(val)} end) + + put_in( + attachment_data["usage"], + %{ + objects: used_in_objects, + duplicates: attachment_duplicates + } + ) + else + attachment_data + end render(conn, "attachment.json", %{attachment: attachment_data}) end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 42cbc1590..b15749aa0 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -658,6 +658,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do pleroma = %{mime_type: media_type} |> Maps.put_if_present(:name, name) + |> Maps.put_if_present(:usage, attachment["usage"]) %{ id: attachment_id, diff --git a/lib/pleroma/web/pleroma_api/controllers/util_controller.ex b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex index 882e9476e..c453e60b7 100644 --- a/lib/pleroma/web/pleroma_api/controllers/util_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.PleromaAPI.UtilController do alias Pleroma.Emoji alias Pleroma.Healthcheck alias Pleroma.User + alias Pleroma.Pagination alias Pleroma.Utils.URIEncoding alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator @@ -38,7 +39,8 @@ defmodule Pleroma.Web.PleromaAPI.UtilController do OAuthScopesPlug, %{scopes: ["read:accounts"]} when action in [ - :list_aliases + :list_aliases, + :uploaded_media ] ) @@ -274,4 +276,19 @@ defmodule Pleroma.Web.PleromaAPI.UtilController do |> put_status(:service_unavailable) |> json(info) end + + def uploaded_media( + %{assigns: %{user: user}} = conn, + _ + ) do + + result = + Pleroma.Upload.list_uploaded_by(user, 0, 50) + |> Enum.map( + fn %Pleroma.Object{ id: id, data: %{ "url" => [ mediaUrl ], "name" => name }, inserted_at: uploaded } -> + Map.merge(%{ id: id, uploaded: uploaded, name: name }, mediaUrl) |> Map.drop(["type"]) end + ) + + json(conn, result) + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 8cae63ae6..3d9994c7f 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -583,6 +583,8 @@ defmodule Pleroma.Web.Router do put("/aliases", UtilController, :add_alias) get("/aliases", UtilController, :list_aliases) delete("/aliases", UtilController, :delete_alias) + + get("/uploaded_media", UtilController, :uploaded_media) end scope "/api/pleroma", Pleroma.Web.PleromaAPI do @@ -803,6 +805,7 @@ defmodule Pleroma.Web.Router do post("/media", MediaController, :create) get("/media/:id", MediaController, :show) put("/media/:id", MediaController, :update) + delete("/media/:id", MediaController, :delete) get("/notifications", NotificationController, :index) get("/notifications/:id", NotificationController, :show) diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex index e2f92b1fd..441e1fcea 100644 --- a/lib/pleroma/workers/attachments_cleanup_worker.ex +++ b/lib/pleroma/workers/attachments_cleanup_worker.ex @@ -17,6 +17,7 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do "object" => %{"data" => %{"attachment" => [_ | _] = attachments, "actor" => actor}} } }) do + if Pleroma.Config.get([:instance, :cleanup_attachments], false) do attachments |> Enum.flat_map(fn item -> Enum.map(item["url"], & &1["href"]) end) @@ -29,6 +30,41 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do {:ok, :success} end + def perform(%Job{ + args: %{ + "op" => "cleanup_attachments", + "object" => %{"data" => %{ "type" => "Document", "url" => [%{"href" => href}]}} = object_data + } + }) do + + if Pleroma.Config.get([:instance, :cleanup_attachments], false) do + clean_href = %URI{ URI.parse(href) | query: nil } |> URI.to_string() + + same_attachment_count = Pleroma.Upload.list_duplicate_attachments(object_data) + + # No copies of this attachment in the database + if same_attachment_count == 0 do + attachment_post_ids = Pleroma.Upload.list_objects_with_attachment(clean_href) + # No posts with this attachment, either + if attachment_post_ids == [] do + base_url = + String.trim_trailing( + Pleroma.Upload.base_url(), + "/" + ) + + uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) + + clean_href + |> String.trim_leading("#{base_url}") + |> uploader.delete_file() + end + end + end + + {:ok, :success} + end + def perform(%Job{args: %{"op" => "cleanup_attachments", "object" => _object}}), do: {:ok, :skip} @impl true @@ -61,6 +97,8 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do # we should delete 1 object for any given attachment, but don't delete # files if there are more than 1 object for it defp filter_objects(objects) do + IO.inspect(objects) + Enum.reduce(objects, {[], []}, fn {href, %{id: id, count: count}}, {ids, hrefs} -> with 1 <- count do {ids ++ [id], hrefs ++ [href]}