diff --git a/CHANGELOG.md b/CHANGELOG.md index 52d92c6d2..155248a81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - HTTPSecurityPlug now sends a response header to opt out of Google's FLoC (Federated Learning of Cohorts) targeted advertising. - Email address is now returned if requesting user is the owner of the user account so it can be exposed in client and FE user settings UIs. - Improved Twittercard and OpenGraph meta tag generation including thumbnails and image dimension metadata when available. +- AdminAPI: sort users so the newest are at the top. +- ActivityPub Client-to-Server(C2S): Limitation on the type of Activity/Object are lifted as they are now passed through ObjectValidators ### Added - MRF (`FollowBotPolicy`): New MRF Policy which makes a designated local Bot account attempt to follow all users in public Notes received by your instance. Users who require approving follower requests or have #nobot in their profile are excluded. - Return OAuth token `id` (primary key) in POST `/oauth/token`. +- AdminAPI: return `created_at` date with users. - `AnalyzeMetadata` upload filter for extracting image/video attachment dimensions and generating blurhashes for images. Blurhashes for videos are not generated at this time. - Attachment dimensions and blurhashes are federated when available. - Pinned posts federation @@ -26,7 +29,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Don't crash so hard when email settings are invalid. - Checking activated Upload Filters for required commands. +- Remote users can no longer reappear after being deleted. +- Deactivated users may now be deleted. - Mix task `pleroma.database prune_objects` +- Linkify: Parsing crash with URLs ending in unbalanced closed paren, no path separator, and no query parameters ### Removed - **Breaking**: Remove deprecated `/api/qvitter/statuses/notifications/read` (replaced by `/api/v1/pleroma/notifications/read`) diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 7e36c1b53..6a991c48e 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -292,7 +292,8 @@ def get_in_reply_to_activity(%Activity{} = activity) do get_in_reply_to_activity_from_object(Object.normalize(activity, fetch: false)) end - def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"]) + def normalize(%Activity{data: %{"id" => ap_id}}), do: get_by_ap_id_with_object(ap_id) + def normalize(%{"id" => ap_id}), do: get_by_ap_id_with_object(ap_id) def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id) def normalize(_), do: nil diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 9365fae2b..62506f37a 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1695,8 +1695,6 @@ def purge_user_changeset(user) do email: nil, name: nil, password_hash: nil, - keys: nil, - public_key: nil, avatar: %{}, tags: [], last_refreshed_at: nil, @@ -1707,9 +1705,7 @@ def purge_user_changeset(user) do follower_count: 0, following_count: 0, is_locked: false, - is_confirmed: true, password_reset_pending: false, - is_approved: true, registration_reason: nil, confirmation_token: nil, domain_blocks: [], @@ -1725,45 +1721,53 @@ def purge_user_changeset(user) do raw_fields: [], is_discoverable: false, also_known_as: [] + # id: preserved + # ap_id: preserved + # nickname: preserved }) end + # Purge doesn't delete the user from the database. + # It just nulls all its fields and deactivates it. + # See `User.purge_user_changeset/1` above. + defp purge(%User{} = user) do + user + |> purge_user_changeset() + |> update_and_set_cache() + end + def delete(users) when is_list(users) do for user <- users, do: delete(user) end def delete(%User{} = user) do + # Purge the user immediately + purge(user) BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id}) end - defp delete_and_invalidate_cache(%User{} = user) do + # *Actually* delete the user from the DB + defp delete_from_db(%User{} = user) do invalidate_cache(user) Repo.delete(user) end - defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user) + # If the user never finalized their account, it's safe to delete them. + defp maybe_delete_from_db(%User{local: true, is_confirmed: false} = user), + do: delete_from_db(user) - defp delete_or_deactivate(%User{local: true} = user) do - status = account_status(user) + defp maybe_delete_from_db(%User{local: true, is_approved: false} = user), + do: delete_from_db(user) - case status do - :confirmation_pending -> - delete_and_invalidate_cache(user) - - :approval_pending -> - delete_and_invalidate_cache(user) - - _ -> - user - |> purge_user_changeset() - |> update_and_set_cache() - end - end + defp maybe_delete_from_db(user), do: {:ok, user} def perform(:force_password_reset, user), do: force_password_reset(user) @spec perform(atom(), User.t()) :: {:ok, User.t()} def perform(:delete, %User{} = user) do + # Purge the user again, in case perform/2 is called directly + purge(user) + # Remove all relationships user |> get_followers() @@ -1781,10 +1785,9 @@ def perform(:delete, %User{} = user) do delete_user_activities(user) delete_notifications_from_user_activities(user) - delete_outgoing_pending_follow_requests(user) - delete_or_deactivate(user) + maybe_delete_from_db(user) end def perform(:set_activation_async, user, status), do: set_activation(user, status) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 18368943d..4c29dda35 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -53,15 +53,18 @@ defp get_recipients(data) do {recipients, to, cc} end - defp check_actor_is_active(nil), do: true + defp check_actor_can_insert(%{"type" => "Delete"}), do: true + defp check_actor_can_insert(%{"type" => "Undo"}), do: true - defp check_actor_is_active(actor) when is_binary(actor) do + defp check_actor_can_insert(%{"actor" => actor}) when is_binary(actor) do case User.get_cached_by_ap_id(actor) do %User{is_active: true} -> true _ -> false end end + defp check_actor_can_insert(_), do: true + defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do limit = Config.get([:instance, :remote_limit]) String.length(content) <= limit @@ -88,7 +91,7 @@ defp increase_replies_count_if_reply(%{ defp increase_replies_count_if_reply(_create_data), do: :noop - @object_types ~w[ChatMessage Question Answer Audio Video Event Article Note] + @object_types ~w[ChatMessage Question Answer Audio Video Event Article Note Page] @impl true def persist(%{"type" => type} = object, meta) when type in @object_types do with {:ok, object} <- Object.create(object) do @@ -117,7 +120,7 @@ def persist(object, meta) do def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do with nil <- Activity.normalize(map), map <- lazy_put_activity_defaults(map, fake), - {_, true} <- {:actor_check, bypass_actor_check || check_actor_is_active(map["actor"])}, + {_, true} <- {:actor_check, bypass_actor_check || check_actor_can_insert(map)}, {_, true} <- {:remote_limit_pass, check_remote_limit(map)}, {:ok, map} <- MRF.filter(map), {recipients, _, _} = get_recipients(map), diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 5aa3b281a..57ac40b42 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -11,7 +11,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Object.Fetcher alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.Pipeline @@ -403,83 +402,90 @@ def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{ |> json(err) end - defp handle_user_activity( - %User{} = user, - %{"type" => "Create", "object" => %{"type" => "Note"} = object} = params - ) do - content = if is_binary(object["content"]), do: object["content"], else: "" - name = if is_binary(object["name"]), do: object["name"], else: "" - summary = if is_binary(object["summary"]), do: object["summary"], else: "" - length = String.length(content <> name <> summary) + defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity) + when is_map(object) do + length = + [object["content"], object["summary"], object["name"]] + |> Enum.filter(&is_binary(&1)) + |> Enum.join("") + |> String.length() - if length > Pleroma.Config.get([:instance, :limit]) do - {:error, dgettext("errors", "Note is over the character limit")} - else + limit = Pleroma.Config.get([:instance, :limit]) + + if length < limit do object = object - |> Map.merge(Map.take(params, ["to", "cc"])) - |> Map.put("attributedTo", user.ap_id) - |> Transmogrifier.fix_object() + |> Transmogrifier.strip_internal_fields() + |> Map.put("attributedTo", actor) + |> Map.put("actor", actor) + |> Map.put("id", Utils.generate_object_id()) - ActivityPub.create(%{ - to: params["to"], - actor: user, - context: object["context"], - object: object, - additional: Map.take(params, ["cc"]) - }) - end - end - - defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do - with %Object{} = object <- Object.normalize(params["object"], fetch: false), - true <- user.is_moderator || user.ap_id == object.data["actor"], - {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]), - {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do - {:ok, delete} + {:ok, Map.put(activity, "object", object)} else - _ -> {:error, dgettext("errors", "Can't delete object")} + {:error, + dgettext( + "errors", + "Character limit (%{limit} characters) exceeded, contains %{length} characters", + limit: limit, + length: length + )} end end - defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do - with %Object{} = object <- Object.normalize(params["object"], fetch: false), - {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)}, - {_, {:ok, %Activity{} = activity, _meta}} <- - {:common_pipeline, - Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do + defp fix_user_message( + %User{ap_id: actor} = user, + %{"type" => "Delete", "object" => object} = activity + ) do + with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)}, + {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do {:ok, activity} else - _ -> {:error, dgettext("errors", "Can't like object")} + {:normalize, _} -> + {:error, "No such object found"} + + {:permission, _} -> + {:forbidden, "You can't delete this object"} end end - defp handle_user_activity(_, _) do - {:error, dgettext("errors", "Unhandled activity type")} + defp fix_user_message(%User{}, activity) do + {:ok, activity} end def update_outbox( - %{assigns: %{user: %User{nickname: nickname} = user}} = conn, + %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn, %{"nickname" => nickname} = params ) do - actor = user.ap_id - params = params - |> Map.drop(["id"]) + |> Map.drop(["nickname"]) + |> Map.put("id", Utils.generate_activity_id()) |> Map.put("actor", actor) - |> Transmogrifier.fix_addressing() - with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do + with {:ok, params} <- fix_user_message(user, params), + {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true), + %Activity{data: activity_data} <- Activity.normalize(activity) do conn |> put_status(:created) - |> put_resp_header("location", activity.data["id"]) - |> json(activity.data) + |> put_resp_header("location", activity_data["id"]) + |> json(activity_data) else + {:forbidden, message} -> + conn + |> put_status(:forbidden) + |> json(message) + {:error, message} -> conn |> put_status(:bad_request) |> json(message) + + e -> + Logger.warn(fn -> "AP C2S: #{inspect(e)}" end) + + conn + |> put_status(:bad_request) + |> json("Bad Request") end end diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 248a12a36..6e40d8b72 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -20,7 +20,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator @@ -102,7 +102,7 @@ def validate( %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity, meta ) - when objtype in ~w[Question Answer Audio Video Event Article Note] do + when objtype in ~w[Question Answer Audio Video Event Article Note Page] do with {:ok, object_data} <- cast_and_apply(object), meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), {:ok, create_activity} <- @@ -115,15 +115,16 @@ def validate( end def validate(%{"type" => type} = object, meta) - when type in ~w[Event Question Audio Video Article Note] do + when type in ~w[Event Question Audio Video Article Note Page] do validator = case type do "Event" -> EventValidator "Question" -> QuestionValidator "Audio" -> AudioVideoValidator "Video" -> AudioVideoValidator - "Article" -> ArticleNoteValidator - "Note" -> ArticleNoteValidator + "Article" -> ArticleNotePageValidator + "Note" -> ArticleNotePageValidator + "Page" -> ArticleNotePageValidator end with {:ok, object} <- @@ -175,6 +176,8 @@ def validate(%{"type" => type} = object, meta) when type in ~w(Add Remove) do end end + def validate(o, m), do: {:error, {:validator_not_set, {o, m}}} + def cast_and_apply(%{"type" => "ChatMessage"} = object) do ChatMessageValidator.cast_and_apply(object) end @@ -195,8 +198,8 @@ def cast_and_apply(%{"type" => "Event"} = object) do EventValidator.cast_and_apply(object) end - def cast_and_apply(%{"type" => type} = object) when type in ~w[Article Note] do - ArticleNoteValidator.cast_and_apply(object) + def cast_and_apply(%{"type" => type} = object) when type in ~w[Article Note Page] do + ArticleNotePageValidator.cast_and_apply(object) end def cast_and_apply(o), do: {:error, {:validator_not_set, o}} diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex similarity index 96% rename from lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex rename to lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index 193f85f49..0d987116c 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2021 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do use Ecto.Schema alias Pleroma.EctoType.ActivityPub.ObjectValidators @@ -113,7 +113,7 @@ def changeset(struct, data) do defp validate_data(data_cng) do data_cng - |> validate_inclusion(:type, ["Article", "Note"]) + |> validate_inclusion(:type, ["Article", "Note", "Page"]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) |> CommonValidations.validate_any_presence([:cc, :to]) |> CommonValidations.validate_fields_match([:actor, :attributedTo]) diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex index 7da67bf16..05f93da82 100644 --- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do alias Pleroma.Activity alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.User import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -57,7 +58,7 @@ defp validate_data(cng) do cng |> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_inclusion(:type, ["Delete"]) - |> validate_actor_presence() + |> validate_delete_actor(:actor) |> validate_modification_rights() |> validate_object_or_user_presence(allowed_types: @deletable_types) |> add_deleted_activity_id() @@ -72,4 +73,13 @@ def cast_and_validate(data) do |> cast_data |> validate_data end + + defp validate_delete_actor(cng, field_name) do + validate_change(cng, field_name, fn field_name, actor -> + case User.get_cached_by_ap_id(actor) do + %User{} -> [] + _ -> [{field_name, "can't find user"}] + end + end) + end end diff --git a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex index e8af60ffa..6ff648c84 100644 --- a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do alias Pleroma.Activity alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.User import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -42,7 +43,7 @@ defp validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["Undo"]) |> validate_required([:id, :type, :object, :actor, :to, :cc]) - |> validate_actor_presence() + |> validate_undo_actor(:actor) |> validate_object_presence() |> validate_undo_rights() end @@ -59,4 +60,13 @@ def validate_undo_rights(cng) do _ -> cng end end + + defp validate_undo_actor(cng, field_name) do + validate_change(cng, field_name, fn field_name, actor -> + case User.get_cached_by_ap_id(actor) do + %User{} -> [] + _ -> [{field_name, "can't find user"}] + end + end) + end end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 1eca1cb31..b0ec84ade 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -437,7 +437,7 @@ def handle_object_creation(%{"type" => "Answer"} = object_map, meta) do end def handle_object_creation(%{"type" => objtype} = object, meta) - when objtype in ~w[Audio Video Question Event Article Note] do + when objtype in ~w[Audio Video Question Event Article Note Page] do with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do {:ok, object, meta} end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 51c0cc860..142af1a13 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -353,29 +353,6 @@ defp get_reported(objects) do end) end - # Compatibility wrapper for Mastodon votes - defp handle_create(%{"object" => %{"type" => "Answer"}} = data, _user) do - handle_incoming(data) - end - - defp handle_create(%{"object" => object} = data, user) do - %{ - to: data["to"], - object: object, - actor: user, - context: object["context"], - local: false, - published: data["published"], - additional: - Map.take(data, [ - "cc", - "directMessage", - "id" - ]) - } - |> ActivityPub.create() - end - def handle_incoming(data, options \\ []) # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them @@ -407,43 +384,6 @@ def handle_incoming(%{"id" => ""}, _options), do: :error def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8, do: :error - # TODO: validate those with a Ecto scheme - # - tags - # - emoji - def handle_incoming( - %{"type" => "Create", "object" => %{"type" => "Page"} = object} = data, - options - ) do - actor = Containment.get_actor(data) - - with nil <- Activity.get_create_by_object_ap_id(object["id"]), - {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor) do - data = - data - |> Map.put("object", fix_object(object, options)) - |> Map.put("actor", actor) - |> fix_addressing() - - with {:ok, created_activity} <- handle_create(data, user) do - reply_depth = (options[:depth] || 0) + 1 - - if Federator.allowed_thread_distance?(reply_depth) do - for reply_id <- replies(object) do - Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{ - "id" => reply_id, - "depth" => reply_depth - }) - end - end - - {:ok, created_activity} - end - else - %Activity{} = activity -> {:ok, activity} - _e -> :error - end - end - def handle_incoming( %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data, options @@ -507,7 +447,7 @@ def handle_incoming( %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, options ) - when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note} do + when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page} do fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1) object = diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex index 01d974479..da38fab56 100644 --- a/lib/pleroma/web/admin_api/search.ex +++ b/lib/pleroma/web/admin_api/search.ex @@ -17,7 +17,7 @@ def user(params \\ %{}) do |> Map.drop([:page, :page_size]) |> Map.put(:invisible, false) |> User.Query.build() - |> order_by([u], u.nickname) + |> order_by(desc: :id) paginated_query = User.Query.paginate(query, params[:page] || 1, params[:page_size] || @page_size) diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index e053a9b67..fae0c07f0 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.AdminAPI.AccountView do alias Pleroma.User alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI alias Pleroma.Web.MediaProxy @@ -81,7 +82,8 @@ def render("show.json", %{user: user}) do "is_approved" => user.is_approved, "url" => user.uri || user.ap_id, "registration_reason" => user.registration_reason, - "actor_type" => user.actor_type + "actor_type" => user.actor_type, + "created_at" => CommonAPI.Utils.to_masto_date(user.inserted_at) } end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 4b49b74ca..10c279893 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -193,7 +193,9 @@ def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do |> ActivityPub.fetch_activities_bounded(following, params) |> Enum.reverse() - render(conn, "index.json", + conn + |> add_link_headers(activities) + |> render("index.json", activities: activities, for: user, as: :activity, diff --git a/mix.exs b/mix.exs index e4b160971..1a7aac6a4 100644 --- a/mix.exs +++ b/mix.exs @@ -157,7 +157,7 @@ defp deps do {:floki, "~> 0.27"}, {:timex, "~> 3.6"}, {:ueberauth, "~> 0.4"}, - {:linkify, "~> 0.5.0"}, + {:linkify, "~> 0.5.1"}, {:http_signatures, "~> 0.1.0"}, {:telemetry, "~> 0.3"}, {:poolboy, "~> 1.5"}, diff --git a/mix.lock b/mix.lock index 65a225504..b78ae0bc9 100644 --- a/mix.lock +++ b/mix.lock @@ -67,7 +67,7 @@ "jose": {:hex, :jose, "1.11.1", "59da64010c69aad6cde2f5b9248b896b84472e99bd18f246085b7b9fe435dcdb", [:mix, :rebar3], [], "hexpm", "078f6c9fb3cd2f4cfafc972c814261a7d1e8d2b3685c0a76eb87e158efff1ac5"}, "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"}, - "linkify": {:hex, :linkify, "0.5.0", "e0ea8de73ff44742d6a889721221f4c4eccaad5284957ee9832ffeb347602d54", [:mix], [], "hexpm", "4ccd958350aee7c51c89e21f05b15d30596ebbba707e051d21766be1809df2d7"}, + "linkify": {:hex, :linkify, "0.5.1", "6dc415cbc948b2f6ecec7cb226aab7ba9d3a1815bb501ae33e042334d707ecee", [:mix], [], "hexpm", "a3128c7e22fada4aa7214009501d8131e1fa3faf2f0a68b33dba379dc84ff944"}, "majic": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/majic.git", "289cda1b6d0d70ccb2ba508a2b0bd24638db2880", [ref: "289cda1b6d0d70ccb2ba508a2b0bd24638db2880"]}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, diff --git a/test/fixtures/tesla_mock/lemmy-page.json b/test/fixtures/tesla_mock/lemmy-page.json new file mode 100644 index 000000000..f07097a0e --- /dev/null +++ b/test/fixtures/tesla_mock/lemmy-page.json @@ -0,0 +1,17 @@ +{ + "commentsEnabled": true, + "sensitive": false, + "stickied": false, + "attributedTo": "https://enterprise.lemmy.ml/u/nutomic", + "summary": "Hello Federation!", + "url": "https://enterprise.lemmy.ml/pictrs/image/US52d9DPvf.jpg", + "image": { + "type": "Image", + "url": "https://enterprise.lemmy.ml/pictrs/image/lwFAcXHUjS.jpg" + }, + "published": "2020-09-14T15:03:11.909105+00:00", + "to": "https://enterprise.lemmy.ml/c/main", + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://enterprise.lemmy.ml/post/3", + "type": "Page" +} diff --git a/test/fixtures/tesla_mock/lemmy-user.json b/test/fixtures/tesla_mock/lemmy-user.json new file mode 100644 index 000000000..d0e9066ac --- /dev/null +++ b/test/fixtures/tesla_mock/lemmy-user.json @@ -0,0 +1,27 @@ +{ + "publicKey": { + "id": "https://enterprise.lemmy.ml/u/nutomic#main-key", + "owner": "https://enterprise.lemmy.ml/u/nutomic", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvfwAYPxp1gOk2HcCRoUd\nupoecvmnpzRc5Gu6/N3YQyOyRsrYuiYLNQq2cgM3kcU80ZeEetkwkYgXkRJOKu/b\nBWb7i1zt2tdr5k6lUdW8dfCyjht8ooFPQdov8J3QYHfgBHyUYxuCNfSujryxx2wu\nLQcdjRQa5NIWcomSO8OXmCF5/Yhg2XWCbtnlxEq6Y+AFddr1mAlTOy5pBr5d+xZz\njLw/U3CioNJ79yGi/sJhgp6IyJqtUSoN3b4BgRIEts2QVvn44W1rQy9wCbRYQrO1\nBcB9Wel4k3rJJK8uHg+LpHVMaZppkNaWGkMBhMbzr8qmIlcNWNi7cbMK/p5vyviy\nSwIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "inbox": "https://enterprise.lemmy.ml/u/nutomic/inbox", + "preferredUsername": "Nutomic", + "endpoints": { + "sharedInbox": "https://enterprise.lemmy.ml/inbox" + }, + "summary": "some bio", + "icon": { + "type": "Image", + "url": "https://enterprise.lemmy.ml/pictrs/image/F6Z7QcWZRJ.jpg" + }, + "image": { + "type": "Image", + "url": "https://enterprise.lemmy.ml:/pictrs/image/Q79N9oCDEG.png" + }, + "published": "2020-09-14T14:54:53.080949+00:00", + "updated": "2020-10-14T10:58:28.139178+00:00", + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://enterprise.lemmy.ml/u/nutomic", + "type": "Person", + "name": "nutomic" +} diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index abc471d13..4021a565d 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -1639,9 +1639,9 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do follower_count: 9, following_count: 9001, is_locked: true, - is_confirmed: false, + is_confirmed: true, password_reset_pending: true, - is_approved: false, + is_approved: true, registration_reason: "ahhhhh", confirmation_token: "qqqq", domain_blocks: ["lain.com"], @@ -1669,8 +1669,8 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do email: nil, name: nil, password_hash: nil, - keys: nil, - public_key: nil, + keys: "RSA begin buplic key", + public_key: "--PRIVATE KEYE--", avatar: %{}, tags: [], last_refreshed_at: nil, @@ -1702,6 +1702,24 @@ test "delete/1 purges a user when they wouldn't be fully deleted" do } = user end + test "delete/1 purges a remote user" do + user = + insert(:user, %{ + name: "qqqqqqq", + avatar: %{"a" => "b"}, + banner: %{"a" => "b"}, + local: false + }) + + {:ok, job} = User.delete(user) + {:ok, _} = ObanHelpers.perform(job) + user = User.get_by_id(user.id) + + assert user.name == nil + assert user.avatar == %{} + assert user.banner == %{} + end + test "get_public_key_for_ap_id fetches a user that's not in the db" do assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin") end diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index c7039d1f8..50315e21f 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -1334,9 +1334,12 @@ test "It returns poll Answers when authenticated", %{conn: conn} do activity: %{ "@context" => "https://www.w3.org/ns/activitystreams", "type" => "Create", - "object" => %{"type" => "Note", "content" => "AP C2S test"}, - "to" => "https://www.w3.org/ns/activitystreams#Public", - "cc" => [] + "object" => %{ + "type" => "Note", + "content" => "AP C2S test", + "to" => "https://www.w3.org/ns/activitystreams#Public", + "cc" => [] + } } ] end @@ -1442,19 +1445,19 @@ test "it erects a tombstone when receiving a delete activity", %{conn: conn} do user = User.get_cached_by_ap_id(note_activity.data["actor"]) data = %{ - type: "Delete", - object: %{ - id: note_object.data["id"] + "type" => "Delete", + "object" => %{ + "id" => note_object.data["id"] } } - conn = + result = conn |> assign(:user, user) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/outbox", data) + |> json_response(201) - result = json_response(conn, 201) assert Activity.get_by_ap_id(result["id"]) assert object = Object.get_by_ap_id(note_object.data["id"]) @@ -1479,7 +1482,7 @@ test "it rejects delete activity of object from other actor", %{conn: conn} do |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/outbox", data) - assert json_response(conn, 400) + assert json_response(conn, 403) end test "it increases like count when receiving a like action", %{conn: conn} do @@ -1557,7 +1560,7 @@ test "Character limitation", %{conn: conn, activity: activity} do |> post("/users/#{user.nickname}/outbox", activity) |> json_response(400) - assert result == "Note is over the character limit" + assert result == "Character limit (5 characters) exceeded, contains 11 characters" end end @@ -1934,10 +1937,10 @@ test "POST /api/ap/upload_media", %{conn: conn} do "object" => %{ "type" => "Note", "content" => "AP C2S test, attachment", - "attachment" => [object] - }, - "to" => "https://www.w3.org/ns/activitystreams#Public", - "cc" => [] + "attachment" => [object], + "to" => "https://www.w3.org/ns/activitystreams#Public", + "cc" => [] + } } activity_response = diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs similarity index 76% rename from test/pleroma/web/activity_pub/object_validators/article_note_validator_test.exs rename to test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index e408c85c3..720c17d8d 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -2,10 +2,10 @@ # Copyright © 2017-2021 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidatorTest do +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest do use Pleroma.DataCase, async: true - alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator alias Pleroma.Web.ActivityPub.Utils import Pleroma.Factory @@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidatorTest do end test "a basic note validates", %{note: note} do - %{valid?: true} = ArticleNoteValidator.cast_and_validate(note) + %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end end end diff --git a/test/pleroma/web/activity_pub/transmogrifier/page_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/page_handling_test.exs new file mode 100644 index 000000000..4ac71e066 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/page_handling_test.exs @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.PageHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Object.Fetcher + + test "Lemmy Page" do + Tesla.Mock.mock(fn + %{url: "https://enterprise.lemmy.ml/post/3"} -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: File.read!("test/fixtures/tesla_mock/lemmy-page.json") + } + + %{url: "https://enterprise.lemmy.ml/u/nutomic"} -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: File.read!("test/fixtures/tesla_mock/lemmy-user.json") + } + end) + + {:ok, object} = Fetcher.fetch_object_from_id("https://enterprise.lemmy.ml/post/3") + + assert object.data["summary"] == "Hello Federation!" + assert object.data["published"] == "2020-09-14T15:03:11.909105Z" + + # WAT + assert object.data["url"] == "https://enterprise.lemmy.ml/pictrs/image/US52d9DPvf.jpg" + end +end diff --git a/test/pleroma/web/admin_api/controllers/user_controller_test.exs b/test/pleroma/web/admin_api/controllers/user_controller_test.exs index 1a3aa439b..d9da34f6e 100644 --- a/test/pleroma/web/admin_api/controllers/user_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/user_controller_test.exs @@ -384,24 +384,22 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do conn = get(conn, "/api/pleroma/admin/users?page=1") - users = - [ - user_response( - admin, - %{"roles" => %{"admin" => true, "moderator" => false}} - ), - user_response(user, %{"local" => false, "tags" => ["foo", "bar"]}), - user_response( - user2, - %{ - "local" => true, - "is_approved" => false, - "registration_reason" => "I'm a chill dude", - "actor_type" => "Person" - } - ) - ] - |> Enum.sort_by(& &1["nickname"]) + users = [ + user_response( + user2, + %{ + "local" => true, + "is_approved" => false, + "registration_reason" => "I'm a chill dude", + "actor_type" => "Person" + } + ), + user_response(user, %{"local" => false, "tags" => ["foo", "bar"]}), + user_response( + admin, + %{"roles" => %{"admin" => true, "moderator" => false}} + ) + ] assert json_response_and_validate_schema(conn, 200) == %{ "count" => 3, @@ -525,7 +523,7 @@ test "regular search with page size", %{conn: conn} do assert json_response_and_validate_schema(conn1, 200) == %{ "count" => 2, "page_size" => 1, - "users" => [user_response(user)] + "users" => [user_response(user2)] } conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2") @@ -533,7 +531,7 @@ test "regular search with page size", %{conn: conn} do assert json_response_and_validate_schema(conn2, 200) == %{ "count" => 2, "page_size" => 1, - "users" => [user_response(user2)] + "users" => [user_response(user)] } end @@ -565,18 +563,16 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do conn = get(conn, "/api/pleroma/admin/users?filters=local") - users = - [ - user_response(user), - user_response(admin, %{ - "roles" => %{"admin" => true, "moderator" => false} - }), - user_response(old_admin, %{ - "is_active" => true, - "roles" => %{"admin" => true, "moderator" => false} - }) - ] - |> Enum.sort_by(& &1["nickname"]) + users = [ + user_response(user), + user_response(admin, %{ + "roles" => %{"admin" => true, "moderator" => false} + }), + user_response(old_admin, %{ + "is_active" => true, + "roles" => %{"admin" => true, "moderator" => false} + }) + ] assert json_response_and_validate_schema(conn, 200) == %{ "count" => 3, @@ -604,7 +600,6 @@ test "only unconfirmed users", %{conn: conn} do "is_approved" => true }) end) - |> Enum.sort_by(& &1["nickname"]) assert result == %{"count" => 2, "page_size" => 50, "users" => users} end @@ -642,18 +637,16 @@ test "load only admins", %{conn: conn, admin: admin} do conn = get(conn, "/api/pleroma/admin/users?filters=is_admin") - users = - [ - user_response(admin, %{ - "is_active" => true, - "roles" => %{"admin" => true, "moderator" => false} - }), - user_response(second_admin, %{ - "is_active" => true, - "roles" => %{"admin" => true, "moderator" => false} - }) - ] - |> Enum.sort_by(& &1["nickname"]) + users = [ + user_response(second_admin, %{ + "is_active" => true, + "roles" => %{"admin" => true, "moderator" => false} + }), + user_response(admin, %{ + "is_active" => true, + "roles" => %{"admin" => true, "moderator" => false} + }) + ] assert json_response_and_validate_schema(conn, 200) == %{ "count" => 2, @@ -693,13 +686,11 @@ test "load users with actor_type is Person", %{admin: admin, conn: conn} do |> get(user_path(conn, :index), %{actor_types: ["Person"]}) |> json_response_and_validate_schema(200) - users = - [ - user_response(admin, %{"roles" => %{"admin" => true, "moderator" => false}}), - user_response(user1), - user_response(user2) - ] - |> Enum.sort_by(& &1["nickname"]) + users = [ + user_response(user2), + user_response(user1), + user_response(admin, %{"roles" => %{"admin" => true, "moderator" => false}}) + ] assert response == %{"count" => 3, "page_size" => 50, "users" => users} end @@ -716,14 +707,12 @@ test "load users with actor_type is Person and Service", %{admin: admin, conn: c |> get(user_path(conn, :index), %{actor_types: ["Person", "Service"]}) |> json_response_and_validate_schema(200) - users = - [ - user_response(admin, %{"roles" => %{"admin" => true, "moderator" => false}}), - user_response(user1), - user_response(user2), - user_response(user_service, %{"actor_type" => "Service"}) - ] - |> Enum.sort_by(& &1["nickname"]) + users = [ + user_response(user2), + user_response(user1), + user_response(user_service, %{"actor_type" => "Service"}), + user_response(admin, %{"roles" => %{"admin" => true, "moderator" => false}}) + ] assert response == %{"count" => 4, "page_size" => 50, "users" => users} end @@ -752,12 +741,10 @@ test "load users with tags list", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users?tags[]=first&tags[]=second") - users = - [ - user_response(user1, %{"tags" => ["first"]}), - user_response(user2, %{"tags" => ["second"]}) - ] - |> Enum.sort_by(& &1["nickname"]) + users = [ + user_response(user2, %{"tags" => ["second"]}), + user_response(user1, %{"tags" => ["first"]}) + ] assert json_response_and_validate_schema(conn, 200) == %{ "count" => 2, @@ -781,8 +768,8 @@ test "`active` filters out users pending approval", %{token: token} do "count" => 2, "page_size" => 50, "users" => [ - %{"id" => ^admin_id}, - %{"id" => ^user_id} + %{"id" => ^user_id}, + %{"id" => ^admin_id} ] } = json_response_and_validate_schema(conn, 200) end @@ -921,7 +908,8 @@ defp user_response(user, attrs \\ %{}) do "is_approved" => true, "url" => user.ap_id, "registration_reason" => nil, - "actor_type" => "Person" + "actor_type" => "Person", + "created_at" => CommonAPI.Utils.to_masto_date(user.inserted_at) } |> Map.merge(attrs) end diff --git a/test/pleroma/web/admin_api/search_test.exs b/test/pleroma/web/admin_api/search_test.exs index b8eeec65b..2335c5228 100644 --- a/test/pleroma/web/admin_api/search_test.exs +++ b/test/pleroma/web/admin_api/search_test.exs @@ -151,9 +151,9 @@ test "it returns users by actor_types" do {:ok, [^user_service], 1} = Search.user(%{actor_types: ["Service"]}) {:ok, [^user_application], 1} = Search.user(%{actor_types: ["Application"]}) - {:ok, [^user1, ^user2], 2} = Search.user(%{actor_types: ["Person"]}) + {:ok, [^user2, ^user1], 2} = Search.user(%{actor_types: ["Person"]}) - {:ok, [^user_service, ^user1, ^user2], 3} = + {:ok, [^user2, ^user1, ^user_service], 3} = Search.user(%{actor_types: ["Person", "Service"]}) end