aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/tasks/pleroma/config.ex9
-rw-r--r--lib/mix/tasks/pleroma/database.ex3
-rw-r--r--lib/mix/tasks/pleroma/instance.ex44
-rw-r--r--lib/mix/tasks/pleroma/user.ex32
-rw-r--r--lib/pleroma/activity.ex5
-rw-r--r--lib/pleroma/activity/html.ex36
-rw-r--r--lib/pleroma/activity/ir/topics.ex31
-rw-r--r--lib/pleroma/activity/queries.ex6
-rw-r--r--lib/pleroma/activity/search.ex16
-rw-r--r--lib/pleroma/announcement.ex160
-rw-r--r--lib/pleroma/announcement_read_relationship.ex55
-rw-r--r--lib/pleroma/application.ex16
-rw-r--r--lib/pleroma/application_requirements.ex3
-rw-r--r--lib/pleroma/bbs/handler.ex105
-rw-r--r--lib/pleroma/config/deprecation_warnings.ex40
-rw-r--r--lib/pleroma/config/loader.ex15
-rw-r--r--lib/pleroma/constants.ex42
-rw-r--r--lib/pleroma/data_migration.ex1
-rw-r--r--lib/pleroma/docs/translator.ex10
-rw-r--r--lib/pleroma/docs/translator/compiler.ex119
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/mime.ex25
-rw-r--r--lib/pleroma/emoji-test.txt125
-rw-r--r--lib/pleroma/emoji.ex14
-rw-r--r--lib/pleroma/emoji/combinations.ex45
-rw-r--r--lib/pleroma/following_relationship.ex3
-rw-r--r--lib/pleroma/migrators/context_objects_deletion_migrator.ex139
-rw-r--r--lib/pleroma/migrators/hashtags_table_migrator.ex2
-rw-r--r--lib/pleroma/notification.ex40
-rw-r--r--lib/pleroma/object.ex22
-rw-r--r--lib/pleroma/object/fetcher.ex34
-rw-r--r--lib/pleroma/object/updater.ex240
-rw-r--r--lib/pleroma/signature.ex28
-rw-r--r--lib/pleroma/upload.ex29
-rw-r--r--lib/pleroma/upload/filter/exiftool/read_description.ex49
-rw-r--r--lib/pleroma/upload/filter/exiftool/strip_location.ex (renamed from lib/pleroma/upload/filter/exiftool.ex)2
-rw-r--r--lib/pleroma/user.ex125
-rw-r--r--lib/pleroma/user/backup.ex18
-rw-r--r--lib/pleroma/user_relationship.ex44
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex42
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex52
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex13
-rw-r--r--lib/pleroma/web/activity_pub/mrf.ex45
-rw-r--r--lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex3
-rw-r--r--lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex6
-rw-r--r--lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex7
-rw-r--r--lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex47
-rw-r--r--lib/pleroma/web/activity_pub/mrf/keyword_policy.ex57
-rw-r--r--lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex9
-rw-r--r--lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex15
-rw-r--r--lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex7
-rw-r--r--lib/pleroma/web/activity_pub/mrf/normalize_markup.ex6
-rw-r--r--lib/pleroma/web/activity_pub/mrf/policy.ex3
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex116
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex18
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex15
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_fields.ex3
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/common_fixes.ex7
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex18
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/event_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/question_validator.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/update_validator.ex4
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex97
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex39
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex23
-rw-r--r--lib/pleroma/web/activity_pub/views/object_view.ex4
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex2
-rw-r--r--lib/pleroma/web/activity_pub/visibility.ex5
-rw-r--r--lib/pleroma/web/admin_api/controllers/announcement_controller.ex83
-rw-r--r--lib/pleroma/web/admin_api/controllers/config_controller.ex50
-rw-r--r--lib/pleroma/web/admin_api/views/announcement_view.ex15
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex55
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/announcement_operation.ex165
-rw-r--r--lib/pleroma/web/api_spec/operations/announcement_operation.ex57
-rw-r--r--lib/pleroma/web/api_spec/operations/notification_operation.ex6
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex4
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_settings_operation.ex72
-rw-r--r--lib/pleroma/web/api_spec/operations/status_operation.ex192
-rw-r--r--lib/pleroma/web/api_spec/operations/twitter_util_operation.ex150
-rw-r--r--lib/pleroma/web/api_spec/schemas/account.ex1
-rw-r--r--lib/pleroma/web/api_spec/schemas/announcement.ex45
-rw-r--r--lib/pleroma/web/api_spec/schemas/status.ex15
-rw-r--r--lib/pleroma/web/common_api.ex35
-rw-r--r--lib/pleroma/web/common_api/activity_draft.ex5
-rw-r--r--lib/pleroma/web/common_api/utils.ex39
-rw-r--r--lib/pleroma/web/federator.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex33
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/announcement_controller.ex60
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex3
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex60
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex11
-rw-r--r--lib/pleroma/web/mastodon_api/views/announcement_view.ex15
-rw-r--r--lib/pleroma/web/mastodon_api/views/instance_view.ex5
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex15
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex173
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy_controller.ex2
-rw-r--r--lib/pleroma/web/metadata/utils.ex15
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/backup_controller.ex2
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/settings_controller.ex79
-rw-r--r--lib/pleroma/web/plugs/http_signature_plug.ex53
-rw-r--r--lib/pleroma/web/plugs/o_auth_plug.ex12
-rw-r--r--lib/pleroma/web/router.ex31
-rw-r--r--lib/pleroma/web/static_fe/static_fe_controller.ex9
-rw-r--r--lib/pleroma/web/streamer.ex18
-rw-r--r--lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex10
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex194
-rw-r--r--lib/pleroma/web/twitter_api/views/remote_follow_view.ex6
-rw-r--r--lib/pleroma/web/twitter_api/views/util_view.ex2
-rw-r--r--lib/pleroma/web/views/streamer_view.ex27
-rw-r--r--lib/pleroma/web/web_finger.ex4
-rw-r--r--lib/pleroma/workers/backup_worker.ex24
-rw-r--r--lib/pleroma/workers/receiver_worker.ex8
114 files changed, 3799 insertions, 476 deletions
diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex
index 33d147d36..3a2ea44f8 100644
--- a/lib/mix/tasks/pleroma/config.ex
+++ b/lib/mix/tasks/pleroma/config.ex
@@ -304,13 +304,8 @@ defmodule Mix.Tasks.Pleroma.Config do
System.cmd("mix", ["format", path])
end
- if Code.ensure_loaded?(Config.Reader) do
- defp config_header, do: "import Config\r\n\r\n"
- defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
- else
- defp config_header, do: "use Mix.Config\r\n\r\n"
- defp read_file(config_file), do: Mix.Config.eval!(config_file)
- end
+ defp config_header, do: "import Config\r\n\r\n"
+ defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
defp write_and_delete(config, file, delete?) do
config
diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex
index 6b8f0ef68..ed560c177 100644
--- a/lib/mix/tasks/pleroma/database.ex
+++ b/lib/mix/tasks/pleroma/database.ex
@@ -154,9 +154,8 @@ defmodule Mix.Tasks.Pleroma.Database do
|> join(:inner, [a], o in Object,
on:
fragment(
- "(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
+ "(?->>'id') = associated_object_id((?))",
o.data,
- a.data,
a.data
)
)
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index f292fc762..5c93f19ff 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -34,7 +34,8 @@ defmodule Mix.Tasks.Pleroma.Instance do
static_dir: :string,
listen_ip: :string,
listen_port: :string,
- strip_uploads: :string,
+ strip_uploads_location: :string,
+ read_uploads_description: :string,
anonymize_uploads: :string,
dedupe_uploads: :string
],
@@ -161,7 +162,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
)
|> Path.expand()
- {strip_uploads_message, strip_uploads_default} =
+ {strip_uploads_location_message, strip_uploads_location_default} =
if Pleroma.Utils.command_available?("exiftool") do
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as installed. (y/n)",
"y"}
@@ -170,12 +171,29 @@ defmodule Mix.Tasks.Pleroma.Instance do
"n"}
end
- strip_uploads =
+ strip_uploads_location =
get_option(
options,
- :strip_uploads,
- strip_uploads_message,
- strip_uploads_default
+ :strip_uploads_location,
+ strip_uploads_location_message,
+ strip_uploads_location_default
+ ) === "y"
+
+ {read_uploads_description_message, read_uploads_description_default} =
+ if Pleroma.Utils.command_available?("exiftool") do
+ {"Do you want to read data from uploaded files so clients can use it to prefill fields like image description? This requires exiftool, it was detected as installed. (y/n)",
+ "y"}
+ else
+ {"Do you want to read data from uploaded files so clients can use it to prefill fields like image description? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
+ "n"}
+ end
+
+ read_uploads_description =
+ get_option(
+ options,
+ :read_uploads_description,
+ read_uploads_description_message,
+ read_uploads_description_default
) === "y"
anonymize_uploads =
@@ -229,7 +247,8 @@ defmodule Mix.Tasks.Pleroma.Instance do
listen_port: listen_port,
upload_filters:
upload_filters(%{
- strip: strip_uploads,
+ strip_location: strip_uploads_location,
+ read_description: read_uploads_description,
anonymize: anonymize_uploads,
dedupe: dedupe_uploads
})
@@ -297,13 +316,20 @@ defmodule Mix.Tasks.Pleroma.Instance do
defp upload_filters(filters) when is_map(filters) do
enabled_filters =
- if filters.strip do
- [Pleroma.Upload.Filter.Exiftool]
+ if filters.strip_location do
+ [Pleroma.Upload.Filter.Exiftool.StripLocation]
else
[]
end
enabled_filters =
+ if filters.read_description do
+ enabled_filters ++ [Pleroma.Upload.Filter.Exiftool.ReadDescription]
+ else
+ enabled_filters
+ end
+
+ enabled_filters =
if filters.anonymize do
enabled_filters ++ [Pleroma.Upload.Filter.AnonymizeFilename]
else
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index 96d4eb90b..50ffb7f27 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -421,6 +421,38 @@ defmodule Mix.Tasks.Pleroma.User do
|> Stream.run()
end
+ def run(["fix_follow_state", local_user, remote_user]) do
+ start_pleroma()
+
+ with {:local, %User{} = local} <- {:local, User.get_by_nickname(local_user)},
+ {:remote, %User{} = remote} <- {:remote, User.get_by_nickname(remote_user)},
+ {:follow_data, %{data: %{"state" => request_state}}} <-
+ {:follow_data, Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(local, remote)} do
+ calculated_state = User.following?(local, remote)
+
+ shell_info(
+ "Request state is #{request_state}, vs calculated state of following=#{calculated_state}"
+ )
+
+ if calculated_state == false && request_state == "accept" do
+ shell_info("Discrepancy found, fixing")
+ Pleroma.Web.CommonAPI.reject_follow_request(local, remote)
+ shell_info("Relationship fixed")
+ else
+ shell_info("No discrepancy found")
+ end
+ else
+ {:local, _} ->
+ shell_error("No local user #{local_user}")
+
+ {:remote, _} ->
+ shell_error("No remote user #{remote_user}")
+
+ {:follow_data, _} ->
+ shell_error("No follow data for #{local_user} and #{remote_user}")
+ end
+ end
+
defp set_moderator(user, value) do
{:ok, user} =
user
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 12c1a3b2e..ebfd4ed45 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -53,7 +53,7 @@ defmodule Pleroma.Activity do
#
# ```
# |> join(:inner, [activity], o in Object,
- # on: fragment("(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
+ # on: fragment("(?->>'id') = associated_object_id((?))",
# o.data, activity.data, activity.data))
# |> preload([activity, object], [object: object])
# ```
@@ -69,9 +69,8 @@ defmodule Pleroma.Activity do
join(query, join_type, [activity], o in Object,
on:
fragment(
- "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
+ "(?->>'id') = associated_object_id(?)",
o.data,
- activity.data,
activity.data
),
as: :object
diff --git a/lib/pleroma/activity/html.ex b/lib/pleroma/activity/html.ex
index 071a89c8d..706b2d36c 100644
--- a/lib/pleroma/activity/html.ex
+++ b/lib/pleroma/activity/html.ex
@@ -8,6 +8,40 @@ defmodule Pleroma.Activity.HTML do
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+ # We store a list of cache keys related to an activity in a
+ # separate cache, scrubber_management_cache. It has the same
+ # size as scrubber_cache (see application.ex). Every time we add
+ # a cache to scrubber_cache, we update scrubber_management_cache.
+ #
+ # The most recent write of a certain key in the management cache
+ # is the same as the most recent write of any record related to that
+ # key in the main cache.
+ # Assuming LRW ( https://hexdocs.pm/cachex/Cachex.Policy.LRW.html ),
+ # this means when the management cache is evicted by cachex, all
+ # related records in the main cache will also have been evicted.
+
+ defp get_cache_keys_for(activity_id) do
+ with {:ok, list} when is_list(list) <- @cachex.get(:scrubber_management_cache, activity_id) do
+ list
+ else
+ _ -> []
+ end
+ end
+
+ defp add_cache_key_for(activity_id, additional_key) do
+ current = get_cache_keys_for(activity_id)
+
+ unless additional_key in current do
+ @cachex.put(:scrubber_management_cache, activity_id, [additional_key | current])
+ end
+ end
+
+ def invalidate_cache_for(activity_id) do
+ keys = get_cache_keys_for(activity_id)
+ Enum.map(keys, &@cachex.del(:scrubber_cache, &1))
+ @cachex.del(:scrubber_management_cache, activity_id)
+ end
+
def get_cached_scrubbed_html_for_activity(
content,
scrubbers,
@@ -19,6 +53,8 @@ defmodule Pleroma.Activity.HTML do
@cachex.fetch!(:scrubber_cache, key, fn _key ->
object = Object.normalize(activity, fetch: false)
+
+ add_cache_key_for(activity.id, key)
HTML.ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
end)
end
diff --git a/lib/pleroma/activity/ir/topics.ex b/lib/pleroma/activity/ir/topics.ex
index 56c52e9d1..8249cbe27 100644
--- a/lib/pleroma/activity/ir/topics.ex
+++ b/lib/pleroma/activity/ir/topics.ex
@@ -13,6 +13,14 @@ defmodule Pleroma.Activity.Ir.Topics do
|> List.flatten()
end
+ defp generate_topics(%{data: %{"type" => "ChatMessage"}}, %{data: %{"type" => "Delete"}}) do
+ ["user", "user:pleroma_chat"]
+ end
+
+ defp generate_topics(%{data: %{"type" => "ChatMessage"}}, %{data: %{"type" => "Create"}}) do
+ []
+ end
+
defp generate_topics(%{data: %{"type" => "Answer"}}, _) do
[]
end
@@ -21,7 +29,7 @@ defmodule Pleroma.Activity.Ir.Topics do
["user", "list"] ++ visibility_tags(object, activity)
end
- defp visibility_tags(object, activity) do
+ defp visibility_tags(object, %{data: %{"type" => type}} = activity) when type != "Announce" do
case Visibility.get_visibility(activity) do
"public" ->
if activity.local do
@@ -31,6 +39,10 @@ defmodule Pleroma.Activity.Ir.Topics do
end
|> item_creation_tags(object, activity)
+ "local" ->
+ ["public:local"]
+ |> item_creation_tags(object, activity)
+
"direct" ->
["direct"]
@@ -39,6 +51,10 @@ defmodule Pleroma.Activity.Ir.Topics do
end
end
+ defp visibility_tags(_object, _activity) do
+ []
+ end
+
defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do
tags ++
remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity)
@@ -63,7 +79,18 @@ defmodule Pleroma.Activity.Ir.Topics do
defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: []
- defp attachment_topics(_object, %{local: true}), do: ["public:media", "public:local:media"]
+ defp attachment_topics(_object, %{local: true} = activity) do
+ case Visibility.get_visibility(activity) do
+ "public" ->
+ ["public:media", "public:local:media"]
+
+ "local" ->
+ ["public:local:media"]
+
+ _ ->
+ []
+ end
+ end
defp attachment_topics(_object, %{actor: actor}) when is_binary(actor),
do: ["public:media", "public:remote:media:" <> URI.parse(actor).host]
diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex
index a898b2ea7..81c44ac05 100644
--- a/lib/pleroma/activity/queries.ex
+++ b/lib/pleroma/activity/queries.ex
@@ -52,8 +52,7 @@ defmodule Pleroma.Activity.Queries do
activity in query,
where:
fragment(
- "coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)",
- activity.data,
+ "associated_object_id((?)) = ANY(?)",
activity.data,
^object_ids
)
@@ -64,8 +63,7 @@ defmodule Pleroma.Activity.Queries do
from(activity in query,
where:
fragment(
- "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
- activity.data,
+ "associated_object_id((?)) = ?",
activity.data,
^object_id
)
diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex
index 694dc5709..0b9b24aa4 100644
--- a/lib/pleroma/activity/search.ex
+++ b/lib/pleroma/activity/search.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Activity.Search do
Activity
|> Activity.with_preloaded_object()
|> Activity.restrict_deactivated_users()
- |> restrict_public()
+ |> restrict_public(user)
|> query_with(index_type, search_query, search_function)
|> maybe_restrict_local(user)
|> maybe_restrict_author(author)
@@ -57,7 +57,19 @@ defmodule Pleroma.Activity.Search do
def maybe_restrict_blocked(query, _), do: query
- defp restrict_public(q) do
+ defp restrict_public(q, user) when not is_nil(user) do
+ intended_recipients = [
+ Pleroma.Constants.as_public(),
+ Pleroma.Web.ActivityPub.Utils.as_local_public()
+ ]
+
+ from([a, o] in q,
+ where: fragment("?->>'type' = 'Create'", a.data),
+ where: fragment("? && ?", ^intended_recipients, a.recipients)
+ )
+ end
+
+ defp restrict_public(q, _user) do
from([a, o] in q,
where: fragment("?->>'type' = 'Create'", a.data),
where: ^Pleroma.Constants.as_public() in a.recipients
diff --git a/lib/pleroma/announcement.ex b/lib/pleroma/announcement.ex
new file mode 100644
index 000000000..d97c5e728
--- /dev/null
+++ b/lib/pleroma/announcement.ex
@@ -0,0 +1,160 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Announcement do
+ use Ecto.Schema
+
+ import Ecto.Changeset, only: [cast: 3, validate_required: 2]
+ import Ecto.Query
+
+ alias Pleroma.AnnouncementReadRelationship
+ alias Pleroma.Repo
+
+ @type t :: %__MODULE__{}
+ @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
+
+ schema "announcements" do
+ field(:data, :map)
+ field(:starts_at, :utc_datetime)
+ field(:ends_at, :utc_datetime)
+ field(:rendered, :map)
+
+ timestamps(type: :utc_datetime)
+ end
+
+ def change(struct, params \\ %{}) do
+ struct
+ |> cast(validate_params(struct, params), [:data, :starts_at, :ends_at, :rendered])
+ |> validate_required([:data])
+ end
+
+ defp validate_params(struct, params) do
+ base_data =
+ %{
+ "content" => "",
+ "all_day" => false
+ }
+ |> Map.merge((struct && struct.data) || %{})
+
+ merged_data =
+ Map.merge(base_data, params.data)
+ |> Map.take(["content", "all_day"])
+
+ params
+ |> Map.merge(%{data: merged_data})
+ |> add_rendered_properties()
+ end
+
+ def add_rendered_properties(params) do
+ {content_html, _, _} =
+ Pleroma.Web.CommonAPI.Utils.format_input(params.data["content"], "text/plain",
+ mentions_format: :full
+ )
+
+ rendered = %{
+ "content" => content_html
+ }
+
+ params
+ |> Map.put(:rendered, rendered)
+ end
+
+ def add(params) do
+ changeset = change(%__MODULE__{}, params)
+
+ Repo.insert(changeset)
+ end
+
+ def update(announcement, params) do
+ changeset = change(announcement, params)
+
+ Repo.update(changeset)
+ end
+
+ def list_all do
+ __MODULE__
+ |> Repo.all()
+ end
+
+ def list_paginated(%{limit: limited_number, offset: offset_number}) do
+ __MODULE__
+ |> limit(^limited_number)
+ |> offset(^offset_number)
+ |> Repo.all()
+ end
+
+ def get_by_id(id) do
+ Repo.get_by(__MODULE__, id: id)
+ end
+
+ def delete_by_id(id) do
+ with announcement when not is_nil(announcement) <- get_by_id(id),
+ {:ok, _} <- Repo.delete(announcement) do
+ :ok
+ else
+ _ ->
+ :error
+ end
+ end
+
+ def read_by?(announcement, user) do
+ AnnouncementReadRelationship.exists?(user, announcement)
+ end
+
+ def mark_read_by(announcement, user) do
+ AnnouncementReadRelationship.mark_read(user, announcement)
+ end
+
+ def render_json(announcement, opts \\ []) do
+ extra_params =
+ case Keyword.fetch(opts, :for) do
+ {:ok, user} when not is_nil(user) ->
+ %{read: read_by?(announcement, user)}
+
+ _ ->
+ %{}
+ end
+
+ admin_extra_params =
+ case Keyword.fetch(opts, :admin) do
+ {:ok, true} ->
+ %{pleroma: %{raw_content: announcement.data["content"]}}
+
+ _ ->
+ %{}
+ end
+
+ base = %{
+ id: announcement.id,
+ content: announcement.rendered["content"],
+ starts_at: announcement.starts_at,
+ ends_at: announcement.ends_at,
+ all_day: announcement.data["all_day"],
+ published_at: announcement.inserted_at,
+ updated_at: announcement.updated_at,
+ mentions: [],
+ statuses: [],
+ tags: [],
+ emojis: [],
+ reactions: []
+ }
+
+ base
+ |> Map.merge(extra_params)
+ |> Map.merge(admin_extra_params)
+ end
+
+ # "visible" means:
+ # starts_at < time < ends_at
+ def list_all_visible_when(time) do
+ __MODULE__
+ |> where([a], is_nil(a.starts_at) or a.starts_at < ^time)
+ |> where([a], is_nil(a.ends_at) or a.ends_at > ^time)
+ |> Repo.all()
+ end
+
+ def list_all_visible do
+ list_all_visible_when(DateTime.now("Etc/UTC") |> elem(1))
+ end
+end
diff --git a/lib/pleroma/announcement_read_relationship.ex b/lib/pleroma/announcement_read_relationship.ex
new file mode 100644
index 000000000..9b64404ce
--- /dev/null
+++ b/lib/pleroma/announcement_read_relationship.ex
@@ -0,0 +1,55 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.AnnouncementReadRelationship do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ alias FlakeId.Ecto.CompatType
+ alias Pleroma.Announcement
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ @type t :: %__MODULE__{}
+
+ schema "announcement_read_relationships" do
+ belongs_to(:user, User, type: CompatType)
+ belongs_to(:announcement, Announcement, type: CompatType)
+
+ timestamps(updated_at: false)
+ end
+
+ def mark_read(user, announcement) do
+ %__MODULE__{}
+ |> cast(%{user_id: user.id, announcement_id: announcement.id}, [:user_id, :announcement_id])
+ |> validate_required([:user_id, :announcement_id])
+ |> foreign_key_constraint(:user_id)
+ |> foreign_key_constraint(:announcement_id)
+ |> unique_constraint([:user_id, :announcement_id])
+ |> Repo.insert()
+ end
+
+ def mark_unread(user, announcement) do
+ with relationship <- get(user, announcement),
+ {:exists, true} <- {:exists, not is_nil(relationship)},
+ {:ok, _} <- Repo.delete(relationship) do
+ :ok
+ else
+ {:exists, false} ->
+ :ok
+
+ _ ->
+ :error
+ end
+ end
+
+ def get(user, announcement) do
+ Repo.get_by(__MODULE__, user_id: user.id, announcement_id: announcement.id)
+ end
+
+ def exists?(user, announcement) do
+ not is_nil(get(user, announcement))
+ end
+end
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 80d891668..97cd88006 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -85,7 +85,17 @@ defmodule Pleroma.Application do
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
- opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
+ # If we have a lot of caches, default max_restarts can cause test
+ # resets to fail.
+ # Go for the default 3 unless we're in test
+ max_restarts =
+ if @mix_env == :test do
+ 100
+ else
+ 3
+ end
+
+ opts = [strategy: :one_for_one, name: Pleroma.Supervisor, max_restarts: max_restarts]
result = Supervisor.start_link(children, opts)
set_postgres_server_version()
@@ -162,6 +172,7 @@ defmodule Pleroma.Application do
build_cachex("object", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),
build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),
build_cachex("scrubber", limit: 2500),
+ build_cachex("scrubber_management", limit: 2500),
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
@@ -211,7 +222,8 @@ defmodule Pleroma.Application do
defp background_migrators do
[
- Pleroma.Migrators.HashtagsTableMigrator
+ Pleroma.Migrators.HashtagsTableMigrator,
+ Pleroma.Migrators.ContextObjectsDeletionMigrator
]
end
diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex
index 06d388694..44b1c1705 100644
--- a/lib/pleroma/application_requirements.ex
+++ b/lib/pleroma/application_requirements.ex
@@ -164,7 +164,8 @@ defmodule Pleroma.ApplicationRequirements do
defp check_system_commands!(:ok) do
filter_commands_statuses = [
- check_filter(Pleroma.Upload.Filter.Exiftool, "exiftool"),
+ check_filter(Pleroma.Upload.Filter.Exiftool.StripLocation, "exiftool"),
+ check_filter(Pleroma.Upload.Filter.Exiftool.ReadDescription, "exiftool"),
check_filter(Pleroma.Upload.Filter.Mogrify, "mogrify"),
check_filter(Pleroma.Upload.Filter.Mogrifun, "mogrify"),
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "mogrify"),
diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex
index a3b623bdf..27799338f 100644
--- a/lib/pleroma/bbs/handler.ex
+++ b/lib/pleroma/bbs/handler.ex
@@ -42,8 +42,45 @@ defmodule Pleroma.BBS.Handler do
def puts_activity(activity) do
status = Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{activity: activity})
+
IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})")
- IO.puts(HTML.strip_tags(status.content))
+
+ status.content
+ |> String.split("<br/>")
+ |> Enum.map(&HTML.strip_tags/1)
+ |> Enum.map(&HtmlEntities.decode/1)
+ |> Enum.map(&IO.puts/1)
+ end
+
+ def puts_notification(activity, user) do
+ notification =
+ Pleroma.Web.MastodonAPI.NotificationView.render("show.json", %{
+ notification: activity,
+ for: user
+ })
+
+ IO.puts(
+ "== (#{notification.type}) #{notification.status.id} by #{notification.account.display_name} (#{notification.account.acct})"
+ )
+
+ notification.status.content
+ |> String.split("<br/>")
+ |> Enum.map(&HTML.strip_tags/1)
+ |> Enum.map(&HtmlEntities.decode/1)
+ |> (fn x ->
+ case x do
+ [content] ->
+ "> " <> content
+
+ [head | _tail] ->
+ # "> " <> hd <> "..."
+ head
+ |> String.slice(1, 80)
+ |> (fn x -> "> " <> x <> "..." end).()
+ end
+ end).()
+ |> IO.puts()
+
IO.puts("")
end
@@ -53,6 +90,11 @@ defmodule Pleroma.BBS.Handler do
IO.puts("home - Show the home timeline")
IO.puts("p <text> - Post the given text")
IO.puts("r <id> <text> - Reply to the post with the given id")
+ IO.puts("t <id> - Show a thread from the given id")
+ IO.puts("n - Show notifications")
+ IO.puts("n read - Mark all notifactions as read")
+ IO.puts("f <id> - Favourites the post with the given id")
+ IO.puts("R <id> - Repeat the post with the given id")
IO.puts("quit - Quit")
state
@@ -73,11 +115,53 @@ defmodule Pleroma.BBS.Handler do
state
end
+ def handle_command(%{user: user} = state, "t " <> activity_id) do
+ with %Activity{} = activity <- Activity.get_by_id(activity_id) do
+ activities =
+ ActivityPub.fetch_activities_for_context(activity.data["context"], %{
+ blocking_user: user,
+ user: user,
+ exclude_id: activity.id
+ })
+
+ case activities do
+ [] ->
+ activity_id
+ |> Activity.get_by_id()
+ |> puts_activity()
+
+ _ ->
+ activities
+ |> Enum.reverse()
+ |> Enum.each(&puts_activity/1)
+ end
+ else
+ _e -> IO.puts("Could not show this thread...")
+ end
+
+ state
+ end
+
+ def handle_command(%{user: user} = state, "n read") do
+ Pleroma.Notification.clear(user)
+ IO.puts("All notifications were marked as read")
+
+ state
+ end
+
+ def handle_command(%{user: user} = state, "n") do
+ user
+ |> Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(%{})
+ |> Enum.each(&puts_notification(&1, user))
+
+ state
+ end
+
def handle_command(%{user: user} = state, "p " <> text) do
text = String.trim(text)
- with {:ok, _activity} <- CommonAPI.post(user, %{status: text}) do
- IO.puts("Posted!")
+ with {:ok, activity} <- CommonAPI.post(user, %{status: text}) do
+ IO.puts("Posted! ID: #{activity.id}")
else
_e -> IO.puts("Could not post...")
end
@@ -85,6 +169,19 @@ defmodule Pleroma.BBS.Handler do
state
end
+ def handle_command(%{user: user} = state, "f " <> id) do
+ id = String.trim(id)
+
+ with %Activity{} = activity <- Activity.get_by_id(id),
+ {:ok, _activity} <- CommonAPI.favorite(user, activity) do
+ IO.puts("Favourited!")
+ else
+ _e -> IO.puts("Could not Favourite...")
+ end
+
+ state
+ end
+
def handle_command(state, "home") do
user = state.user
@@ -123,7 +220,7 @@ defmodule Pleroma.BBS.Handler do
loop(%{state | counter: state.counter + 1})
- {:error, :interrupted} ->
+ {:input, ^input, {:error, :interrupted}} ->
IO.puts("Caught Ctrl+C...")
loop(%{state | counter: state.counter + 1})
diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
index e0423f179..aad67c51d 100644
--- a/lib/pleroma/config/deprecation_warnings.ex
+++ b/lib/pleroma/config/deprecation_warnings.ex
@@ -20,6 +20,43 @@ defmodule Pleroma.Config.DeprecationWarnings do
"\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"}
]
+ def check_exiftool_filter do
+ filters = Config.get([Pleroma.Upload]) |> Keyword.get(:filters, [])
+
+ if Pleroma.Upload.Filter.Exiftool in filters do
+ Logger.warn("""
+ !!!DEPRECATION WARNING!!!
+ Your config is using Exiftool as a filter instead of Exiftool.StripLocation. This should work for now, but you are advised to change to the new configuration to prevent possible issues later:
+
+ ```
+ config :pleroma, Pleroma.Upload,
+ filters: [Pleroma.Upload.Filter.Exiftool]
+ ```
+
+ Is now
+
+
+ ```
+ config :pleroma, Pleroma.Upload,
+ filters: [Pleroma.Upload.Filter.Exiftool.StripLocation]
+ ```
+ """)
+
+ new_config =
+ filters
+ |> Enum.map(fn
+ Pleroma.Upload.Filter.Exiftool -> Pleroma.Upload.Filter.Exiftool.StripLocation
+ filter -> filter
+ end)
+
+ Config.put([Pleroma.Upload, :filters], new_config)
+
+ :error
+ else
+ :ok
+ end
+ end
+
def check_simple_policy_tuples do
has_strings =
Config.get([:mrf_simple])
@@ -179,7 +216,8 @@ defmodule Pleroma.Config.DeprecationWarnings do
check_old_chat_shoutbox(),
check_quarantined_instances_tuples(),
check_transparency_exclusions_tuples(),
- check_simple_policy_tuples()
+ check_simple_policy_tuples(),
+ check_exiftool_filter()
]
|> Enum.reduce(:ok, fn
:ok, :ok -> :ok
diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex
index 015be3d8e..bd85eccab 100644
--- a/lib/pleroma/config/loader.ex
+++ b/lib/pleroma/config/loader.ex
@@ -19,21 +19,10 @@ defmodule Pleroma.Config.Loader do
:tesla
]
- if Code.ensure_loaded?(Config.Reader) do
- @reader Config.Reader
-
- def read(path), do: @reader.read!(path)
- else
- # support for Elixir less than 1.9
- @reader Mix.Config
- def read(path) do
- path
- |> @reader.eval!()
- |> elem(0)
- end
- end
+ @reader Config.Reader
@spec read(Path.t()) :: keyword()
+ def read(path), do: @reader.read!(path)
@spec merge(keyword(), keyword()) :: keyword()
def merge(c1, c2), do: @reader.merge(c1, c2)
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index a42c71d23..cfb405218 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -27,4 +27,46 @@ 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(updatable_object_types,
+ do: [
+ "Note",
+ "Question",
+ "Audio",
+ "Video",
+ "Event",
+ "Article",
+ "Page"
+ ]
+ )
+
+ const(actor_types,
+ do: [
+ "Application",
+ "Group",
+ "Organization",
+ "Person",
+ "Service"
+ ]
+ )
+
+ # basic regex, just there to weed out potential mistakes
+ # https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
+ const(mime_regex,
+ do: ~r/^[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+\/[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+(; .*)?$/
+ )
end
diff --git a/lib/pleroma/data_migration.ex b/lib/pleroma/data_migration.ex
index 59d891d8d..8451678fc 100644
--- a/lib/pleroma/data_migration.ex
+++ b/lib/pleroma/data_migration.ex
@@ -42,4 +42,5 @@ defmodule Pleroma.DataMigration do
end
def populate_hashtags_table, do: get_by_name("populate_hashtags_table")
+ def delete_context_objects, do: get_by_name("delete_context_objects")
end
diff --git a/lib/pleroma/docs/translator.ex b/lib/pleroma/docs/translator.ex
new file mode 100644
index 000000000..13e33c87e
--- /dev/null
+++ b/lib/pleroma/docs/translator.ex
@@ -0,0 +1,10 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Docs.Translator do
+ require Pleroma.Docs.Translator.Compiler
+ require Pleroma.Web.Gettext
+
+ @before_compile Pleroma.Docs.Translator.Compiler
+end
diff --git a/lib/pleroma/docs/translator/compiler.ex b/lib/pleroma/docs/translator/compiler.ex
new file mode 100644
index 000000000..5d27d9fa2
--- /dev/null
+++ b/lib/pleroma/docs/translator/compiler.ex
@@ -0,0 +1,119 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Docs.Translator.Compiler do
+ @external_resource "config/description.exs"
+ @raw_config Pleroma.Config.Loader.read("config/description.exs")
+ @raw_descriptions @raw_config[:pleroma][:config_description]
+
+ defmacro __before_compile__(_env) do
+ strings =
+ __MODULE__.descriptions()
+ |> __MODULE__.extract_strings()
+
+ quote do
+ def placeholder do
+ unquote do
+ Enum.map(
+ strings,
+ fn {path, type, string} ->
+ ctxt = msgctxt_for(path, type)
+
+ quote do
+ Pleroma.Web.Gettext.dpgettext_noop(
+ "config_descriptions",
+ unquote(ctxt),
+ unquote(string)
+ )
+ end
+ end
+ )
+ end
+ end
+ end
+ end
+
+ def descriptions do
+ Pleroma.Web.ActivityPub.MRF.config_descriptions()
+ |> Enum.reduce(@raw_descriptions, fn description, acc -> [description | acc] end)
+ |> Pleroma.Docs.Generator.convert_to_strings()
+ end
+
+ def extract_strings(descriptions) do
+ descriptions
+ |> Enum.reduce(%{strings: [], path: []}, &process_item/2)
+ |> Map.get(:strings)
+ end
+
+ defp process_item(entity, acc) do
+ current_level =
+ acc
+ |> process_desc(entity)
+ |> process_label(entity)
+
+ process_children(entity, current_level)
+ end
+
+ defp process_desc(acc, %{description: desc} = item) do
+ %{
+ strings: [{acc.path ++ [key_for(item)], "description", desc} | acc.strings],
+ path: acc.path
+ }
+ end
+
+ defp process_desc(acc, _) do
+ acc
+ end
+
+ defp process_label(acc, %{label: label} = item) do
+ %{
+ strings: [{acc.path ++ [key_for(item)], "label", label} | acc.strings],
+ path: acc.path
+ }
+ end
+
+ defp process_label(acc, _) do
+ acc
+ end
+
+ defp process_children(%{children: children} = item, acc) do
+ current_level = Map.put(acc, :path, acc.path ++ [key_for(item)])
+
+ children
+ |> Enum.reduce(current_level, &process_item/2)
+ |> Map.put(:path, acc.path)
+ end
+
+ defp process_children(_, acc) do
+ acc
+ end
+
+ def msgctxt_for(path, type) do
+ "config #{type} at #{Enum.join(path, " > ")}"
+ end
+
+ defp convert_group({_, group}) do
+ group
+ end
+
+ defp convert_group(group) do
+ group
+ end
+
+ def key_for(%{group: group, key: key}) do
+ "#{convert_group(group)}-#{key}"
+ end
+
+ def key_for(%{group: group}) do
+ convert_group(group)
+ end
+
+ def key_for(%{key: key}) do
+ key
+ end
+
+ def key_for(_) do
+ nil
+ end
+end
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/mime.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/mime.ex
new file mode 100644
index 000000000..31d51577d
--- /dev/null
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/mime.ex
@@ -0,0 +1,25 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.MIME do
+ use Ecto.Type
+
+ require Pleroma.Constants
+
+ def type, do: :string
+
+ def cast(mime) when is_binary(mime) do
+ if mime =~ Pleroma.Constants.mime_regex() do
+ {:ok, mime}
+ else
+ {:ok, "application/octet-stream"}
+ end
+ end
+
+ def cast(_), do: :error
+
+ def dump(data), do: {:ok, data}
+
+ def load(data), do: {:ok, data}
+end
diff --git a/lib/pleroma/emoji-test.txt b/lib/pleroma/emoji-test.txt
index dd5493366..87d093d64 100644
--- a/lib/pleroma/emoji-test.txt
+++ b/lib/pleroma/emoji-test.txt
@@ -1,13 +1,13 @@
# emoji-test.txt
-# Date: 2021-08-26, 17:22:23 GMT
-# © 2021 Unicode®, Inc.
+# Date: 2022-08-12, 20:24:39 GMT
+# © 2022 Unicode®, Inc.
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
-# For terms of use, see http://www.unicode.org/terms_of_use.html
+# For terms of use, see https://www.unicode.org/terms_of_use.html
#
# Emoji Keyboard/Display Test Data for UTS #51
-# Version: 14.0
+# Version: 15.0
#
-# For documentation and usage, see http://www.unicode.org/reports/tr51
+# For documentation and usage, see https://www.unicode.org/reports/tr51
#
# This file provides data for testing which emoji forms should be in keyboards and which should also be displayed/processed.
# Format: code points; status # emoji name
@@ -92,6 +92,7 @@
1F62C ; fully-qualified # 😬 E1.0 grimacing face
1F62E 200D 1F4A8 ; fully-qualified # 😮‍💨 E13.1 face exhaling
1F925 ; fully-qualified # 🤥 E3.0 lying face
+1FAE8 ; fully-qualified # 🫨 E15.0 shaking face
# subgroup: face-sleepy
1F60C ; fully-qualified # 😌 E0.6 relieved face
@@ -155,7 +156,7 @@
# subgroup: face-negative
1F624 ; fully-qualified # 😤 E0.6 face with steam from nose
-1F621 ; fully-qualified # 😡 E0.6 pouting face
+1F621 ; fully-qualified # 😡 E0.6 enraged face
1F620 ; fully-qualified # 😠 E0.6 angry face
1F92C ; fully-qualified # 🤬 E5.0 face with symbols on mouth
1F608 ; fully-qualified # 😈 E1.0 smiling face with horns
@@ -190,8 +191,7 @@
1F649 ; fully-qualified # 🙉 E0.6 hear-no-evil monkey
1F64A ; fully-qualified # 🙊 E0.6 speak-no-evil monkey
-# subgroup: emotion
-1F48B ; fully-qualified # 💋 E0.6 kiss mark
+# subgroup: heart
1F48C ; fully-qualified # 💌 E0.6 love letter
1F498 ; fully-qualified # 💘 E0.6 heart with arrow
1F49D ; fully-qualified # 💝 E0.6 heart with ribbon
@@ -210,14 +210,20 @@
2764 200D 1FA79 ; unqualified # ❤‍🩹 E13.1 mending heart
2764 FE0F ; fully-qualified # ❤️ E0.6 red heart
2764 ; unqualified # ❤ E0.6 red heart
+1FA77 ; fully-qualified # 🩷 E15.0 pink heart
1F9E1 ; fully-qualified # 🧡 E5.0 orange heart
1F49B ; fully-qualified # 💛 E0.6 yellow heart
1F49A ; fully-qualified # 💚 E0.6 green heart
1F499 ; fully-qualified # 💙 E0.6 blue heart
+1FA75 ; fully-qualified # 🩵 E15.0 light blue heart
1F49C ; fully-qualified # 💜 E0.6 purple heart
1F90E ; fully-qualified # 🤎 E12.0 brown heart
1F5A4 ; fully-qualified # 🖤 E3.0 black heart
+1FA76 ; fully-qualified # 🩶 E15.0 grey heart
1F90D ; fully-qualified # 🤍 E12.0 white heart
+
+# subgroup: emotion
+1F48B ; fully-qualified # 💋 E0.6 kiss mark
1F4AF ; fully-qualified # 💯 E0.6 hundred points
1F4A2 ; fully-qualified # 💢 E0.6 anger symbol
1F4A5 ; fully-qualified # 💥 E0.6 collision
@@ -226,21 +232,20 @@
1F4A8 ; fully-qualified # 💨 E0.6 dashing away
1F573 FE0F ; fully-qualified # 🕳️ E0.7 hole
1F573 ; unqualified # 🕳 E0.7 hole
-1F4A3 ; fully-qualified # 💣 E0.6 bomb
1F4AC ; fully-qualified # 💬 E0.6 speech balloon
1F441 FE0F 200D 1F5E8 FE0F ; fully-qualified # 👁️‍🗨️ E2.0 eye in speech bubble
1F441 200D 1F5E8 FE0F ; unqualified # 👁‍🗨️ E2.0 eye in speech bubble
-1F441 FE0F 200D 1F5E8 ; unqualified # 👁️‍🗨 E2.0 eye in speech bubble
+1F441 FE0F 200D 1F5E8 ; minimally-qualified # 👁️‍🗨 E2.0 eye in speech bubble
1F441 200D 1F5E8 ; unqualified # 👁‍🗨 E2.0 eye in speech bubble
1F5E8 FE0F ; fully-qualified # 🗨️ E2.0 left speech bubble
1F5E8 ; unqualified # 🗨 E2.0 left speech bubble
1F5EF FE0F ; fully-qualified # 🗯️ E0.7 right anger bubble
1F5EF ; unqualified # 🗯 E0.7 right anger bubble
1F4AD ; fully-qualified # 💭 E1.0 thought balloon
-1F4A4 ; fully-qualified # 💤 E0.6 zzz
+1F4A4 ; fully-qualified # 💤 E0.6 ZZZ
-# Smileys & Emotion subtotal: 177
-# Smileys & Emotion subtotal: 177 w/o modifiers
+# Smileys & Emotion subtotal: 180
+# Smileys & Emotion subtotal: 180 w/o modifiers
# group: People & Body
@@ -300,6 +305,18 @@
1FAF4 1F3FD ; fully-qualified # 🫴🏽 E14.0 palm up hand: medium skin tone
1FAF4 1F3FE ; fully-qualified # 🫴🏾 E14.0 palm up hand: medium-dark skin tone
1FAF4 1F3FF ; fully-qualified # 🫴🏿 E14.0 palm up hand: dark skin tone
+1FAF7 ; fully-qualified # 🫷 E15.0 leftwards pushing hand
+1FAF7 1F3FB ; fully-qualified # 🫷🏻 E15.0 leftwards pushing hand: light skin tone
+1FAF7 1F3FC ; fully-qualified # 🫷🏼 E15.0 leftwards pushing hand: medium-light skin tone
+1FAF7 1F3FD ; fully-qualified # 🫷🏽 E15.0 leftwards pushing hand: medium skin tone
+1FAF7 1F3FE ; fully-qualified # 🫷🏾 E15.0 leftwards pushing hand: medium-dark skin tone
+1FAF7 1F3FF ; fully-qualified # 🫷🏿 E15.0 leftwards pushing hand: dark skin tone
+1FAF8 ; fully-qualified # 🫸 E15.0 rightwards pushing hand
+1FAF8 1F3FB ; fully-qualified # 🫸🏻 E15.0 rightwards pushing hand: light skin tone
+1FAF8 1F3FC ; fully-qualified # 🫸🏼 E15.0 rightwards pushing hand: medium-light skin tone
+1FAF8 1F3FD ; fully-qualified # 🫸🏽 E15.0 rightwards pushing hand: medium skin tone
+1FAF8 1F3FE ; fully-qualified # 🫸🏾 E15.0 rightwards pushing hand: medium-dark skin tone
+1FAF8 1F3FF ; fully-qualified # 🫸🏿 E15.0 rightwards pushing hand: dark skin tone
# subgroup: hand-fingers-partial
1F44C ; fully-qualified # 👌 E0.6 OK hand
@@ -473,11 +490,11 @@
1F932 1F3FE ; fully-qualified # 🤲🏾 E5.0 palms up together: medium-dark skin tone
1F932 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone
1F91D ; fully-qualified # 🤝 E3.0 handshake
-1F91D 1F3FB ; fully-qualified # 🤝🏻 E3.0 handshake: light skin tone
-1F91D 1F3FC ; fully-qualified # 🤝🏼 E3.0 handshake: medium-light skin tone
-1F91D 1F3FD ; fully-qualified # 🤝🏽 E3.0 handshake: medium skin tone
-1F91D 1F3FE ; fully-qualified # 🤝🏾 E3.0 handshake: medium-dark skin tone
-1F91D 1F3FF ; fully-qualified # 🤝🏿 E3.0 handshake: dark skin tone
+1F91D 1F3FB ; fully-qualified # 🤝🏻 E14.0 handshake: light skin tone
+1F91D 1F3FC ; fully-qualified # 🤝🏼 E14.0 handshake: medium-light skin tone
+1F91D 1F3FD ; fully-qualified # 🤝🏽 E14.0 handshake: medium skin tone
+1F91D 1F3FE ; fully-qualified # 🤝🏾 E14.0 handshake: medium-dark skin tone
+1F91D 1F3FF ; fully-qualified # 🤝🏿 E14.0 handshake: dark skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏻‍🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏻‍🫲🏽 E14.0 handshake: light skin tone, medium skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏻‍🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone
@@ -1455,7 +1472,7 @@
1F575 1F3FF ; fully-qualified # 🕵🏿 E2.0 detective: dark skin tone
1F575 FE0F 200D 2642 FE0F ; fully-qualified # 🕵️‍♂️ E4.0 man detective
1F575 200D 2642 FE0F ; unqualified # 🕵‍♂️ E4.0 man detective
-1F575 FE0F 200D 2642 ; unqualified # 🕵️‍♂ E4.0 man detective
+1F575 FE0F 200D 2642 ; minimally-qualified # 🕵️‍♂ E4.0 man detective
1F575 200D 2642 ; unqualified # 🕵‍♂ E4.0 man detective
1F575 1F3FB 200D 2642 FE0F ; fully-qualified # 🕵🏻‍♂️ E4.0 man detective: light skin tone
1F575 1F3FB 200D 2642 ; minimally-qualified # 🕵🏻‍♂ E4.0 man detective: light skin tone
@@ -1469,7 +1486,7 @@
1F575 1F3FF 200D 2642 ; minimally-qualified # 🕵🏿‍♂ E4.0 man detective: dark skin tone
1F575 FE0F 200D 2640 FE0F ; fully-qualified # 🕵️‍♀️ E4.0 woman detective
1F575 200D 2640 FE0F ; unqualified # 🕵‍♀️ E4.0 woman detective
-1F575 FE0F 200D 2640 ; unqualified # 🕵️‍♀ E4.0 woman detective
+1F575 FE0F 200D 2640 ; minimally-qualified # 🕵️‍♀ E4.0 woman detective
1F575 200D 2640 ; unqualified # 🕵‍♀ E4.0 woman detective
1F575 1F3FB 200D 2640 FE0F ; fully-qualified # 🕵🏻‍♀️ E4.0 woman detective: light skin tone
1F575 1F3FB 200D 2640 ; minimally-qualified # 🕵🏻‍♀ E4.0 woman detective: light skin tone
@@ -2302,7 +2319,7 @@
1F3CC 1F3FF ; fully-qualified # 🏌🏿 E4.0 person golfing: dark skin tone
1F3CC FE0F 200D 2642 FE0F ; fully-qualified # 🏌️‍♂️ E4.0 man golfing
1F3CC 200D 2642 FE0F ; unqualified # 🏌‍♂️ E4.0 man golfing
-1F3CC FE0F 200D 2642 ; unqualified # 🏌️‍♂ E4.0 man golfing
+1F3CC FE0F 200D 2642 ; minimally-qualified # 🏌️‍♂ E4.0 man golfing
1F3CC 200D 2642 ; unqualified # 🏌‍♂ E4.0 man golfing
1F3CC 1F3FB 200D 2642 FE0F ; fully-qualified # 🏌🏻‍♂️ E4.0 man golfing: light skin tone
1F3CC 1F3FB 200D 2642 ; minimally-qualified # 🏌🏻‍♂ E4.0 man golfing: light skin tone
@@ -2316,7 +2333,7 @@
1F3CC 1F3FF 200D 2642 ; minimally-qualified # 🏌🏿‍♂ E4.0 man golfing: dark skin tone
1F3CC FE0F 200D 2640 FE0F ; fully-qualified # 🏌️‍♀️ E4.0 woman golfing
1F3CC 200D 2640 FE0F ; unqualified # 🏌‍♀️ E4.0 woman golfing
-1F3CC FE0F 200D 2640 ; unqualified # 🏌️‍♀ E4.0 woman golfing
+1F3CC FE0F 200D 2640 ; minimally-qualified # 🏌️‍♀ E4.0 woman golfing
1F3CC 200D 2640 ; unqualified # 🏌‍♀ E4.0 woman golfing
1F3CC 1F3FB 200D 2640 FE0F ; fully-qualified # 🏌🏻‍♀️ E4.0 woman golfing: light skin tone
1F3CC 1F3FB 200D 2640 ; minimally-qualified # 🏌🏻‍♀ E4.0 woman golfing: light skin tone
@@ -2427,7 +2444,7 @@
26F9 1F3FF ; fully-qualified # ⛹🏿 E2.0 person bouncing ball: dark skin tone
26F9 FE0F 200D 2642 FE0F ; fully-qualified # ⛹️‍♂️ E4.0 man bouncing ball
26F9 200D 2642 FE0F ; unqualified # ⛹‍♂️ E4.0 man bouncing ball
-26F9 FE0F 200D 2642 ; unqualified # ⛹️‍♂ E4.0 man bouncing ball
+26F9 FE0F 200D 2642 ; minimally-qualified # ⛹️‍♂ E4.0 man bouncing ball
26F9 200D 2642 ; unqualified # ⛹‍♂ E4.0 man bouncing ball
26F9 1F3FB 200D 2642 FE0F ; fully-qualified # ⛹🏻‍♂️ E4.0 man bouncing ball: light skin tone
26F9 1F3FB 200D 2642 ; minimally-qualified # ⛹🏻‍♂ E4.0 man bouncing ball: light skin tone
@@ -2441,7 +2458,7 @@
26F9 1F3FF 200D 2642 ; minimally-qualified # ⛹🏿‍♂ E4.0 man bouncing ball: dark skin tone
26F9 FE0F 200D 2640 FE0F ; fully-qualified # ⛹️‍♀️ E4.0 woman bouncing ball
26F9 200D 2640 FE0F ; unqualified # ⛹‍♀️ E4.0 woman bouncing ball
-26F9 FE0F 200D 2640 ; unqualified # ⛹️‍♀ E4.0 woman bouncing ball
+26F9 FE0F 200D 2640 ; minimally-qualified # ⛹️‍♀ E4.0 woman bouncing ball
26F9 200D 2640 ; unqualified # ⛹‍♀ E4.0 woman bouncing ball
26F9 1F3FB 200D 2640 FE0F ; fully-qualified # ⛹🏻‍♀️ E4.0 woman bouncing ball: light skin tone
26F9 1F3FB 200D 2640 ; minimally-qualified # ⛹🏻‍♀ E4.0 woman bouncing ball: light skin tone
@@ -2462,7 +2479,7 @@
1F3CB 1F3FF ; fully-qualified # 🏋🏿 E2.0 person lifting weights: dark skin tone
1F3CB FE0F 200D 2642 FE0F ; fully-qualified # 🏋️‍♂️ E4.0 man lifting weights
1F3CB 200D 2642 FE0F ; unqualified # 🏋‍♂️ E4.0 man lifting weights
-1F3CB FE0F 200D 2642 ; unqualified # 🏋️‍♂ E4.0 man lifting weights
+1F3CB FE0F 200D 2642 ; minimally-qualified # 🏋️‍♂ E4.0 man lifting weights
1F3CB 200D 2642 ; unqualified # 🏋‍♂ E4.0 man lifting weights
1F3CB 1F3FB 200D 2642 FE0F ; fully-qualified # 🏋🏻‍♂️ E4.0 man lifting weights: light skin tone
1F3CB 1F3FB 200D 2642 ; minimally-qualified # 🏋🏻‍♂ E4.0 man lifting weights: light skin tone
@@ -2476,7 +2493,7 @@
1F3CB 1F3FF 200D 2642 ; minimally-qualified # 🏋🏿‍♂ E4.0 man lifting weights: dark skin tone
1F3CB FE0F 200D 2640 FE0F ; fully-qualified # 🏋️‍♀️ E4.0 woman lifting weights
1F3CB 200D 2640 FE0F ; unqualified # 🏋‍♀️ E4.0 woman lifting weights
-1F3CB FE0F 200D 2640 ; unqualified # 🏋️‍♀ E4.0 woman lifting weights
+1F3CB FE0F 200D 2640 ; minimally-qualified # 🏋️‍♀ E4.0 woman lifting weights
1F3CB 200D 2640 ; unqualified # 🏋‍♀ E4.0 woman lifting weights
1F3CB 1F3FB 200D 2640 FE0F ; fully-qualified # 🏋🏻‍♀️ E4.0 woman lifting weights: light skin tone
1F3CB 1F3FB 200D 2640 ; minimally-qualified # 🏋🏻‍♀ E4.0 woman lifting weights: light skin tone
@@ -3262,8 +3279,8 @@
1FAC2 ; fully-qualified # 🫂 E13.0 people hugging
1F463 ; fully-qualified # 👣 E0.6 footprints
-# People & Body subtotal: 2986
-# People & Body subtotal: 506 w/o modifiers
+# People & Body subtotal: 2998
+# People & Body subtotal: 508 w/o modifiers
# group: Component
@@ -3306,6 +3323,8 @@
1F405 ; fully-qualified # 🐅 E1.0 tiger
1F406 ; fully-qualified # 🐆 E1.0 leopard
1F434 ; fully-qualified # 🐴 E0.6 horse face
+1FACE ; fully-qualified # 🫎 E15.0 moose
+1FACF ; fully-qualified # 🫏 E15.0 donkey
1F40E ; fully-qualified # 🐎 E0.6 horse
1F984 ; fully-qualified # 🦄 E1.0 unicorn
1F993 ; fully-qualified # 🦓 E5.0 zebra
@@ -3373,6 +3392,9 @@
1F9A9 ; fully-qualified # 🦩 E12.0 flamingo
1F99A ; fully-qualified # 🦚 E11.0 peacock
1F99C ; fully-qualified # 🦜 E11.0 parrot
+1FABD ; fully-qualified # 🪽 E15.0 wing
+1F426 200D 2B1B ; fully-qualified # 🐦‍⬛ E15.0 black bird
+1FABF ; fully-qualified # 🪿 E15.0 goose
# subgroup: animal-amphibian
1F438 ; fully-qualified # 🐸 E0.6 frog
@@ -3399,6 +3421,7 @@
1F419 ; fully-qualified # 🐙 E0.6 octopus
1F41A ; fully-qualified # 🐚 E0.6 spiral shell
1FAB8 ; fully-qualified # 🪸 E14.0 coral
+1FABC ; fully-qualified # 🪼 E15.0 jellyfish
# subgroup: animal-bug
1F40C ; fully-qualified # 🐌 E0.6 snail
@@ -3433,6 +3456,7 @@
1F33B ; fully-qualified # 🌻 E0.6 sunflower
1F33C ; fully-qualified # 🌼 E0.6 blossom
1F337 ; fully-qualified # 🌷 E0.6 tulip
+1FABB ; fully-qualified # 🪻 E15.0 hyacinth
# subgroup: plant-other
1F331 ; fully-qualified # 🌱 E0.6 seedling
@@ -3451,9 +3475,10 @@
1F343 ; fully-qualified # 🍃 E0.6 leaf fluttering in wind
1FAB9 ; fully-qualified # 🪹 E14.0 empty nest
1FABA ; fully-qualified # 🪺 E14.0 nest with eggs
+1F344 ; fully-qualified # 🍄 E0.6 mushroom
-# Animals & Nature subtotal: 151
-# Animals & Nature subtotal: 151 w/o modifiers
+# Animals & Nature subtotal: 159
+# Animals & Nature subtotal: 159 w/o modifiers
# group: Food & Drink
@@ -3492,10 +3517,11 @@
1F966 ; fully-qualified # 🥦 E5.0 broccoli
1F9C4 ; fully-qualified # 🧄 E12.0 garlic
1F9C5 ; fully-qualified # 🧅 E12.0 onion
-1F344 ; fully-qualified # 🍄 E0.6 mushroom
1F95C ; fully-qualified # 🥜 E3.0 peanuts
1FAD8 ; fully-qualified # 🫘 E14.0 beans
1F330 ; fully-qualified # 🌰 E0.6 chestnut
+1FADA ; fully-qualified # 🫚 E15.0 ginger root
+1FADB ; fully-qualified # 🫛 E15.0 pea pod
# subgroup: food-prepared
1F35E ; fully-qualified # 🍞 E0.6 bread
@@ -3607,8 +3633,8 @@
1FAD9 ; fully-qualified # 🫙 E14.0 jar
1F3FA ; fully-qualified # 🏺 E1.0 amphora
-# Food & Drink subtotal: 134
-# Food & Drink subtotal: 134 w/o modifiers
+# Food & Drink subtotal: 135
+# Food & Drink subtotal: 135 w/o modifiers
# group: Travel & Places
@@ -3974,11 +4000,10 @@
1F3AF ; fully-qualified # 🎯 E0.6 bullseye
1FA80 ; fully-qualified # 🪀 E12.0 yo-yo
1FA81 ; fully-qualified # 🪁 E12.0 kite
+1F52B ; fully-qualified # 🔫 E0.6 water pistol
1F3B1 ; fully-qualified # 🎱 E0.6 pool 8 ball
1F52E ; fully-qualified # 🔮 E0.6 crystal ball
1FA84 ; fully-qualified # 🪄 E13.0 magic wand
-1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet
-1FAAC ; fully-qualified # 🪬 E14.0 hamsa
1F3AE ; fully-qualified # 🎮 E0.6 video game
1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick
1F579 ; unqualified # 🕹 E0.7 joystick
@@ -4013,8 +4038,8 @@
1F9F6 ; fully-qualified # 🧶 E11.0 yarn
1FAA2 ; fully-qualified # 🪢 E13.0 knot
-# Activities subtotal: 97
-# Activities subtotal: 97 w/o modifiers
+# Activities subtotal: 96
+# Activities subtotal: 96 w/o modifiers
# group: Objects
@@ -4040,6 +4065,7 @@
1FA73 ; fully-qualified # 🩳 E12.0 shorts
1F459 ; fully-qualified # 👙 E0.6 bikini
1F45A ; fully-qualified # 👚 E0.6 woman’s clothes
+1FAAD ; fully-qualified # 🪭 E15.0 folding hand fan
1F45B ; fully-qualified # 👛 E0.6 purse
1F45C ; fully-qualified # 👜 E0.6 handbag
1F45D ; fully-qualified # 👝 E0.6 clutch bag
@@ -4055,6 +4081,7 @@
1F461 ; fully-qualified # 👡 E0.6 woman’s sandal
1FA70 ; fully-qualified # 🩰 E12.0 ballet shoes
1F462 ; fully-qualified # 👢 E0.6 woman’s boot
+1FAAE ; fully-qualified # 🪮 E15.0 hair pick
1F451 ; fully-qualified # 👑 E0.6 crown
1F452 ; fully-qualified # 👒 E0.6 woman’s hat
1F3A9 ; fully-qualified # 🎩 E0.6 top hat
@@ -4103,6 +4130,8 @@
1FA95 ; fully-qualified # 🪕 E12.0 banjo
1F941 ; fully-qualified # 🥁 E3.0 drum
1FA98 ; fully-qualified # 🪘 E13.0 long drum
+1FA87 ; fully-qualified # 🪇 E15.0 maracas
+1FA88 ; fully-qualified # 🪈 E15.0 flute
# subgroup: phone
1F4F1 ; fully-qualified # 📱 E0.6 mobile phone
@@ -4275,7 +4304,7 @@
1F5E1 ; unqualified # 🗡 E0.7 dagger
2694 FE0F ; fully-qualified # ⚔️ E1.0 crossed swords
2694 ; unqualified # ⚔ E1.0 crossed swords
-1F52B ; fully-qualified # 🔫 E0.6 water pistol
+1F4A3 ; fully-qualified # 💣 E0.6 bomb
1FA83 ; fully-qualified # 🪃 E13.0 boomerang
1F3F9 ; fully-qualified # 🏹 E1.0 bow and arrow
1F6E1 FE0F ; fully-qualified # 🛡️ E0.7 shield
@@ -4354,12 +4383,14 @@
1FAA6 ; fully-qualified # 🪦 E13.0 headstone
26B1 FE0F ; fully-qualified # ⚱️ E1.0 funeral urn
26B1 ; unqualified # ⚱ E1.0 funeral urn
+1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet
+1FAAC ; fully-qualified # 🪬 E14.0 hamsa
1F5FF ; fully-qualified # 🗿 E0.6 moai
1FAA7 ; fully-qualified # 🪧 E13.0 placard
1FAAA ; fully-qualified # 🪪 E14.0 identification card
-# Objects subtotal: 304
-# Objects subtotal: 304 w/o modifiers
+# Objects subtotal: 310
+# Objects subtotal: 310 w/o modifiers
# group: Symbols
@@ -4455,6 +4486,7 @@
262E ; unqualified # ☮ E1.0 peace symbol
1F54E ; fully-qualified # 🕎 E1.0 menorah
1F52F ; fully-qualified # 🔯 E0.6 dotted six-pointed star
+1FAAF ; fully-qualified # 🪯 E15.0 khanda
# subgroup: zodiac
2648 ; fully-qualified # ♈ E0.6 Aries
@@ -4503,6 +4535,7 @@
1F505 ; fully-qualified # 🔅 E1.0 dim button
1F506 ; fully-qualified # 🔆 E1.0 bright button
1F4F6 ; fully-qualified # 📶 E0.6 antenna bars
+1F6DC ; fully-qualified # 🛜 E15.0 wireless
1F4F3 ; fully-qualified # 📳 E0.6 vibration mode
1F4F4 ; fully-qualified # 📴 E0.6 mobile phone off
@@ -4693,8 +4726,8 @@
1F533 ; fully-qualified # 🔳 E0.6 white square button
1F532 ; fully-qualified # 🔲 E0.6 black square button
-# Symbols subtotal: 302
-# Symbols subtotal: 302 w/o modifiers
+# Symbols subtotal: 304
+# Symbols subtotal: 304 w/o modifiers
# group: Flags
@@ -4709,7 +4742,7 @@
1F3F3 200D 1F308 ; unqualified # 🏳‍🌈 E4.0 rainbow flag
1F3F3 FE0F 200D 26A7 FE0F ; fully-qualified # 🏳️‍⚧️ E13.0 transgender flag
1F3F3 200D 26A7 FE0F ; unqualified # 🏳‍⚧️ E13.0 transgender flag
-1F3F3 FE0F 200D 26A7 ; unqualified # 🏳️‍⚧ E13.0 transgender flag
+1F3F3 FE0F 200D 26A7 ; minimally-qualified # 🏳️‍⚧ E13.0 transgender flag
1F3F3 200D 26A7 ; unqualified # 🏳‍⚧ E13.0 transgender flag
1F3F4 200D 2620 FE0F ; fully-qualified # 🏴‍☠️ E11.0 pirate flag
1F3F4 200D 2620 ; minimally-qualified # 🏴‍☠ E11.0 pirate flag
@@ -4983,9 +5016,9 @@
# Flags subtotal: 275 w/o modifiers
# Status Counts
-# fully-qualified : 3624
-# minimally-qualified : 817
-# unqualified : 252
+# fully-qualified : 3655
+# minimally-qualified : 827
+# unqualified : 242
# component : 9
#EOF
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index 35f0da816..dd65d56ae 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Emoji do
"""
use GenServer
+ alias Pleroma.Emoji.Combinations
alias Pleroma.Emoji.Loader
require Logger
@@ -137,4 +138,17 @@ defmodule Pleroma.Emoji do
end
def is_unicode_emoji?(_), do: false
+
+ emoji_qualification_map =
+ emojis
+ |> Enum.filter(&String.contains?(&1, "\uFE0F"))
+ |> Combinations.variate_emoji_qualification()
+
+ for {qualified, unqualified_list} <- emoji_qualification_map do
+ for unqualified <- unqualified_list do
+ def fully_qualify_emoji(unquote(unqualified)), do: unquote(qualified)
+ end
+ end
+
+ def fully_qualify_emoji(emoji), do: emoji
end
diff --git a/lib/pleroma/emoji/combinations.ex b/lib/pleroma/emoji/combinations.ex
new file mode 100644
index 000000000..981c73596
--- /dev/null
+++ b/lib/pleroma/emoji/combinations.ex
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Emoji.Combinations do
+ # FE0F is the emoji variation sequence. It is used for fully-qualifying
+ # emoji, and that includes emoji combinations.
+ # This code generates combinations per emoji: for each FE0F, all possible
+ # combinations of the character being removed or staying will be generated.
+ # This is made as an attempt to find all partially-qualified and unqualified
+ # versions of a fully-qualified emoji.
+ # I have found *no cases* for which this would be a problem, after browsing
+ # the entire emoji list in emoji-test.txt. This is safe, and, sadly, most
+ # likely sane too.
+
+ defp qualification_combinations(codepoints) do
+ qualification_combinations([[]], codepoints)
+ end
+
+ defp qualification_combinations(acc, []), do: acc
+
+ defp qualification_combinations(acc, ["\uFE0F" | tail]) do
+ acc
+ |> Enum.flat_map(fn x -> [x, x ++ ["\uFE0F"]] end)
+ |> qualification_combinations(tail)
+ end
+
+ defp qualification_combinations(acc, [codepoint | tail]) do
+ acc
+ |> Enum.map(&Kernel.++(&1, [codepoint]))
+ |> qualification_combinations(tail)
+ end
+
+ def variate_emoji_qualification(emoji) when is_binary(emoji) do
+ emoji
+ |> String.codepoints()
+ |> qualification_combinations()
+ |> Enum.map(&List.to_string/1)
+ end
+
+ def variate_emoji_qualification(emoji) when is_list(emoji) do
+ emoji
+ |> Enum.map(fn emoji -> {emoji, variate_emoji_qualification(emoji)} end)
+ end
+end
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
index e6449aa67..15664c876 100644
--- a/lib/pleroma/following_relationship.ex
+++ b/lib/pleroma/following_relationship.ex
@@ -194,12 +194,13 @@ defmodule Pleroma.FollowingRelationship do
|> join(:inner, [r], f in assoc(r, :follower))
|> where(following_id: ^origin.id)
|> where([r, f], f.allow_following_move == true)
+ |> where([r, f], f.local == true)
|> limit(50)
|> preload([:follower])
|> Repo.all()
|> Enum.map(fn following_relationship ->
- Repo.delete(following_relationship)
Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
+ Pleroma.Web.CommonAPI.unfollow(following_relationship.follower, origin)
end)
|> case do
[] ->
diff --git a/lib/pleroma/migrators/context_objects_deletion_migrator.ex b/lib/pleroma/migrators/context_objects_deletion_migrator.ex
new file mode 100644
index 000000000..fb224795a
--- /dev/null
+++ b/lib/pleroma/migrators/context_objects_deletion_migrator.ex
@@ -0,0 +1,139 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Migrators.ContextObjectsDeletionMigrator do
+ defmodule State do
+ use Pleroma.Migrators.Support.BaseMigratorState
+
+ @impl Pleroma.Migrators.Support.BaseMigratorState
+ defdelegate data_migration(), to: Pleroma.DataMigration, as: :delete_context_objects
+ end
+
+ use Pleroma.Migrators.Support.BaseMigrator
+
+ alias Pleroma.Migrators.Support.BaseMigrator
+ alias Pleroma.Object
+
+ @doc "This migration removes objects created exclusively for contexts, containing only an `id` field."
+
+ @impl BaseMigrator
+ def feature_config_path, do: [:features, :delete_context_objects]
+
+ @impl BaseMigrator
+ def fault_rate_allowance, do: Config.get([:delete_context_objects, :fault_rate_allowance], 0)
+
+ @impl BaseMigrator
+ def perform do
+ data_migration_id = data_migration_id()
+ max_processed_id = get_stat(:max_processed_id, 0)
+
+ Logger.info("Deleting context objects from `objects` (from oid: #{max_processed_id})...")
+
+ query()
+ |> where([object], object.id > ^max_processed_id)
+ |> Repo.chunk_stream(100, :batches, timeout: :infinity)
+ |> Stream.each(fn objects ->
+ object_ids = Enum.map(objects, & &1.id)
+
+ results = Enum.map(object_ids, &delete_context_object(&1))
+
+ failed_ids =
+ results
+ |> Enum.filter(&(elem(&1, 0) == :error))
+ |> Enum.map(&elem(&1, 1))
+
+ chunk_affected_count =
+ results
+ |> Enum.filter(&(elem(&1, 0) == :ok))
+ |> length()
+
+ for failed_id <- failed_ids do
+ _ =
+ Repo.query(
+ "INSERT INTO data_migration_failed_ids(data_migration_id, record_id) " <>
+ "VALUES ($1, $2) ON CONFLICT DO NOTHING;",
+ [data_migration_id, failed_id]
+ )
+ end
+
+ _ =
+ Repo.query(
+ "DELETE FROM data_migration_failed_ids " <>
+ "WHERE data_migration_id = $1 AND record_id = ANY($2)",
+ [data_migration_id, object_ids -- failed_ids]
+ )
+
+ max_object_id = Enum.at(object_ids, -1)
+
+ put_stat(:max_processed_id, max_object_id)
+ increment_stat(:iteration_processed_count, length(object_ids))
+ increment_stat(:processed_count, length(object_ids))
+ increment_stat(:failed_count, length(failed_ids))
+ increment_stat(:affected_count, chunk_affected_count)
+ put_stat(:records_per_second, records_per_second())
+ persist_state()
+
+ # A quick and dirty approach to controlling the load this background migration imposes
+ sleep_interval = Config.get([:delete_context_objects, :sleep_interval_ms], 0)
+ Process.sleep(sleep_interval)
+ end)
+ |> Stream.run()
+ end
+
+ @impl BaseMigrator
+ def query do
+ # Context objects have no activity type, and only one field, `id`.
+ # Only those context objects are without types.
+ from(
+ object in Object,
+ where: fragment("(?)->'type' IS NULL", object.data),
+ select: %{
+ id: object.id
+ }
+ )
+ end
+
+ @spec delete_context_object(integer()) :: {:ok | :error, integer()}
+ defp delete_context_object(id) do
+ result =
+ %Object{id: id}
+ |> Repo.delete()
+ |> elem(0)
+
+ {result, id}
+ end
+
+ @impl BaseMigrator
+ def retry_failed do
+ data_migration_id = data_migration_id()
+
+ failed_objects_query()
+ |> Repo.chunk_stream(100, :one)
+ |> Stream.each(fn object ->
+ with {res, _} when res != :error <- delete_context_object(object.id) do
+ _ =
+ Repo.query(
+ "DELETE FROM data_migration_failed_ids " <>
+ "WHERE data_migration_id = $1 AND record_id = $2",
+ [data_migration_id, object.id]
+ )
+ end
+ end)
+ |> Stream.run()
+
+ put_stat(:failed_count, failures_count())
+ persist_state()
+
+ force_continue()
+ end
+
+ defp failed_objects_query do
+ from(o in Object)
+ |> join(:inner, [o], dmf in fragment("SELECT * FROM data_migration_failed_ids"),
+ on: dmf.record_id == o.id
+ )
+ |> where([_o, dmf], dmf.data_migration_id == ^data_migration_id())
+ |> order_by([o], asc: o.id)
+ end
+end
diff --git a/lib/pleroma/migrators/hashtags_table_migrator.ex b/lib/pleroma/migrators/hashtags_table_migrator.ex
index fa1190b7d..dca4bfa6f 100644
--- a/lib/pleroma/migrators/hashtags_table_migrator.ex
+++ b/lib/pleroma/migrators/hashtags_table_migrator.ex
@@ -183,7 +183,7 @@ defmodule Pleroma.Migrators.HashtagsTableMigrator do
DELETE FROM hashtags_objects WHERE object_id IN
(SELECT DISTINCT objects.id FROM objects
JOIN hashtags_objects ON hashtags_objects.object_id = objects.id LEFT JOIN activities
- ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') =
+ ON associated_object_id(activities) =
(objects.data->>'id')
AND activities.data->>'type' = 'Create'
WHERE activities.id IS NULL);
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 52fd2656b..c2d4d86a3 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -117,9 +117,8 @@ defmodule Pleroma.Notification do
|> join(:left, [n, a], object in Object,
on:
fragment(
- "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
+ "(?->>'id') = associated_object_id(?)",
object.data,
- a.data,
a.data
)
)
@@ -193,13 +192,11 @@ defmodule Pleroma.Notification do
|> join(:left, [n, a], mutated_activity in Pleroma.Activity,
on:
fragment(
- "COALESCE((?->'object')->>'id', ?->>'object')",
- a.data,
+ "associated_object_id(?)",
a.data
) ==
fragment(
- "COALESCE((?->'object')->>'id', ?->>'object')",
- mutated_activity.data,
+ "associated_object_id(?)",
mutated_activity.data
) and
fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
@@ -385,7 +382,7 @@ defmodule Pleroma.Notification do
end
def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
- when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag"] do
+ when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do
do_create_notifications(activity, options)
end
@@ -439,6 +436,9 @@ defmodule Pleroma.Notification do
activity
|> type_from_activity_object()
+ "Update" ->
+ "update"
+
t ->
raise "No notification type for activity type #{t}"
end
@@ -513,7 +513,16 @@ defmodule Pleroma.Notification do
def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
- when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact", "Flag"] do
+ when type in [
+ "Create",
+ "Like",
+ "Announce",
+ "Follow",
+ "Move",
+ "EmojiReact",
+ "Flag",
+ "Update"
+ ] do
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
potential_receivers =
@@ -553,6 +562,21 @@ defmodule Pleroma.Notification do
(User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor]
end
+ # Update activity: notify all who repeated this
+ def get_potential_receiver_ap_ids(%{data: %{"type" => "Update", "actor" => actor}} = activity) do
+ with %Object{data: %{"id" => object_id}} <- Object.normalize(activity, fetch: false) do
+ repeaters =
+ Activity.Queries.by_type("Announce")
+ |> Activity.Queries.by_object_id(object_id)
+ |> Activity.with_joined_user_actor()
+ |> where([a, u], u.local)
+ |> select([a, u], u.ap_id)
+ |> Repo.all()
+
+ repeaters -- [actor]
+ end
+ end
+
def get_potential_receiver_ap_ids(activity) do
[]
|> Utils.maybe_notify_to_recipients(activity)
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index fe264b5e0..38accae5d 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -40,8 +40,7 @@ defmodule Pleroma.Object do
join(query, join_type, [{object, object_position}], a in Activity,
on:
fragment(
- "COALESCE(?->'object'->>'id', ?->>'object') = (? ->> 'id') AND (?->>'type' = ?) ",
- a.data,
+ "associated_object_id(?) = (? ->> 'id') AND (?->>'type' = ?) ",
a.data,
object.data,
a.data,
@@ -145,7 +144,7 @@ defmodule Pleroma.Object do
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
end
- def normalize(_, options \\ [fetch: false])
+ def normalize(_, options \\ [fetch: false, id_only: false])
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
# Use this whenever possible, especially when walking graphs in an O(N) loop!
@@ -173,10 +172,15 @@ defmodule Pleroma.Object do
def normalize(%{"id" => ap_id}, options), do: normalize(ap_id, options)
def normalize(ap_id, options) when is_binary(ap_id) do
- if Keyword.get(options, :fetch) do
- Fetcher.fetch_object_from_id!(ap_id, options)
- else
- get_cached_by_ap_id(ap_id)
+ cond do
+ Keyword.get(options, :id_only) ->
+ ap_id
+
+ Keyword.get(options, :fetch) ->
+ Fetcher.fetch_object_from_id!(ap_id, options)
+
+ true ->
+ get_cached_by_ap_id(ap_id)
end
end
@@ -208,10 +212,6 @@ defmodule Pleroma.Object do
end
end
- def context_mapping(context) do
- Object.change(%Object{}, %{data: %{"id" => context}})
- end
-
def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
%ObjectTombstone{
id: id,
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index deb3dc711..d81fdcf24 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -26,8 +26,42 @@ defmodule Pleroma.Object.Fetcher do
end
defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do
+ has_history? = fn
+ %{"formerRepresentations" => %{"orderedItems" => list}} when is_list(list) -> true
+ _ -> false
+ end
+
internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
+ remote_history_exists? = has_history?.(new_data)
+
+ # If the remote history exists, we treat that as the only source of truth.
+ new_data =
+ if has_history?.(old_data) and not remote_history_exists? do
+ Map.put(new_data, "formerRepresentations", old_data["formerRepresentations"])
+ else
+ new_data
+ end
+
+ # If the remote does not have history information, we need to manage it ourselves
+ new_data =
+ if not remote_history_exists? do
+ changed? =
+ Pleroma.Constants.status_updatable_fields()
+ |> Enum.any?(fn field -> Map.get(old_data, field) != Map.get(new_data, field) end)
+
+ %{updated_object: updated_object} =
+ new_data
+ |> Object.Updater.maybe_update_history(old_data,
+ updated: changed?,
+ use_history_in_new_object?: false
+ )
+
+ updated_object
+ else
+ new_data
+ end
+
Map.merge(new_data, internal_fields)
end
diff --git a/lib/pleroma/object/updater.ex b/lib/pleroma/object/updater.ex
new file mode 100644
index 000000000..ab38d3ed2
--- /dev/null
+++ b/lib/pleroma/object/updater.ex
@@ -0,0 +1,240 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Object.Updater do
+ require Pleroma.Constants
+
+ def update_content_fields(orig_object_data, updated_object) do
+ Pleroma.Constants.status_updatable_fields()
+ |> Enum.reduce(
+ %{data: orig_object_data, updated: false},
+ fn field, %{data: data, updated: updated} ->
+ updated =
+ updated or
+ (field != "updated" and
+ Map.get(updated_object, field) != Map.get(orig_object_data, field))
+
+ data =
+ if Map.has_key?(updated_object, field) do
+ Map.put(data, field, updated_object[field])
+ else
+ Map.drop(data, [field])
+ end
+
+ %{data: data, updated: updated}
+ end
+ )
+ end
+
+ def maybe_history(object) do
+ with history <- Map.get(object, "formerRepresentations"),
+ true <- is_map(history),
+ "OrderedCollection" <- Map.get(history, "type"),
+ true <- is_list(Map.get(history, "orderedItems")),
+ true <- is_integer(Map.get(history, "totalItems")) do
+ history
+ else
+ _ -> nil
+ end
+ end
+
+ def history_for(object) do
+ with history when not is_nil(history) <- maybe_history(object) do
+ history
+ else
+ _ -> history_skeleton()
+ end
+ end
+
+ defp history_skeleton do
+ %{
+ "type" => "OrderedCollection",
+ "totalItems" => 0,
+ "orderedItems" => []
+ }
+ end
+
+ def maybe_update_history(
+ updated_object,
+ orig_object_data,
+ opts
+ ) do
+ updated = opts[:updated]
+ use_history_in_new_object? = opts[:use_history_in_new_object?]
+
+ if not updated do
+ %{updated_object: updated_object, used_history_in_new_object?: false}
+ else
+ # Put edit history
+ # Note that we may have got the edit history by first fetching the object
+ {new_history, used_history_in_new_object?} =
+ with true <- use_history_in_new_object?,
+ updated_history when not is_nil(updated_history) <- maybe_history(opts[:new_data]) do
+ {updated_history, true}
+ else
+ _ ->
+ history = history_for(orig_object_data)
+
+ latest_history_item =
+ orig_object_data
+ |> Map.drop(["id", "formerRepresentations"])
+
+ updated_history =
+ history
+ |> Map.put("orderedItems", [latest_history_item | history["orderedItems"]])
+ |> Map.put("totalItems", history["totalItems"] + 1)
+
+ {updated_history, false}
+ end
+
+ updated_object =
+ updated_object
+ |> Map.put("formerRepresentations", new_history)
+
+ %{updated_object: updated_object, used_history_in_new_object?: used_history_in_new_object?}
+ end
+ end
+
+ defp maybe_update_poll(to_be_updated, updated_object) do
+ choice_key = fn data ->
+ if Map.has_key?(data, "anyOf"), do: "anyOf", else: "oneOf"
+ end
+
+ with true <- to_be_updated["type"] == "Question",
+ key <- choice_key.(updated_object),
+ true <- key == choice_key.(to_be_updated),
+ orig_choices <- to_be_updated[key] |> Enum.map(&Map.drop(&1, ["replies"])),
+ new_choices <- updated_object[key] |> Enum.map(&Map.drop(&1, ["replies"])),
+ true <- orig_choices == new_choices do
+ # Choices are the same, but counts are different
+ to_be_updated
+ |> Map.put(key, updated_object[key])
+ else
+ # Choices (or vote type) have changed, do not allow this
+ _ -> to_be_updated
+ end
+ end
+
+ # This calculates the data to be sent as the object of an Update.
+ # new_data's formerRepresentations is not considered.
+ # formerRepresentations is added to the returned data.
+ def make_update_object_data(original_data, new_data, date) do
+ %{data: updated_data, updated: updated} =
+ original_data
+ |> update_content_fields(new_data)
+
+ if not updated do
+ updated_data
+ else
+ %{updated_object: updated_data} =
+ updated_data
+ |> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false)
+
+ updated_data
+ |> Map.put("updated", date)
+ end
+ end
+
+ # This calculates the data of the new Object from an Update.
+ # new_data's formerRepresentations is considered.
+ def make_new_object_data_from_update_object(original_data, new_data) do
+ update_is_reasonable =
+ with {_, updated} when not is_nil(updated) <- {:cur_updated, new_data["updated"]},
+ {_, {:ok, updated_time, _}} <- {:cur_updated, DateTime.from_iso8601(updated)},
+ {_, last_updated} when not is_nil(last_updated) <-
+ {:last_updated, original_data["updated"] || original_data["published"]},
+ {_, {:ok, last_updated_time, _}} <-
+ {:last_updated, DateTime.from_iso8601(last_updated)},
+ :gt <- DateTime.compare(updated_time, last_updated_time) do
+ :update_everything
+ else
+ # only allow poll updates
+ {:cur_updated, _} -> :no_content_update
+ :eq -> :no_content_update
+ # allow all updates
+ {:last_updated, _} -> :update_everything
+ # allow no updates
+ _ -> false
+ end
+
+ %{
+ updated_object: updated_data,
+ used_history_in_new_object?: used_history_in_new_object?,
+ updated: updated
+ } =
+ if update_is_reasonable == :update_everything do
+ %{data: updated_data, updated: updated} =
+ original_data
+ |> update_content_fields(new_data)
+
+ updated_data
+ |> maybe_update_history(original_data,
+ updated: updated,
+ use_history_in_new_object?: true,
+ new_data: new_data
+ )
+ |> Map.put(:updated, updated)
+ else
+ %{
+ updated_object: original_data,
+ used_history_in_new_object?: false,
+ updated: false
+ }
+ end
+
+ updated_data =
+ if update_is_reasonable != false do
+ updated_data
+ |> maybe_update_poll(new_data)
+ else
+ updated_data
+ end
+
+ %{
+ updated_data: updated_data,
+ updated: updated,
+ used_history_in_new_object?: used_history_in_new_object?
+ }
+ end
+
+ def for_each_history_item(%{"orderedItems" => items} = history, _object, fun) do
+ new_items =
+ Enum.map(items, fun)
+ |> Enum.reduce_while(
+ {:ok, []},
+ fn
+ {:ok, item}, {:ok, acc} -> {:cont, {:ok, acc ++ [item]}}
+ e, _acc -> {:halt, e}
+ end
+ )
+
+ case new_items do
+ {:ok, items} -> {:ok, Map.put(history, "orderedItems", items)}
+ e -> e
+ end
+ end
+
+ def for_each_history_item(history, _, _) do
+ {:ok, history}
+ end
+
+ def do_with_history(object, fun) do
+ with history <- object["formerRepresentations"],
+ object <- Map.drop(object, ["formerRepresentations"]),
+ {_, {:ok, object}} <- {:main_body, fun.(object)},
+ {_, {:ok, history}} <- {:history_items, for_each_history_item(history, object, fun)} do
+ object =
+ if history do
+ Map.put(object, "formerRepresentations", history)
+ else
+ object
+ end
+
+ {:ok, object}
+ else
+ {:main_body, e} -> e
+ {:history_items, e} -> e
+ end
+ end
+end
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
index dbe6fd209..5cfdae051 100644
--- a/lib/pleroma/signature.ex
+++ b/lib/pleroma/signature.ex
@@ -10,17 +10,14 @@ defmodule Pleroma.Signature do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ @known_suffixes ["/publickey", "/main-key"]
+
def key_id_to_actor_id(key_id) do
uri =
- URI.parse(key_id)
+ key_id
+ |> URI.parse()
|> Map.put(:fragment, nil)
-
- uri =
- if not is_nil(uri.path) and String.ends_with?(uri.path, "/publickey") do
- Map.put(uri, :path, String.replace(uri.path, "/publickey", ""))
- else
- uri
- end
+ |> remove_suffix(@known_suffixes)
maybe_ap_id = URI.to_string(uri)
@@ -36,6 +33,16 @@ defmodule Pleroma.Signature do
end
end
+ defp remove_suffix(uri, [test | rest]) do
+ if not is_nil(uri.path) and String.ends_with?(uri.path, test) do
+ Map.put(uri, :path, String.replace(uri.path, test, ""))
+ else
+ remove_suffix(uri, rest)
+ end
+ end
+
+ defp remove_suffix(uri, []), do: uri
+
def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:ok, actor_id} <- key_id_to_actor_id(kid),
@@ -59,9 +66,8 @@ defmodule Pleroma.Signature do
end
end
- def sign(%User{} = user, headers) do
- with {:ok, %{keys: keys}} <- User.ensure_keys_present(user),
- {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
+ def sign(%User{keys: keys} = user, headers) do
+ with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
end
end
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 242813dcd..4aee9326f 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -36,6 +36,7 @@ defmodule Pleroma.Upload do
alias Ecto.UUID
alias Pleroma.Config
alias Pleroma.Maps
+ alias Pleroma.Web.ActivityPub.Utils
require Logger
@type source ::
@@ -60,12 +61,23 @@ defmodule Pleroma.Upload do
width: integer(),
height: integer(),
blurhash: String.t(),
+ description: String.t(),
path: String.t()
}
- defstruct [:id, :name, :tempfile, :content_type, :width, :height, :blurhash, :path]
-
- defp get_description(opts, upload) do
- case {opts[:description], Pleroma.Config.get([Pleroma.Upload, :default_description])} do
+ defstruct [
+ :id,
+ :name,
+ :tempfile,
+ :content_type,
+ :width,
+ :height,
+ :blurhash,
+ :description,
+ :path
+ ]
+
+ defp get_description(upload) do
+ case {upload.description, Pleroma.Config.get([Pleroma.Upload, :default_description])} do
{description, _} when is_binary(description) -> description
{_, :filename} -> upload.name
{_, str} when is_binary(str) -> str
@@ -81,13 +93,14 @@ defmodule Pleroma.Upload do
with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
- description = get_description(opts, upload),
+ description = get_description(upload),
{_, true} <-
{:description_limit,
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok,
%{
+ "id" => Utils.generate_object_id(),
"type" => opts.activity_type,
"mediaType" => upload.content_type,
"url" => [
@@ -152,7 +165,8 @@ defmodule Pleroma.Upload do
id: UUID.generate(),
name: file.filename,
tempfile: file.path,
- content_type: file.content_type
+ content_type: file.content_type,
+ description: opts.description
}}
end
end
@@ -172,7 +186,8 @@ defmodule Pleroma.Upload do
id: UUID.generate(),
name: hash <> "." <> ext,
tempfile: tmp_path,
- content_type: content_type
+ content_type: content_type,
+ description: opts.description
}}
end
end
diff --git a/lib/pleroma/upload/filter/exiftool/read_description.ex b/lib/pleroma/upload/filter/exiftool/read_description.ex
new file mode 100644
index 000000000..03d698a81
--- /dev/null
+++ b/lib/pleroma/upload/filter/exiftool/read_description.ex
@@ -0,0 +1,49 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.Exiftool.ReadDescription do
+ @moduledoc """
+ Gets a valid description from the related EXIF tags and provides them in the response if no description is provided yet.
+ It will first check ImageDescription, when that doesn't probide a valid description, it will check iptc:Caption-Abstract.
+ A valid description means the fields are filled in and not too long (see `:instance, :description_limit`).
+ """
+ @behaviour Pleroma.Upload.Filter
+
+ @spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()}
+
+ def filter(%Pleroma.Upload{description: description})
+ when is_binary(description),
+ do: {:ok, :noop}
+
+ def filter(%Pleroma.Upload{tempfile: file} = upload),
+ do: {:ok, :filtered, upload |> Map.put(:description, read_description_from_exif_data(file))}
+
+ def filter(_, _), do: {:ok, :noop}
+
+ defp read_description_from_exif_data(file) do
+ nil
+ |> read_when_empty(file, "-ImageDescription")
+ |> read_when_empty(file, "-iptc:Caption-Abstract")
+ end
+
+ defp read_when_empty(current_description, _, _) when is_binary(current_description),
+ do: current_description
+
+ defp read_when_empty(_, file, tag) do
+ try do
+ {tag_content, 0} =
+ System.cmd("exiftool", ["-b", "-s3", tag, file], stderr_to_stdout: true, parallelism: true)
+
+ tag_content = String.trim(tag_content)
+
+ if tag_content != "" and
+ String.length(tag_content) <=
+ Pleroma.Config.get([:instance, :description_limit]),
+ do: tag_content,
+ else: nil
+ rescue
+ _ in ErlangError -> nil
+ end
+ end
+end
diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool/strip_location.ex
index 36cc045c2..6100527d3 100644
--- a/lib/pleroma/upload/filter/exiftool.ex
+++ b/lib/pleroma/upload/filter/exiftool/strip_location.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Upload.Filter.Exiftool do
+defmodule Pleroma.Upload.Filter.Exiftool.StripLocation do
@moduledoc """
Strips GPS related EXIF tags and overwrites the file in place.
Also strips or replaces filesystem metadata e.g., timestamps.
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 747a83e8d..85d3382cb 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -706,11 +706,12 @@ defmodule Pleroma.User do
])
|> validate_required([:name, :nickname])
|> unique_constraint(:nickname)
- |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
+ |> validate_not_restricted_nickname(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_and_featured_address()
+ |> put_private_key()
end
def register_changeset(struct, params \\ %{}, opts \\ []) do
@@ -754,17 +755,9 @@ defmodule Pleroma.User do
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
- |> validate_change(:email, fn :email, email ->
- valid? =
- Config.get([User, :email_blacklist])
- |> Enum.all?(fn blacklisted_domain ->
- !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
- end)
-
- if valid?, do: [], else: [email: "Invalid email"]
- end)
+ |> validate_email_not_in_blacklisted_domain(:email)
|> unique_constraint(:nickname)
- |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
+ |> validate_not_restricted_nickname(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
@@ -776,6 +769,36 @@ defmodule Pleroma.User do
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_and_featured_address()
+ |> put_private_key()
+ end
+
+ def validate_not_restricted_nickname(changeset, field) do
+ validate_change(changeset, field, fn _, value ->
+ valid? =
+ Config.get([User, :restricted_nicknames])
+ |> Enum.all?(fn restricted_nickname ->
+ String.downcase(value) != String.downcase(restricted_nickname)
+ end)
+
+ if valid?, do: [], else: [nickname: "Invalid nickname"]
+ end)
+ end
+
+ def validate_email_not_in_blacklisted_domain(changeset, field) do
+ validate_change(changeset, field, fn _, value ->
+ valid? =
+ Config.get([User, :email_blacklist])
+ |> Enum.all?(fn blacklisted_domain ->
+ blacklisted_domain_downcase = String.downcase(blacklisted_domain)
+
+ !String.ends_with?(String.downcase(value), [
+ "@" <> blacklisted_domain_downcase,
+ "." <> blacklisted_domain_downcase
+ ])
+ end)
+
+ if valid?, do: [], else: [email: "Invalid email"]
+ end)
end
def maybe_validate_required_email(changeset, true), do: changeset
@@ -825,6 +848,11 @@ defmodule Pleroma.User do
|> put_change(:featured_address, featured)
end
+ defp put_private_key(changeset) do
+ {:ok, pem} = Keys.generate_rsa_pem()
+ put_change(changeset, :keys, pem)
+ end
+
defp autofollow_users(user) do
candidates = Config.get([:instance, :autofollowed_nicknames])
@@ -1459,17 +1487,30 @@ defmodule Pleroma.User do
{:ok, list(UserRelationship.t())} | {:error, String.t()}
def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
notifications? = Map.get(params, :notifications, true)
- expires_in = Map.get(params, :expires_in, 0)
+ duration = Map.get(params, :duration, 0)
- with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
+ expires_at =
+ if duration > 0 do
+ DateTime.utc_now()
+ |> DateTime.add(duration)
+ else
+ nil
+ end
+
+ with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee, expires_at),
{:ok, user_notification_mute} <-
- (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
+ (notifications? &&
+ UserRelationship.create_notification_mute(
+ muter,
+ mutee,
+ expires_at
+ )) ||
{:ok, nil} do
- if expires_in > 0 do
+ if duration > 0 do
Pleroma.Workers.MuteExpireWorker.enqueue(
"unmute_user",
%{"muter_id" => muter.id, "mutee_id" => mutee.id},
- schedule_in: expires_in
+ scheduled_at: expires_at
)
end
@@ -1540,13 +1581,19 @@ defmodule Pleroma.User do
blocker
end
- # clear any requested follows as well
+ # clear any requested follows from both sides as well
blocked =
case CommonAPI.reject_follow_request(blocked, blocker) do
{:ok, %User{} = updated_blocked} -> updated_blocked
nil -> blocked
end
+ blocker =
+ case CommonAPI.reject_follow_request(blocker, blocked) do
+ {:ok, %User{} = updated_blocker} -> updated_blocker
+ nil -> blocker
+ end
+
unsubscribe(blocked, blocker)
unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
@@ -2046,6 +2093,7 @@ defmodule Pleroma.User do
follower_address: uri <> "/followers"
}
|> change
+ |> put_private_key()
|> unique_constraint(:nickname)
|> Repo.insert()
|> set_cache()
@@ -2311,17 +2359,6 @@ defmodule Pleroma.User do
}
end
- def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
-
- def ensure_keys_present(%User{} = user) do
- with {:ok, pem} <- Keys.generate_rsa_pem() do
- user
- |> cast(%{keys: pem}, [:keys])
- |> validate_required([:keys])
- |> update_and_set_cache()
- end
- end
-
def get_ap_ids_by_nicknames(nicknames) do
from(u in User,
where: u.nickname in ^nicknames,
@@ -2364,6 +2401,38 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
+ def alias_users(user) do
+ user.also_known_as
+ |> Enum.map(&User.get_cached_by_ap_id/1)
+ |> Enum.filter(fn user -> user != nil end)
+ end
+
+ def add_alias(user, new_alias_user) do
+ current_aliases = user.also_known_as || []
+ new_alias_ap_id = new_alias_user.ap_id
+
+ if new_alias_ap_id in current_aliases do
+ {:ok, user}
+ else
+ user
+ |> cast(%{also_known_as: current_aliases ++ [new_alias_ap_id]}, [:also_known_as])
+ |> update_and_set_cache()
+ end
+ end
+
+ def delete_alias(user, alias_user) do
+ current_aliases = user.also_known_as || []
+ alias_ap_id = alias_user.ap_id
+
+ if alias_ap_id in current_aliases do
+ user
+ |> cast(%{also_known_as: current_aliases -- [alias_ap_id]}, [:also_known_as])
+ |> update_and_set_cache()
+ else
+ {:error, :no_such_alias}
+ end
+ end
+
# Internal function; public one is `deactivate/2`
defp set_activation_status(user, status) do
user
diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex
index 9cb329663..9df010605 100644
--- a/lib/pleroma/user/backup.ex
+++ b/lib/pleroma/user/backup.ex
@@ -32,9 +32,7 @@ defmodule Pleroma.User.Backup do
end
def create(user, admin_id \\ nil) do
- with :ok <- validate_email_enabled(),
- :ok <- validate_user_email(user),
- :ok <- validate_limit(user, admin_id),
+ with :ok <- validate_limit(user, admin_id),
{:ok, backup} <- user |> new() |> Repo.insert() do
BackupWorker.process(backup, admin_id)
end
@@ -86,20 +84,6 @@ defmodule Pleroma.User.Backup do
end
end
- defp validate_email_enabled do
- if Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
- :ok
- else
- {:error, dgettext("errors", "Backups require enabled email")}
- end
- end
-
- defp validate_user_email(%User{email: nil}) do
- {:error, dgettext("errors", "Email is required")}
- end
-
- defp validate_user_email(%User{email: email}) when is_binary(email), do: :ok
-
def get_last(user_id) do
__MODULE__
|> where(user_id: ^user_id)
diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex
index 1432a1d83..fbecf3129 100644
--- a/lib/pleroma/user_relationship.ex
+++ b/lib/pleroma/user_relationship.ex
@@ -18,16 +18,17 @@ defmodule Pleroma.UserRelationship do
belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
field(:relationship_type, Pleroma.UserRelationship.Type)
+ field(:expires_at, :utc_datetime)
timestamps(updated_at: false)
end
for relationship_type <- Keyword.keys(Pleroma.UserRelationship.Type.__enum_map__()) do
- # `def create_block/2`, `def create_mute/2`, `def create_reblog_mute/2`,
- # `def create_notification_mute/2`, `def create_inverse_subscription/2`,
- # `def endorsement/2`
- def unquote(:"create_#{relationship_type}")(source, target),
- do: create(unquote(relationship_type), source, target)
+ # `def create_block/3`, `def create_mute/3`, `def create_reblog_mute/3`,
+ # `def create_notification_mute/3`, `def create_inverse_subscription/3`,
+ # `def endorsement/3`
+ def unquote(:"create_#{relationship_type}")(source, target, expires_at \\ nil),
+ do: create(unquote(relationship_type), source, target, expires_at)
# `def delete_block/2`, `def delete_mute/2`, `def delete_reblog_mute/2`,
# `def delete_notification_mute/2`, `def delete_inverse_subscription/2`,
@@ -37,9 +38,15 @@ defmodule Pleroma.UserRelationship do
# `def block_exists?/2`, `def mute_exists?/2`, `def reblog_mute_exists?/2`,
# `def notification_mute_exists?/2`, `def inverse_subscription_exists?/2`,
- # `def inverse_endorsement?/2`
+ # `def inverse_endorsement_exists?/2`
def unquote(:"#{relationship_type}_exists?")(source, target),
do: exists?(unquote(relationship_type), source, target)
+
+ # `def get_block_expire_date/2`, `def get_mute_expire_date/2`,
+ # `def get_reblog_mute_expire_date/2`, `def get_notification_mute_exists?/2`,
+ # `def get_inverse_subscription_expire_date/2`, `def get_inverse_endorsement_expire_date/2`
+ def unquote(:"get_#{relationship_type}_expire_date")(source, target),
+ do: get_expire_date(unquote(relationship_type), source, target)
end
def user_relationship_types, do: Keyword.keys(user_relationship_mappings())
@@ -48,7 +55,7 @@ defmodule Pleroma.UserRelationship do
def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do
user_relationship
- |> cast(params, [:relationship_type, :source_id, :target_id])
+ |> cast(params, [:relationship_type, :source_id, :target_id, :expires_at])
|> validate_required([:relationship_type, :source_id, :target_id])
|> unique_constraint(:relationship_type,
name: :user_relationships_source_id_relationship_type_target_id_index
@@ -62,16 +69,31 @@ defmodule Pleroma.UserRelationship do
|> Repo.exists?()
end
- def create(relationship_type, %User{} = source, %User{} = target) do
+ def get_expire_date(relationship_type, %User{} = source, %User{} = target) do
+ %UserRelationship{expires_at: expires_at} =
+ UserRelationship
+ |> where(
+ relationship_type: ^relationship_type,
+ source_id: ^source.id,
+ target_id: ^target.id
+ )
+ |> Repo.one!()
+
+ expires_at
+ end
+
+ def create(relationship_type, %User{} = source, %User{} = target, expires_at \\ nil) do
%UserRelationship{}
|> changeset(%{
relationship_type: relationship_type,
source_id: source.id,
- target_id: target.id
+ target_id: target.id,
+ expires_at: expires_at
})
|> Repo.insert(
- on_conflict: {:replace_all_except, [:id]},
- conflict_target: [:source_id, :relationship_type, :target_id]
+ on_conflict: {:replace_all_except, [:id, :inserted_at]},
+ conflict_target: [:source_id, :relationship_type, :target_id],
+ returning: true
)
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 064f93b22..a5d7036d9 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -190,7 +190,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def notify_and_stream(activity) do
Notification.create_notifications(activity)
- conversation = create_or_bump_conversation(activity, activity.actor)
+ original_activity =
+ case activity do
+ %{data: %{"type" => "Update"}, object: %{data: %{"id" => id}}} ->
+ Activity.get_create_by_object_ap_id_with_object(id)
+
+ _ ->
+ activity
+ end
+
+ conversation = create_or_bump_conversation(original_activity, original_activity.actor)
participations = get_participations(conversation)
stream_out(activity)
stream_out_participations(participations)
@@ -256,7 +265,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
@impl true
def stream_out(%Activity{data: %{"type" => data_type}} = activity)
- when data_type in ["Create", "Announce", "Delete"] do
+ when data_type in ["Create", "Announce", "Delete", "Update"] do
activity
|> Topics.get_activity_topics()
|> Streamer.stream(activity)
@@ -413,7 +422,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"type" => "Move",
"actor" => origin.ap_id,
"object" => origin.ap_id,
- "target" => target.ap_id
+ "target" => target.ap_id,
+ "to" => [origin.follower_address]
}
with true <- origin.ap_id in target.also_known_as,
@@ -501,9 +511,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
@spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
+ includes_local_public = Map.get(opts, :includes_local_public, false)
+
opts = Map.delete(opts, :user)
- [Constants.as_public()]
+ intended_recipients =
+ if includes_local_public do
+ [Constants.as_public(), as_local_public()]
+ else
+ [Constants.as_public()]
+ end
+
+ intended_recipients
|> fetch_activities_query(opts)
|> restrict_unlisted(opts)
|> fetch_paginated_optimized(opts, pagination)
@@ -603,9 +622,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
do: query
defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do
+ local_public = as_local_public()
+
from(
a in query,
- where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
+ where: fragment("thread_visibility(?, (?)->>'id', ?) = true", ^ap_id, a.data, ^local_public)
)
end
@@ -692,8 +713,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp user_activities_recipients(%{godmode: true}), do: []
defp user_activities_recipients(%{reading_user: reading_user}) do
- if reading_user do
- [Constants.as_public(), reading_user.ap_id | User.following(reading_user)]
+ if not is_nil(reading_user) and reading_user.local do
+ [
+ Constants.as_public(),
+ as_local_public(),
+ reading_user.ap_id | User.following(reading_user)
+ ]
else
[Constants.as_public()]
end
@@ -1134,8 +1159,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
[activity, object: o] in query,
where:
fragment(
- "(?)->>'type' = 'Create' and coalesce((?)->'object'->>'id', (?)->>'object') = any (?)",
- activity.data,
+ "(?)->>'type' = 'Create' and associated_object_id((?)) = any (?)",
activity.data,
activity.data,
^ids
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index b8f63d69d..1357c379c 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -66,8 +66,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def user(conn, %{"nickname" => nickname}) do
- with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
- {:ok, user} <- User.ensure_keys_present(user) do
+ with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
@@ -174,7 +173,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
- {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
{:show_follows, true} <-
{:show_follows, (for_user && for_user == user) || !user.hide_follows} do
{page, _} = Integer.parse(page)
@@ -192,8 +190,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
- with %User{} = user <- User.get_cached_by_nickname(nickname),
- {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
+ with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
@@ -213,7 +210,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
- {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
{:show_followers, true} <-
{:show_followers, (for_user && for_user == user) || !user.hide_followers} do
{page, _} = Integer.parse(page)
@@ -231,8 +227,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
- with %User{} = user <- User.get_cached_by_nickname(nickname),
- {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
+ with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
@@ -245,8 +240,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
%{"nickname" => nickname, "page" => page?} = params
)
when page? in [true, "true"] do
- with %User{} = user <- User.get_cached_by_nickname(nickname),
- {:ok, user} <- User.ensure_keys_present(user) do
+ with %User{} = user <- User.get_cached_by_nickname(nickname) do
# "include_poll_votes" is a hack because postgres generates inefficient
# queries when filtering by 'Answer', poll votes will be hidden by the
# visibility filter in this case anyway
@@ -270,8 +264,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def outbox(conn, %{"nickname" => nickname}) do
- with %User{} = user <- User.get_cached_by_nickname(nickname),
- {:ok, user} <- User.ensure_keys_present(user) do
+ with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
@@ -328,14 +321,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
defp represent_service_actor(%User{} = user, conn) do
- with {:ok, user} <- User.ensure_keys_present(user) do
- conn
- |> put_resp_content_type("application/activity+json")
- |> put_view(UserView)
- |> render("user.json", %{user: user})
- else
- nil -> {:error, :not_found}
- end
+ conn
+ |> put_resp_content_type("application/activity+json")
+ |> put_view(UserView)
+ |> render("user.json", %{user: user})
end
defp represent_service_actor(nil, _), do: {:error, :not_found}
@@ -388,12 +377,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
"nickname" => nickname
}) do
- with {:ok, user} <- User.ensure_keys_present(user) do
- conn
- |> put_resp_content_type("application/activity+json")
- |> put_view(UserView)
- |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
- end
+ conn
+ |> put_resp_content_type("application/activity+json")
+ |> put_view(UserView)
+ |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
end
def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
@@ -530,19 +517,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
conn
end
- defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
- {:ok, new_user} = User.ensure_keys_present(user)
-
- for_user =
- if new_user != user and match?(%User{}, for_user) do
- User.get_cached_by_nickname(for_user.nickname)
- else
- for_user
- end
-
- {new_user, for_user}
- end
-
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
ActivityPub.upload(
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index 5b25138a4..532047599 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -218,10 +218,16 @@ defmodule Pleroma.Web.ActivityPub.Builder 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,
%{
@@ -229,7 +235,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do
"type" => "Update",
"actor" => actor.ap_id,
"object" => object,
- "to" => to
+ "to" => to,
+ "cc" => cc
}, []}
end
diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex
index 323ecdbf1..ff9f84497 100644
--- a/lib/pleroma/web/activity_pub/mrf.ex
+++ b/lib/pleroma/web/activity_pub/mrf.ex
@@ -53,10 +53,53 @@ defmodule Pleroma.Web.ActivityPub.MRF do
@required_description_keys [:key, :related_policy]
+ def filter_one(policy, message) do
+ should_plug_history? =
+ if function_exported?(policy, :history_awareness, 0) do
+ policy.history_awareness()
+ else
+ :manual
+ end
+ |> Kernel.==(:auto)
+
+ if not should_plug_history? do
+ policy.filter(message)
+ else
+ main_result = policy.filter(message)
+
+ with {_, {:ok, main_message}} <- {:main, main_result},
+ {_,
+ %{
+ "formerRepresentations" => %{
+ "orderedItems" => [_ | _]
+ }
+ }} = {_, object} <- {:object, message["object"]},
+ {_, {:ok, new_history}} <-
+ {:history,
+ Pleroma.Object.Updater.for_each_history_item(
+ object["formerRepresentations"],
+ object,
+ fn item ->
+ with {:ok, filtered} <- policy.filter(Map.put(message, "object", item)) do
+ {:ok, filtered["object"]}
+ else
+ e -> e
+ end
+ end
+ )} do
+ {:ok, put_in(main_message, ["object", "formerRepresentations"], new_history)}
+ else
+ {:main, _} -> main_result
+ {:object, _} -> main_result
+ {:history, e} -> e
+ end
+ end
+ end
+
def filter(policies, %{} = message) do
policies
|> Enum.reduce({:ok, message}, fn
- policy, {:ok, message} -> policy.filter(message)
+ policy, {:ok, message} -> filter_one(policy, message)
_, error -> error
end)
end
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
index f0504ead4..3ec9c52ee 100644
--- a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
@@ -9,6 +9,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
require Logger
+ @impl true
+ def history_awareness, do: :auto
+
# has the user successfully posted before?
defp old_user?(%User{} = u) do
u.note_count > 0 || u.follower_count > 0
diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
index 51596c09f..a148cc1e7 100644
--- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
+++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
@@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
@reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless])
+ def history_awareness, do: :auto
+
def filter_by_summary(
%{data: %{"summary" => parent_summary}} = _in_reply_to,
%{"summary" => child_summary} = child
@@ -27,8 +29,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
def filter_by_summary(_in_reply_to, child), do: child
- def filter(%{"type" => "Create", "object" => child_object} = object)
- when is_map(child_object) do
+ def filter(%{"type" => type, "object" => child_object} = object)
+ when type in ["Create", "Update"] and is_map(child_object) do
child =
child_object["inReplyTo"]
|> Object.normalize(fetch: false)
diff --git a/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex b/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex
index 255910b2f..70224561c 100644
--- a/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex
+++ b/lib/pleroma/web/activity_pub/mrf/force_mentions_in_content.ex
@@ -11,6 +11,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
+ @impl true
+ def history_awareness, do: :auto
+
defp do_extract({:a, attrs, _}, acc) do
if Enum.find(attrs, fn {name, value} ->
name == "class" && value in ["mention", "u-url mention", "mention u-url"]
@@ -74,11 +77,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
@impl true
def filter(
%{
- "type" => "Create",
+ "type" => type,
"object" => %{"type" => "Note", "to" => to, "inReplyTo" => in_reply_to}
} = object
)
- when is_list(to) and is_binary(in_reply_to) do
+ when type in ["Create", "Update"] and is_list(to) and is_binary(in_reply_to) do
# image-only posts from pleroma apparently reach this MRF without the content field
content = object["object"]["content"] || ""
diff --git a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex
index 2142b7add..b73fd974c 100644
--- a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex
@@ -16,6 +16,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
+ @impl true
+ def history_awareness, do: :manual
+
defp check_reject(message, hashtags) do
if Enum.any?(Config.get([:mrf_hashtag, :reject]), fn match -> match in hashtags end) do
{:reject, "[HashtagPolicy] Matches with rejected keyword"}
@@ -47,22 +50,46 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
defp check_ftl_removal(message, _hashtags), do: {:ok, message}
- defp check_sensitive(message, hashtags) do
- if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do
- {:ok, Kernel.put_in(message, ["object", "sensitive"], true)}
- else
- {:ok, message}
- end
+ defp check_sensitive(message) do
+ {:ok, new_object} =
+ Object.Updater.do_with_history(message["object"], fn object ->
+ hashtags = Object.hashtags(%Object{data: object})
+
+ if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do
+ {:ok, Map.put(object, "sensitive", true)}
+ else
+ {:ok, object}
+ end
+ end)
+
+ {:ok, Map.put(message, "object", new_object)}
end
@impl true
- def filter(%{"type" => "Create", "object" => object} = message) do
- hashtags = Object.hashtags(%Object{data: object})
+ def filter(%{"type" => type, "object" => object} = message) when type in ["Create", "Update"] do
+ history_items =
+ with %{"formerRepresentations" => %{"orderedItems" => items}} <- object do
+ items
+ else
+ _ -> []
+ end
+
+ historical_hashtags =
+ Enum.reduce(history_items, [], fn item, acc ->
+ acc ++ Object.hashtags(%Object{data: item})
+ end)
+
+ hashtags = Object.hashtags(%Object{data: object}) ++ historical_hashtags
if hashtags != [] do
with {:ok, message} <- check_reject(message, hashtags),
- {:ok, message} <- check_ftl_removal(message, hashtags),
- {:ok, message} <- check_sensitive(message, hashtags) do
+ {:ok, message} <-
+ (if "type" == "Create" do
+ check_ftl_removal(message, hashtags)
+ else
+ {:ok, message}
+ end),
+ {:ok, message} <- check_sensitive(message) do
{:ok, message}
end
else
diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
index 00b64744f..687ec6c2f 100644
--- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
@@ -27,24 +27,46 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
end
defp check_reject(%{"object" => %{} = object} = message) do
- payload = object_payload(object)
-
- if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
- string_matches?(payload, pattern)
- end) do
- {:reject, "[KeywordPolicy] Matches with rejected keyword"}
- else
+ with {:ok, _new_object} <-
+ Pleroma.Object.Updater.do_with_history(object, fn object ->
+ payload = object_payload(object)
+
+ if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
+ string_matches?(payload, pattern)
+ end) do
+ {:reject, "[KeywordPolicy] Matches with rejected keyword"}
+ else
+ {:ok, message}
+ end
+ end) do
{:ok, message}
+ else
+ e -> e
end
end
- defp check_ftl_removal(%{"to" => to, "object" => %{} = object} = message) do
- payload = object_payload(object)
+ defp check_ftl_removal(%{"type" => "Create", "to" => to, "object" => %{} = object} = message) do
+ check_keyword = fn object ->
+ payload = object_payload(object)
- if Pleroma.Constants.as_public() in to and
- Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
+ if Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
string_matches?(payload, pattern)
end) do
+ {:should_delist, nil}
+ else
+ {:ok, %{}}
+ end
+ end
+
+ should_delist? = fn object ->
+ with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check_keyword) do
+ false
+ else
+ _ -> true
+ end
+ end
+
+ if Pleroma.Constants.as_public() in to and should_delist?.(object) do
to = List.delete(to, Pleroma.Constants.as_public())
cc = [Pleroma.Constants.as_public() | message["cc"] || []]
@@ -59,8 +81,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
end
end
+ defp check_ftl_removal(message) do
+ {:ok, message}
+ end
+
defp check_replace(%{"object" => %{} = object} = message) do
- object =
+ replace_kw = fn object ->
["content", "name", "summary"]
|> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end)
|> Enum.reduce(object, fn field, object ->
@@ -73,6 +99,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
Map.put(object, field, data)
end)
+ |> (fn object -> {:ok, object} end).()
+ end
+
+ {:ok, object} = Pleroma.Object.Updater.do_with_history(object, replace_kw)
message = Map.put(message, "object", object)
@@ -80,7 +110,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
end
@impl true
- def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do
+ def filter(%{"type" => type, "object" => %{"content" => _content}} = message)
+ when type in ["Create", "Update"] do
with {:ok, message} <- check_reject(message),
{:ok, message} <- check_ftl_removal(message),
{:ok, message} <- check_replace(message) do
diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
index 5b4017eb8..d517f671b 100644
--- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex
@@ -15,6 +15,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
recv_timeout: 10_000
]
+ @impl true
+ def history_awareness, do: :auto
+
defp prefetch(url) do
# Fetching only proxiable resources
if MediaProxy.enabled?() and MediaProxy.url_proxiable?(url) do
@@ -53,10 +56,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
end
@impl true
- def filter(
- %{"type" => "Create", "object" => %{"attachment" => attachments} = _object} = message
- )
- when is_list(attachments) and length(attachments) > 0 do
+ def filter(%{"type" => type, "object" => %{"attachment" => attachments} = _object} = message)
+ when type in ["Create", "Update"] and is_list(attachments) and length(attachments) > 0 do
preload(message)
{:ok, message}
diff --git a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex
index 4dc96e068..855cda3b9 100644
--- a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
@impl true
def filter(%{"actor" => actor} = object) do
with true <- is_local?(actor),
+ true <- is_eligible_type?(object),
true <- is_note?(object),
false <- has_attachment?(object),
true <- only_mentions?(object) do
@@ -32,7 +33,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
end
defp has_attachment?(%{
- "type" => "Create",
"object" => %{"type" => "Note", "attachment" => attachments}
})
when length(attachments) > 0,
@@ -40,7 +40,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
defp has_attachment?(_), do: false
- defp only_mentions?(%{"type" => "Create", "object" => %{"type" => "Note", "source" => source}}) do
+ defp only_mentions?(%{"object" => %{"type" => "Note", "source" => source}}) do
+ source =
+ case source do
+ %{"content" => text} -> text
+ _ -> source
+ end
+
non_mentions =
source |> String.split() |> Enum.filter(&(not String.starts_with?(&1, "@"))) |> length
@@ -53,9 +59,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
defp only_mentions?(_), do: false
- defp is_note?(%{"type" => "Create", "object" => %{"type" => "Note"}}), do: true
+ defp is_note?(%{"object" => %{"type" => "Note"}}), do: true
defp is_note?(_), do: false
+ defp is_eligible_type?(%{"type" => type}) when type in ["Create", "Update"], do: true
+ defp is_eligible_type?(_), do: false
+
@impl true
def describe, do: {:ok, %{}}
end
diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex
index aab647d8e..f81e9e52a 100644
--- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex
@@ -7,13 +7,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
@impl true
+ def history_awareness, do: :auto
+
+ @impl true
def filter(
%{
- "type" => "Create",
+ "type" => type,
"object" => %{"content" => content, "attachment" => _} = _child_object
} = object
)
- when content in [".", "<p>.</p>"] do
+ when type in ["Create", "Update"] and content in [".", "<p>.</p>"] do
{:ok, put_in(object, ["object", "content"], "")}
end
diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
index dc2c19d49..2dfc9a901 100644
--- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
+++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
@@ -9,7 +9,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
@impl true
- def filter(%{"type" => "Create", "object" => child_object} = object) do
+ def history_awareness, do: :auto
+
+ @impl true
+ def filter(%{"type" => type, "object" => child_object} = object)
+ when type in ["Create", "Update"] do
scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy])
content =
diff --git a/lib/pleroma/web/activity_pub/mrf/policy.ex b/lib/pleroma/web/activity_pub/mrf/policy.ex
index 0ac250c3d..0234de4d5 100644
--- a/lib/pleroma/web/activity_pub/mrf/policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/policy.ex
@@ -12,5 +12,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do
label: String.t(),
description: String.t()
}
- @optional_callbacks config_description: 0
+ @callback history_awareness() :: :auto | :manual
+ @optional_callbacks config_description: 0, history_awareness: 0
end
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index f3e31c931..5bcd6da46 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -103,8 +103,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
meta
)
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),
+ with {:ok, object_data} <- cast_and_apply_and_stringify_with_history(object),
+ meta = Keyword.put(meta, :object_data, object_data),
{:ok, create_activity} <-
create_activity
|> CreateGenericValidator.cast_and_validate(meta)
@@ -128,16 +128,50 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
with {:ok, object} <-
- object
- |> validator.cast_and_validate()
- |> Ecto.Changeset.apply_action(:insert) do
- object = stringify_keys(object)
+ do_separate_with_history(object, fn object ->
+ with {:ok, object} <-
+ object
+ |> validator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+
+ # Insert copy of hashtags as strings for the non-hashtag table indexing
+ tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object})
+ object = Map.put(object, "tag", tag)
+
+ {:ok, object}
+ end
+ end) do
+ {:ok, object, meta}
+ end
+ end
- # Insert copy of hashtags as strings for the non-hashtag table indexing
- tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object})
- object = Map.put(object, "tag", tag)
+ def validate(
+ %{"type" => "Update", "object" => %{"type" => objtype} = object} = update_activity,
+ meta
+ )
+ when objtype in ~w[Question Answer Audio Video Event Article Note Page] do
+ with {_, false} <- {:local, Access.get(meta, :local, false)},
+ {_, {:ok, object_data, _}} <- {:object_validation, validate(object, meta)},
+ meta = Keyword.put(meta, :object_data, object_data),
+ {:ok, update_activity} <-
+ update_activity
+ |> UpdateValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ update_activity = stringify_keys(update_activity)
+ {:ok, update_activity, meta}
+ else
+ {:local, _} ->
+ with {:ok, object} <-
+ update_activity
+ |> UpdateValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {:ok, object, meta}
+ end
- {:ok, object, meta}
+ {:object_validation, e} ->
+ e
end
end
@@ -178,6 +212,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(o, m), do: {:error, {:validator_not_set, {o, m}}}
+ def cast_and_apply_and_stringify_with_history(object) do
+ do_separate_with_history(object, fn object ->
+ with {:ok, object_data} <- cast_and_apply(object),
+ object_data <- object_data |> stringify_keys() do
+ {:ok, object_data}
+ end
+ end)
+ end
+
def cast_and_apply(%{"type" => "ChatMessage"} = object) do
ChatMessageValidator.cast_and_apply(object)
end
@@ -204,8 +247,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
- # is_struct/1 appears in Elixir 1.11
- def stringify_keys(%{__struct__: _} = object) do
+ def stringify_keys(object) when is_struct(object) do
object
|> Map.from_struct()
|> stringify_keys
@@ -236,4 +278,54 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
Object.normalize(object["object"], fetch: true)
:ok
end
+
+ defp for_each_history_item(
+ %{"type" => "OrderedCollection", "orderedItems" => items} = history,
+ object,
+ fun
+ ) do
+ processed_items =
+ Enum.map(items, fn item ->
+ with item <- Map.put(item, "id", object["id"]),
+ {:ok, item} <- fun.(item) do
+ item
+ else
+ _ -> nil
+ end
+ end)
+
+ if Enum.all?(processed_items, &(not is_nil(&1))) do
+ {:ok, Map.put(history, "orderedItems", processed_items)}
+ else
+ {:error, :invalid_history}
+ end
+ end
+
+ defp for_each_history_item(nil, _object, _fun) do
+ {:ok, nil}
+ end
+
+ defp for_each_history_item(_, _object, _fun) do
+ {:error, :invalid_history}
+ end
+
+ # fun is (object -> {:ok, validated_object_with_string_keys})
+ defp do_separate_with_history(object, fun) do
+ with history <- object["formerRepresentations"],
+ object <- Map.drop(object, ["formerRepresentations"]),
+ {_, {:ok, object}} <- {:main_body, fun.(object)},
+ {_, {:ok, history}} <- {:history_items, for_each_history_item(history, object, fun)} do
+ object =
+ if history do
+ Map.put(object, "formerRepresentations", history)
+ else
+ object
+ end
+
+ {:ok, object}
+ else
+ {:main_body, e} -> e
+ {:history_items, e} -> e
+ end
+ end
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
index ca335bc8a..2670e3f17 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
@@ -49,7 +49,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
defp fix_url(%{"url" => url} = data) when is_map(url), do: Map.put(data, "url", url["href"])
defp fix_url(data), do: data
- defp fix_tag(%{"tag" => tag} = data) when is_list(tag), do: data
+ defp fix_tag(%{"tag" => tag} = data) when is_list(tag) do
+ Map.put(data, "tag", Enum.filter(tag, &is_map/1))
+ end
+
defp fix_tag(%{"tag" => tag} = data) when is_map(tag), do: Map.put(data, "tag", [tag])
defp fix_tag(data), do: Map.drop(data, ["tag"])
@@ -60,11 +63,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies),
do: Map.put(data, "replies", replies)
- defp fix_replies(%{"replies" => replies} = data) when is_bitstring(replies),
+ # TODO: Pleroma does not have any support for Collections at the moment.
+ # If the `replies` field is not something the ObjectID validator can handle,
+ # the activity/object would be rejected, which is bad behavior.
+ defp fix_replies(%{"replies" => replies} = data) when not is_list(replies),
do: Map.drop(data, ["replies"])
defp fix_replies(data), do: data
+ def fix_attachments(%{"attachment" => attachment} = data) when is_map(attachment),
+ do: Map.put(data, "attachment", [attachment])
+
+ def fix_attachments(data), do: data
+
defp fix(data) do
data
|> CommonFixes.fix_actor()
@@ -72,6 +83,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|> fix_url()
|> fix_tag()
|> fix_replies()
+ |> fix_attachments()
|> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()
end
@@ -88,7 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Article", "Note", "Page"])
- |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])
+ |> validate_required([:id, :actor, :attributedTo, :type, :context])
|> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence()
diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
index d1c61ac82..14f51e2c5 100644
--- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
@@ -11,15 +11,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
@primary_key false
embedded_schema do
+ field(:id, :string)
field(:type, :string)
- field(:mediaType, :string, default: "application/octet-stream")
+ field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
field(:name, :string)
field(:blurhash, :string)
embeds_many :url, UrlObjectValidator, primary_key: false do
field(:type, :string)
field(:href, ObjectValidators.Uri)
- field(:mediaType, :string, default: "application/octet-stream")
+ field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
field(:width, :integer)
field(:height, :integer)
end
@@ -43,7 +44,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|> fix_url()
struct
- |> cast(data, [:type, :mediaType, :name, :blurhash])
+ |> cast(data, [:id, :type, :mediaType, :name, :blurhash])
|> cast_embed(:url, with: &url_changeset/2)
|> validate_inclusion(:type, ~w[Link Document Audio Image Video])
|> validate_required([:type, :mediaType, :url])
@@ -59,13 +60,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
end
def fix_media_type(data) do
- data = Map.put_new(data, "mediaType", data["mimeType"])
-
- if is_bitstring(data["mediaType"]) && MIME.extensions(data["mediaType"]) != [] do
- data
- else
- Map.put(data, "mediaType", "application/octet-stream")
- end
+ Map.put_new(data, "mediaType", data["mimeType"] || "application/octet-stream")
end
defp handle_href(href, mediaType, data) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
index 8e768ffbf..7b60c139a 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
@@ -33,6 +33,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
field(:content, :string)
field(:published, ObjectValidators.DateTime)
+ field(:updated, ObjectValidators.DateTime)
field(:emoji, ObjectValidators.Emoji, default: %{})
embeds_many(:attachment, AttachmentValidator)
end
@@ -51,8 +52,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
field(:summary, :string)
field(:context, :string)
- # short identifier for PleromaFE to group statuses by context
- field(:context_id, :integer)
field(:sensitive, :boolean, default: false)
field(:replies_count, :integer, default: 0)
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
index 4f8c083eb..add46d561 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -22,14 +22,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
end
def fix_object_defaults(data) do
- %{data: %{"id" => context}, id: context_id} =
- Utils.create_context(data["context"] || data["conversation"])
+ context =
+ Utils.maybe_create_context(
+ data["context"] || data["conversation"] || data["inReplyTo"] || data["id"]
+ )
%User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"])
data
|> Map.put("context", context)
- |> Map.put("context_id", context_id)
|> cast_and_filter_recipients("to", follower_collection)
|> cast_and_filter_recipients("cc", follower_collection)
|> cast_and_filter_recipients("bto", follower_collection)
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
index c9a621cb1..2395abfd4 100644
--- a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
@@ -75,7 +75,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
data
|> CommonFixes.fix_actor()
- |> Map.put_new("context", object["context"])
+ |> Map.put("context", object["context"])
|> fix_addressing(object)
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
index ed072b888..0858281e5 100644
--- a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
@@ -49,6 +49,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
defp fix(data) do
data =
data
+ |> fix_emoji_qualification()
|> CommonFixes.fix_actor()
|> CommonFixes.fix_activity_addressing()
@@ -61,6 +62,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
end
end
+ defp fix_emoji_qualification(%{"content" => emoji} = data) do
+ new_emoji = Pleroma.Emoji.fully_qualify_emoji(emoji)
+
+ cond do
+ Pleroma.Emoji.is_unicode_emoji?(emoji) ->
+ data
+
+ Pleroma.Emoji.is_unicode_emoji?(new_emoji) ->
+ data |> Map.put("content", new_emoji)
+
+ true ->
+ data
+ end
+ end
+
+ defp fix_emoji_qualification(data), do: data
+
defp validate_emoji(cng) do
content = get_field(cng, :content)
diff --git a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
index 0e99f2037..ab204f69a 100644
--- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex
@@ -62,7 +62,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Event"])
- |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])
+ |> validate_required([:id, :actor, :attributedTo, :type, :context])
|> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence()
diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
index 9412be4bc..ce3305142 100644
--- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
@@ -80,7 +80,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Question"])
- |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])
+ |> validate_required([:id, :actor, :attributedTo, :type, :context])
|> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence()
diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
index a5def312e..1e940a400 100644
--- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
@@ -51,7 +51,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
with actor = get_field(cng, :actor),
object = get_field(cng, :object),
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
- true <- actor == object_id do
+ actor_uri <- URI.parse(actor),
+ object_uri <- URI.parse(object_id),
+ true <- actor_uri.host == object_uri.host do
cng
else
_e ->
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index b997c15db..5eefd2824 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -25,6 +25,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Web.Streamer
alias Pleroma.Workers.PollWorker
+ require Pleroma.Constants
require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@@ -153,23 +154,26 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# Tasks this handles:
# - Update the user
+ # - Update a non-user object (Note, Question, etc.)
#
# For a local user, we also get a changeset with the full information, so we
# can update non-federating, non-activitypub settings as well.
@impl true
def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do
- if changeset = Keyword.get(meta, :user_update_changeset) do
- changeset
- |> User.update_and_set_cache()
+ updated_object_id = updated_object["id"]
+
+ with {_, true} <- {:has_id, is_binary(updated_object_id)},
+ %{"type" => type} <- updated_object,
+ {_, is_user} <- {:is_user, type in Pleroma.Constants.actor_types()} do
+ if is_user do
+ handle_update_user(object, meta)
+ else
+ handle_update_object(object, meta)
+ end
else
- {:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object)
-
- User.get_by_ap_id(updated_object["id"])
- |> User.remote_user_changeset(new_user_data)
- |> User.update_and_set_cache()
+ _ ->
+ {:ok, object, meta}
end
-
- {:ok, object, meta}
end
# Tasks this handles:
@@ -390,6 +394,79 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
{:ok, object, meta}
end
+ defp handle_update_user(
+ %{data: %{"type" => "Update", "object" => updated_object}} = object,
+ meta
+ ) do
+ if changeset = Keyword.get(meta, :user_update_changeset) do
+ changeset
+ |> User.update_and_set_cache()
+ else
+ {:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object)
+
+ User.get_by_ap_id(updated_object["id"])
+ |> User.remote_user_changeset(new_user_data)
+ |> User.update_and_set_cache()
+ end
+
+ {:ok, object, meta}
+ end
+
+ defp handle_update_object(
+ %{data: %{"type" => "Update", "object" => updated_object}} = object,
+ meta
+ ) do
+ orig_object_ap_id = updated_object["id"]
+ orig_object = Object.get_by_ap_id(orig_object_ap_id)
+ orig_object_data = orig_object.data
+
+ updated_object =
+ if meta[:local] do
+ # If this is a local Update, we don't process it by transmogrifier,
+ # so we use the embedded object as-is.
+ updated_object
+ else
+ meta[:object_data]
+ end
+
+ if orig_object_data["type"] in Pleroma.Constants.updatable_object_types() do
+ %{
+ updated_data: updated_object_data,
+ updated: updated,
+ used_history_in_new_object?: used_history_in_new_object?
+ } = Object.Updater.make_new_object_data_from_update_object(orig_object_data, updated_object)
+
+ changeset =
+ orig_object
+ |> Repo.preload(:hashtags)
+ |> Object.change(%{data: updated_object_data})
+
+ with {:ok, new_object} <- Repo.update(changeset),
+ {:ok, _} <- Object.invalid_object_cache(new_object),
+ {:ok, _} <- Object.set_cache(new_object),
+ # The metadata/utils.ex uses the object id for the cache.
+ {:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(new_object.id) do
+ if used_history_in_new_object? do
+ with create_activity when not is_nil(create_activity) <-
+ Pleroma.Activity.get_create_by_object_ap_id(orig_object_ap_id),
+ {:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(create_activity.id) do
+ nil
+ else
+ _ -> nil
+ end
+ end
+
+ if updated do
+ object
+ |> Activity.normalize()
+ |> ActivityPub.notify_and_stream()
+ end
+ end
+ end
+
+ {:ok, object, meta}
+ end
+
def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, meta) do
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
actor = User.get_cached_by_ap_id(object.data["actor"])
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index a70330f0e..e4c04da0d 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -203,13 +203,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
media_type =
cond do
- is_map(url) && MIME.extensions(url["mediaType"]) != [] ->
+ is_map(url) && url =~ Pleroma.Constants.mime_regex() ->
url["mediaType"]
- is_bitstring(data["mediaType"]) && MIME.extensions(data["mediaType"]) != [] ->
+ is_bitstring(data["mediaType"]) && data["mediaType"] =~ Pleroma.Constants.mime_regex() ->
data["mediaType"]
- is_bitstring(data["mimeType"]) && MIME.extensions(data["mimeType"]) != [] ->
+ is_bitstring(data["mimeType"]) && data["mimeType"] =~ Pleroma.Constants.mime_regex() ->
data["mimeType"]
true ->
@@ -687,6 +687,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> strip_internal_fields
|> strip_internal_tags
|> set_type
+ |> maybe_process_history
+ end
+
+ defp maybe_process_history(%{"formerRepresentations" => %{"orderedItems" => history}} = object) do
+ processed_history =
+ Enum.map(
+ history,
+ fn
+ item when is_map(item) -> prepare_object(item)
+ item -> item
+ end
+ )
+
+ put_in(object, ["formerRepresentations", "orderedItems"], processed_history)
+ end
+
+ defp maybe_process_history(object) do
+ object
end
# @doc
@@ -711,6 +729,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, data}
end
+ def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
+ when objtype in Pleroma.Constants.updatable_object_types() do
+ object =
+ object
+ |> prepare_object
+
+ data =
+ data
+ |> Map.put("object", object)
+ |> Map.merge(Utils.make_json_ld_header())
+ |> Map.delete("bcc")
+
+ {:ok, data}
+ end
+
def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
object =
object_id
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 9cde7805c..d3b7d804f 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -154,22 +154,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Notification.get_notified_from_activity(%Activity{data: object}, false)
end
- def create_context(context) do
- context = context || generate_id("contexts")
-
- # Ecto has problems accessing the constraint inside the jsonb,
- # so we explicitly check for the existed object before insert
- object = Object.get_cached_by_ap_id(context)
-
- with true <- is_nil(object),
- changeset <- Object.context_mapping(context),
- {:ok, inserted_object} <- Repo.insert(changeset) do
- inserted_object
- else
- _ ->
- object
- end
- end
+ def maybe_create_context(context), do: context || generate_id("contexts")
@doc """
Enqueues an activity for federation if it's local
@@ -201,18 +186,16 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> Map.put_new("id", "pleroma:fakeid")
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", "pleroma:fakecontext")
- |> Map.put_new("context_id", -1)
|> lazy_put_object_defaults(true)
end
def lazy_put_activity_defaults(map, _fake?) do
- %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
+ context = maybe_create_context(map["context"])
map
|> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", context)
- |> Map.put_new("context_id", context_id)
|> lazy_put_object_defaults(false)
end
@@ -226,7 +209,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> Map.put_new("id", "pleroma:fake_object_id")
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"])
- |> Map.put_new("context_id", activity["context_id"])
|> Map.put_new("fake", true)
%{activity | "object" => object}
@@ -239,7 +221,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> Map.put_new_lazy("id", &generate_object_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"])
- |> Map.put_new("context_id", activity["context_id"])
%{activity | "object" => object}
end
diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex
index f848aba3a..63caa915c 100644
--- a/lib/pleroma/web/activity_pub/views/object_view.ex
+++ b/lib/pleroma/web/activity_pub/views/object_view.ex
@@ -29,11 +29,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
def render("object.json", %{object: %Activity{} = activity}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
- object = Object.normalize(activity, fetch: false)
+ object_id = Object.normalize(activity, id_only: true)
additional =
Transmogrifier.prepare_object(activity.data)
- |> Map.put("object", object.data["id"])
+ |> Map.put("object", object_id)
Map.merge(base, additional)
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 52f6bb56d..f69fca075 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -34,7 +34,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
def render("endpoints.json", _), do: %{}
def render("service.json", %{user: user}) do
- {:ok, user} = User.ensure_keys_present(user)
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
@@ -71,7 +70,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
def render("user.json", %{user: user}) do
- {:ok, user} = User.ensure_keys_present(user)
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex
index 465f8a9b7..7c57f88f9 100644
--- a/lib/pleroma/web/activity_pub/visibility.ex
+++ b/lib/pleroma/web/activity_pub/visibility.ex
@@ -84,7 +84,10 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
when module in [Activity, Object] do
x = [user.ap_id | User.following(user)]
y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || [])
- is_public?(message) || Enum.any?(x, &(&1 in y))
+
+ user_is_local = user.local
+ federatable = not is_local_public?(message)
+ (is_public?(message) || Enum.any?(x, &(&1 in y))) and (user_is_local || federatable)
end
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
diff --git a/lib/pleroma/web/admin_api/controllers/announcement_controller.ex b/lib/pleroma/web/admin_api/controllers/announcement_controller.ex
new file mode 100644
index 000000000..6ad5fc12c
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/announcement_controller.ex
@@ -0,0 +1,83 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.AnnouncementController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Announcement
+ alias Pleroma.Web.ControllerHelper
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action in [:create, :delete, :change])
+ plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action in [:index, :show])
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.AnnouncementOperation
+
+ defp default_limit, do: 20
+
+ def index(conn, params) do
+ limit = Map.get(params, :limit, default_limit())
+ offset = Map.get(params, :offset, 0)
+
+ announcements = Announcement.list_paginated(%{limit: limit, offset: offset})
+
+ render(conn, "index.json", announcements: announcements)
+ end
+
+ def show(conn, %{id: id} = _params) do
+ announcement = Announcement.get_by_id(id)
+
+ if is_nil(announcement) do
+ {:error, :not_found}
+ else
+ render(conn, "show.json", announcement: announcement)
+ end
+ end
+
+ def create(%{body_params: params} = conn, _params) do
+ with {:ok, announcement} <- Announcement.add(change_params(params)) do
+ render(conn, "show.json", announcement: announcement)
+ else
+ _ ->
+ {:error, 400}
+ end
+ end
+
+ def change_params(orig_params) do
+ data =
+ %{}
+ |> Pleroma.Maps.put_if_present("content", orig_params, &Map.fetch(&1, :content))
+ |> Pleroma.Maps.put_if_present("all_day", orig_params, &Map.fetch(&1, :all_day))
+
+ orig_params
+ |> Map.merge(%{data: data})
+ end
+
+ def change(%{body_params: params} = conn, %{id: id} = _params) do
+ with announcement <- Announcement.get_by_id(id),
+ {:exists, true} <- {:exists, not is_nil(announcement)},
+ {:ok, announcement} <- Announcement.update(announcement, change_params(params)) do
+ render(conn, "show.json", announcement: announcement)
+ else
+ {:exists, false} ->
+ {:error, :not_found}
+
+ _ ->
+ {:error, 400}
+ end
+ end
+
+ def delete(conn, %{id: id} = _params) do
+ case Announcement.delete_by_id(id) do
+ :ok ->
+ conn
+ |> ControllerHelper.json_response(:ok, %{})
+
+ _ ->
+ {:error, :not_found}
+ end
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex
index 55ab6d063..a03318c0e 100644
--- a/lib/pleroma/web/admin_api/controllers/config_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex
@@ -22,10 +22,58 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation
+ defp translate_descriptions(descriptions, path \\ []) do
+ Enum.map(descriptions, fn desc -> translate_item(desc, path) end)
+ end
+
+ defp translate_string(str, path, type) do
+ Gettext.dpgettext(
+ Pleroma.Web.Gettext,
+ "config_descriptions",
+ Pleroma.Docs.Translator.Compiler.msgctxt_for(path, type),
+ str
+ )
+ end
+
+ defp maybe_put_translated(item, key, path) do
+ if item[key] do
+ Map.put(
+ item,
+ key,
+ translate_string(
+ item[key],
+ path ++ [Pleroma.Docs.Translator.Compiler.key_for(item)],
+ to_string(key)
+ )
+ )
+ else
+ item
+ end
+ end
+
+ defp translate_item(item, path) do
+ item
+ |> maybe_put_translated(:label, path)
+ |> maybe_put_translated(:description, path)
+ |> translate_children(path)
+ end
+
+ defp translate_children(%{children: children} = item, path) when is_list(children) do
+ item
+ |> Map.put(
+ :children,
+ translate_descriptions(children, path ++ [Pleroma.Docs.Translator.Compiler.key_for(item)])
+ )
+ end
+
+ defp translate_children(item, _path) do
+ item
+ end
+
def descriptions(conn, _params) do
descriptions = Enum.filter(Pleroma.Docs.JSON.compiled_descriptions(), &whitelisted_config?/1)
- json(conn, descriptions)
+ json(conn, translate_descriptions(descriptions))
end
def show(conn, %{only_db: true}) do
diff --git a/lib/pleroma/web/admin_api/views/announcement_view.ex b/lib/pleroma/web/admin_api/views/announcement_view.ex
new file mode 100644
index 000000000..a35bd60cf
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/announcement_view.ex
@@ -0,0 +1,15 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.AnnouncementView do
+ use Pleroma.Web, :view
+
+ def render("index.json", %{announcements: announcements}) do
+ render_many(announcements, __MODULE__, "show.json")
+ end
+
+ def render("show.json", %{announcement: announcement}) do
+ Pleroma.Announcement.render_json(announcement, admin: true)
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index a64762285..aed59293c 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -279,10 +279,16 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
"Mute notifications in addition to statuses? Defaults to `true`."
),
Operation.parameter(
+ :duration,
+ :query,
+ %Schema{type: :integer},
+ "Expire the mute in `duration` seconds. Default 0 for infinity"
+ ),
+ Operation.parameter(
:expires_in,
:query,
%Schema{type: :integer, default: 0},
- "Expire the mute in `expires_in` seconds. Default 0 for infinity"
+ "Deprecated, use `duration` instead"
)
],
responses: %{
@@ -370,6 +376,22 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
+ def remove_from_followers_operation do
+ %Operation{
+ tags: ["Account actions"],
+ summary: "Remove from followers",
+ operationId: "AccountController.remove_from_followers",
+ security: [%{"oAuth" => ["follow", "write:follows"]}],
+ description: "Remove the given account from followers",
+ parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
+ responses: %{
+ 200 => Operation.response("Relationship", "application/json", AccountRelationship),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
def note_operation do
%Operation{
tags: ["Account actions"],
@@ -545,10 +567,18 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description: "Invite token required when the registrations aren't public"
},
birthday: %Schema{
- type: :string,
nullable: true,
description: "User's birthday",
- format: :date
+ anyOf: [
+ %Schema{
+ type: :string,
+ format: :date
+ },
+ %Schema{
+ type: :string,
+ maxLength: 0
+ }
+ ]
},
language: %Schema{
type: :string,
@@ -733,10 +763,18 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
},
actor_type: ActorType,
birthday: %Schema{
- type: :string,
nullable: true,
description: "User's birthday",
- format: :date
+ anyOf: [
+ %Schema{
+ type: :string,
+ format: :date
+ },
+ %Schema{
+ type: :string,
+ maxLength: 0
+ }
+ ]
},
show_birthday: %Schema{
allOf: [BooleanLike],
@@ -861,10 +899,15 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description: "Mute notifications in addition to statuses? Defaults to true.",
default: true
},
+ duration: %Schema{
+ type: :integer,
+ nullable: true,
+ description: "Expire the mute in `expires_in` seconds. Default 0 for infinity"
+ },
expires_in: %Schema{
type: :integer,
nullable: true,
- description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
+ description: "Deprecated, use `duration` instead",
default: 0
}
},
diff --git a/lib/pleroma/web/api_spec/operations/admin/announcement_operation.ex b/lib/pleroma/web/api_spec/operations/admin/announcement_operation.ex
new file mode 100644
index 000000000..58a039e72
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/announcement_operation.ex
@@ -0,0 +1,165 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.AnnouncementOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Announcement
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ 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: ["Announcement managment"],
+ summary: "Retrieve a list of announcements",
+ operationId: "AdminAPI.AnnouncementController.index",
+ security: [%{"oAuth" => ["admin:read"]}],
+ parameters: [
+ Operation.parameter(
+ :limit,
+ :query,
+ %Schema{type: :integer, minimum: 1},
+ "the maximum number of announcements to return"
+ ),
+ Operation.parameter(
+ :offset,
+ :query,
+ %Schema{type: :integer, minimum: 0},
+ "the offset of the first announcement to return"
+ )
+ | admin_api_params()
+ ],
+ responses: %{
+ 200 => Operation.response("Response", "application/json", list_of_announcements()),
+ 400 => Operation.response("Forbidden", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Announcement managment"],
+ summary: "Display one announcement",
+ operationId: "AdminAPI.AnnouncementController.show",
+ security: [%{"oAuth" => ["admin:read"]}],
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "announcement id"
+ )
+ | admin_api_params()
+ ],
+ responses: %{
+ 200 => Operation.response("Response", "application/json", Announcement),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Announcement managment"],
+ summary: "Delete one announcement",
+ operationId: "AdminAPI.AnnouncementController.delete",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "announcement id"
+ )
+ | admin_api_params()
+ ],
+ responses: %{
+ 200 => Operation.response("Response", "application/json", %Schema{type: :object}),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Announcement managment"],
+ summary: "Create one announcement",
+ operationId: "AdminAPI.AnnouncementController.create",
+ security: [%{"oAuth" => ["admin:write"]}],
+ requestBody: request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Response", "application/json", Announcement),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def change_operation do
+ %Operation{
+ tags: ["Announcement managment"],
+ summary: "Change one announcement",
+ operationId: "AdminAPI.AnnouncementController.change",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "announcement id"
+ )
+ | admin_api_params()
+ ],
+ requestBody: request_body("Parameters", change_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Response", "application/json", Announcement),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp create_or_change_props do
+ %{
+ content: %Schema{type: :string},
+ starts_at: %Schema{type: :string, format: "date-time", nullable: true},
+ ends_at: %Schema{type: :string, format: "date-time", nullable: true},
+ all_day: %Schema{type: :boolean}
+ }
+ end
+
+ def create_request do
+ %Schema{
+ title: "AnnouncementCreateRequest",
+ type: :object,
+ required: [:content],
+ properties: create_or_change_props()
+ }
+ end
+
+ def change_request do
+ %Schema{
+ title: "AnnouncementChangeRequest",
+ type: :object,
+ properties: create_or_change_props()
+ }
+ end
+
+ def list_of_announcements do
+ %Schema{
+ type: :array,
+ items: Announcement
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/announcement_operation.ex b/lib/pleroma/web/api_spec/operations/announcement_operation.ex
new file mode 100644
index 000000000..71be0002a
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/announcement_operation.ex
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.AnnouncementOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Announcement
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Announcement"],
+ summary: "Retrieve a list of announcements",
+ operationId: "MastodonAPI.AnnouncementController.index",
+ security: [%{"oAuth" => []}],
+ responses: %{
+ 200 => Operation.response("Response", "application/json", list_of_announcements()),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def mark_read_operation do
+ %Operation{
+ tags: ["Announcement"],
+ summary: "Mark one announcement as read",
+ operationId: "MastodonAPI.AnnouncementController.mark_read",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "announcement id"
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Response", "application/json", %Schema{type: :object}),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def list_of_announcements do
+ %Schema{
+ type: :array,
+ items: Announcement
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex
index 7f2336ff6..56aa129d2 100644
--- a/lib/pleroma/web/api_spec/operations/notification_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -51,6 +51,12 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
:include_types,
:query,
%Schema{type: :array, items: notification_type()},
+ "Deprecated, use `types` instead"
+ ),
+ Operation.parameter(
+ :types,
+ :query,
+ %Schema{type: :array, items: notification_type()},
"Include the notifications for activities with the given types"
),
Operation.parameter(
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex
index 82ec1e7bb..45fa2b058 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
%Operation{
tags: ["Backups"],
summary: "List backups",
- security: [%{"oAuth" => ["read:account"]}],
+ security: [%{"oAuth" => ["read:backups"]}],
operationId: "PleromaAPI.BackupController.index",
responses: %{
200 =>
@@ -37,7 +37,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
%Operation{
tags: ["Backups"],
summary: "Create a backup",
- security: [%{"oAuth" => ["read:account"]}],
+ security: [%{"oAuth" => ["read:backups"]}],
operationId: "PleromaAPI.BackupController.create",
responses: %{
200 =>
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_settings_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_settings_operation.ex
new file mode 100644
index 000000000..e2cef4f67
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_settings_operation.ex
@@ -0,0 +1,72 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaSettingsOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Settings"],
+ summary: "Get settings for an application",
+ description: "Get synchronized settings for an application",
+ operationId: "SettingsController.show",
+ parameters: [app_name_param()],
+ security: [%{"oAuth" => ["read:accounts"]}],
+ responses: %{
+ 200 => Operation.response("object", "application/json", object())
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Settings"],
+ summary: "Update settings for an application",
+ description: "Update synchronized settings for an application",
+ operationId: "SettingsController.update",
+ parameters: [app_name_param()],
+ security: [%{"oAuth" => ["write:accounts"]}],
+ requestBody: request_body("Parameters", update_request(), required: true),
+ responses: %{
+ 200 => Operation.response("object", "application/json", object())
+ }
+ }
+ end
+
+ def app_name_param do
+ Operation.parameter(:app, :path, %Schema{type: :string}, "Application name",
+ example: "pleroma-fe",
+ required: true
+ )
+ end
+
+ def object do
+ %Schema{
+ title: "Settings object",
+ description: "The object that contains settings for the application.",
+ type: :object
+ }
+ end
+
+ def update_request do
+ %Schema{
+ title: "SettingsUpdateRequest",
+ type: :object,
+ description:
+ "The settings object to be merged with the current settings. To remove a field, set it to null.",
+ example: %{
+ "config1" => true,
+ "config2_to_unset" => nil
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
index 639f24d49..e921128c7 100644
--- a/lib/pleroma/web/api_spec/operations/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -6,9 +6,13 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.AccountOperation
+ alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.Attachment
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+ alias Pleroma.Web.ApiSpec.Schemas.Poll
alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
@@ -434,6 +438,59 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
}
end
+ def show_history_operation do
+ %Operation{
+ tags: ["Retrieve status history"],
+ summary: "Status history",
+ description: "View history of a status",
+ operationId: "StatusController.show_history",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ id_param()
+ ],
+ responses: %{
+ 200 => status_history_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_source_operation do
+ %Operation{
+ tags: ["Retrieve status source"],
+ summary: "Status source",
+ description: "View source of a status",
+ operationId: "StatusController.show_source",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [
+ id_param()
+ ],
+ responses: %{
+ 200 => status_source_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ 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
%Schema{type: :array, items: Status, example: [Status.schema().example]}
end
@@ -537,6 +594,60 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation 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,
@@ -579,6 +690,87 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
Operation.response("Status", "application/json", Status)
end
+ defp status_history_response do
+ Operation.response(
+ "Status History",
+ "application/json",
+ %Schema{
+ title: "Status history",
+ description: "Response schema for history of a status",
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ account: %Schema{
+ allOf: [Account],
+ description: "The account that authored this status"
+ },
+ content: %Schema{
+ type: :string,
+ format: :html,
+ description: "HTML-encoded status content"
+ },
+ sensitive: %Schema{
+ type: :boolean,
+ description: "Is this status marked as sensitive content?"
+ },
+ spoiler_text: %Schema{
+ type: :string,
+ description:
+ "Subject or summary line, below which status content is collapsed until expanded"
+ },
+ created_at: %Schema{
+ type: :string,
+ format: "date-time",
+ description: "The date when this status was created"
+ },
+ media_attachments: %Schema{
+ type: :array,
+ items: Attachment,
+ description: "Media that is attached to this status"
+ },
+ emojis: %Schema{
+ type: :array,
+ items: Emoji,
+ description: "Custom emoji to be used when rendering status content"
+ },
+ poll: %Schema{
+ allOf: [Poll],
+ nullable: true,
+ description: "The poll attached to the status"
+ }
+ }
+ }
+ }
+ )
+ end
+
+ defp status_source_response do
+ Operation.response(
+ "Status Source",
+ "application/json",
+ %Schema{
+ type: :object,
+ properties: %{
+ id: FlakeID,
+ text: %Schema{
+ type: :string,
+ description: "Raw source of status content"
+ },
+ spoiler_text: %Schema{
+ type: :string,
+ description:
+ "Subject or summary line, below which status content is collapsed until expanded"
+ },
+ content_type: %Schema{
+ type: :string,
+ description: "The content type of the source"
+ }
+ }
+ }
+ )
+ end
+
defp context do
%Schema{
title: "StatusContext",
diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
index c59e3b12a..29df03e34 100644
--- a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
@@ -214,6 +214,146 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
}
end
+ def move_account_operation do
+ %Operation{
+ tags: ["Account credentials"],
+ summary: "Move account",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ operationId: "UtilController.move_account",
+ requestBody: request_body("Parameters", move_account_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response("Success", "application/json", %Schema{
+ type: :object,
+ properties: %{status: %Schema{type: :string, example: "success"}}
+ }),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp move_account_request do
+ %Schema{
+ title: "MoveAccountRequest",
+ description: "POST body for moving the account",
+ type: :object,
+ required: [:password, :target_account],
+ properties: %{
+ password: %Schema{type: :string, description: "Current password"},
+ target_account: %Schema{
+ type: :string,
+ description: "The nickname of the target account to move to"
+ }
+ }
+ }
+ end
+
+ def list_aliases_operation do
+ %Operation{
+ tags: ["Account credentials"],
+ summary: "List account aliases",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ operationId: "UtilController.list_aliases",
+ 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
+
+ def add_alias_operation do
+ %Operation{
+ tags: ["Account credentials"],
+ summary: "Add an alias to this account",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ operationId: "UtilController.add_alias",
+ requestBody: request_body("Parameters", add_alias_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response("Success", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ status: %Schema{
+ type: :string,
+ example: "success"
+ }
+ }
+ }),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp add_alias_request do
+ %Schema{
+ title: "AddAliasRequest",
+ description: "PUT body for adding aliases",
+ type: :object,
+ required: [:alias],
+ properties: %{
+ alias: %Schema{
+ type: :string,
+ description: "The nickname of the account to add to aliases"
+ }
+ }
+ }
+ end
+
+ def delete_alias_operation do
+ %Operation{
+ tags: ["Account credentials"],
+ summary: "Delete an alias from this account",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ operationId: "UtilController.delete_alias",
+ requestBody: request_body("Parameters", delete_alias_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response("Success", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ status: %Schema{
+ type: :string,
+ example: "success"
+ }
+ }
+ }),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp delete_alias_request do
+ %Schema{
+ title: "DeleteAliasRequest",
+ description: "PUT body for deleting aliases",
+ type: :object,
+ required: [:alias],
+ properties: %{
+ alias: %Schema{
+ type: :string,
+ description: "The nickname of the account to delete from aliases"
+ }
+ }
+ }
+ end
+
def healthcheck_operation do
%Operation{
tags: ["Accounts"],
@@ -265,6 +405,16 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
}
end
+ def show_subscribe_form_operation do
+ %Operation{
+ tags: ["Accounts"],
+ summary: "Show remote subscribe form",
+ operationId: "UtilController.show_subscribe_form",
+ parameters: [],
+ responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})}
+ }
+ end
+
defp delete_account_request do
%Schema{
title: "AccountDeleteRequest",
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
index e8a529f2e..8aeb821a8 100644
--- a/lib/pleroma/web/api_spec/schemas/account.ex
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -33,6 +33,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
header: %Schema{type: :string, format: :uri},
id: FlakeID,
locked: %Schema{type: :boolean},
+ mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
note: %Schema{type: :string, format: :html},
statuses_count: %Schema{type: :integer},
url: %Schema{type: :string, format: :uri},
diff --git a/lib/pleroma/web/api_spec/schemas/announcement.ex b/lib/pleroma/web/api_spec/schemas/announcement.ex
new file mode 100644
index 000000000..67d129ef6
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/announcement.ex
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.Announcement do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Announcement",
+ description: "Response schema for an announcement",
+ type: :object,
+ properties: %{
+ id: FlakeID,
+ content: %Schema{type: :string},
+ starts_at: %Schema{
+ type: :string,
+ format: "date-time",
+ nullable: true
+ },
+ ends_at: %Schema{
+ type: :string,
+ format: "date-time",
+ nullable: true
+ },
+ all_day: %Schema{type: :boolean},
+ published_at: %Schema{type: :string, format: "date-time"},
+ updated_at: %Schema{type: :string, format: "date-time"},
+ read: %Schema{type: :boolean},
+ mentions: %Schema{type: :array},
+ statuses: %Schema{type: :array},
+ tags: %Schema{type: :array},
+ emojis: %Schema{type: :array},
+ reactions: %Schema{type: :array},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ raw_content: %Schema{type: :string}
+ }
+ }
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index 6e6e30315..698f11794 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -73,6 +73,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
format: "date-time",
description: "The date when this status was created"
},
+ edited_at: %Schema{
+ type: :string,
+ format: "date-time",
+ nullable: true,
+ description: "The date when this status was last edited"
+ },
emojis: %Schema{
type: :array,
items: Emoji,
@@ -142,9 +148,15 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
description:
"A map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`"
},
+ context: %Schema{
+ type: :string,
+ description: "The thread identifier the status is associated with"
+ },
conversation_id: %Schema{
type: :integer,
- description: "The ID of the AP context the status is associated with (if any)"
+ deprecated: true,
+ description:
+ "The ID of the AP context the status is associated with (if any); deprecated, please use `context` instead"
},
direct_conversation_id: %Schema{
type: :integer,
@@ -319,6 +331,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
"pinned" => false,
"pleroma" => %{
"content" => %{"text/plain" => "foobar"},
+ "context" => "http://localhost:4001/objects/8b4c0c80-6a37-4d2a-b1b9-05a19e3875aa",
"conversation_id" => 345_972,
"direct_conversation_id" => nil,
"emoji_reactions" => [],
diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex
index 1b95ee89c..89f5dd606 100644
--- a/lib/pleroma/web/common_api.ex
+++ b/lib/pleroma/web/common_api.ex
@@ -402,6 +402,41 @@ defmodule Pleroma.Web.CommonAPI 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),
+ in_reply_to_id:
+ with replied_id when is_binary(replied_id) <- orig_object.data["inReplyTo"],
+ %Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(replied_id) do
+ activity_id
+ else
+ _ -> nil
+ end
+ }
+
+ params = Map.merge(changes, kept_params)
+
+ with {:ok, draft} <- ActivityDraft.create(user, params) do
+ change =
+ Object.Updater.make_update_object_data(orig_object.data, draft.object, 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),
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 7c21c8c3a..9af635da8 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -224,7 +224,10 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
object =
note_data
|> Map.put("emoji", emoji)
- |> Map.put("source", draft.status)
+ |> Map.put("source", %{
+ "content" => draft.status,
+ "mediaType" => Utils.get_content_type(draft.params[:content_type])
+ })
|> Map.put("generator", draft.params[:generator])
%__MODULE__{draft | object: object}
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index ce850b038..ff0814329 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -37,7 +37,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def attachments_from_ids_no_descs(ids) do
Enum.map(ids, fn media_id ->
- case Repo.get(Object, media_id) do
+ case get_attachment(media_id) do
%Object{data: data} -> data
_ -> nil
end
@@ -51,13 +51,17 @@ defmodule Pleroma.Web.CommonAPI.Utils do
{_, descs} = Jason.decode(descs_str)
Enum.map(ids, fn media_id ->
- with %Object{data: data} <- Repo.get(Object, media_id) do
+ with %Object{data: data} <- get_attachment(media_id) do
Map.put(data, "name", descs[media_id])
end
end)
|> Enum.reject(&is_nil/1)
end
+ defp get_attachment(media_id) do
+ Repo.get(Object, media_id)
+ end
+
@spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
def get_to_and_cc(%{in_reply_to_conversation: %Participation{} = participation}) do
@@ -219,7 +223,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> maybe_add_attachments(draft.attachments, attachment_links)
end
- defp get_content_type(content_type) do
+ def get_content_type(content_type) do
if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do
content_type
else
@@ -449,35 +453,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def get_report_statuses(_, _), do: {:ok, nil}
- # DEPRECATED mostly, context objects are now created at insertion time.
- def context_to_conversation_id(context) do
- with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
- id
- else
- _e ->
- changeset = Object.context_mapping(context)
-
- case Repo.insert(changeset) do
- {:ok, %{id: id}} ->
- id
-
- # This should be solved by an upsert, but it seems ecto
- # has problems accessing the constraint inside the jsonb.
- {:error, _} ->
- Object.get_cached_by_ap_id(context).id
- end
- end
- end
-
- def conversation_id_to_context(id) do
- with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
- context
- else
- _e ->
- {:error, dgettext("errors", "No such conversation")}
- end
- end
-
def validate_character_limit("" = _full_payload, [] = _attachments) do
{:error, dgettext("errors", "Cannot post an empty status without attachments")}
end
diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex
index e7feefc07..3be71c1b6 100644
--- a/lib/pleroma/web/federator.ex
+++ b/lib/pleroma/web/federator.ex
@@ -61,10 +61,8 @@ defmodule Pleroma.Web.Federator do
def perform(:publish, activity) do
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
- with %User{} = actor <- User.get_cached_by_ap_id(activity.data["actor"]),
- {:ok, actor} <- User.ensure_keys_present(actor) do
- Publisher.publish(actor, activity)
- end
+ %User{} = actor = User.get_cached_by_ap_id(activity.data["actor"])
+ Publisher.publish(actor, activity)
end
def perform(:incoming_ap_doc, params) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 50c12a1b1..7c24c35d2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -76,16 +76,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(
OAuthScopesPlug,
- %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
+ %{scopes: ["follow", "write:follows"]}
+ when action in [:follow_by_uri, :follow, :unfollow, :remove_from_followers]
)
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
- @relationship_actions [:follow, :unfollow]
+ @relationship_actions [:follow, :unfollow, :remove_from_followers]
@needs_account ~W(
- followers following lists follow unfollow mute unmute block unblock note endorse unendorse
+ followers following lists follow unfollow mute unmute block unblock
+ note endorse unendorse remove_from_followers
)a
plug(
@@ -411,6 +413,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/mute"
def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
+ params =
+ params
+ |> Map.put_new(:duration, Map.get(params, :expires_in, 0))
+
with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
render(conn, "relationship.json", user: muter, target: muted)
else
@@ -473,6 +479,20 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
end
+ @doc "POST /api/v1/accounts/:id/remove_from_followers"
+ def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
+ {:error, "Can not unfollow yourself"}
+ end
+
+ def remove_from_followers(%{assigns: %{user: followed, account: follower}} = conn, _params) do
+ with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
+ render(conn, "relationship.json", user: followed, target: follower)
+ else
+ nil ->
+ render_error(conn, :not_found, "Record not found")
+ end
+ end
+
@doc "POST /api/v1/follows"
def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
case User.get_cached_by_nickname(uri) do
@@ -491,7 +511,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
users =
user
|> User.muted_users_relation(_restrict_deactivated = true)
- |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
+ |> Pleroma.Pagination.fetch_paginated(params)
conn
|> add_link_headers(users)
@@ -499,7 +519,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
users: users,
for: user,
as: :user,
- embed_relationships: embed_relationships?(params)
+ embed_relationships: embed_relationships?(params),
+ mutes: true
)
end
@@ -508,7 +529,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
users =
user
|> User.blocked_users_relation(_restrict_deactivated = true)
- |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
+ |> Pleroma.Pagination.fetch_paginated(params)
conn
|> add_link_headers(users)
diff --git a/lib/pleroma/web/mastodon_api/controllers/announcement_controller.ex b/lib/pleroma/web/mastodon_api/controllers/announcement_controller.ex
new file mode 100644
index 000000000..080af96d5
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/announcement_controller.ex
@@ -0,0 +1,60 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.AnnouncementController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper,
+ only: [
+ json_response: 3
+ ]
+
+ alias Pleroma.Announcement
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ # Mastodon docs say this only requires a user token, no scopes needed
+ # As the op `|` requires at least one scope to be present, we use `&` here.
+ plug(
+ OAuthScopesPlug,
+ %{scopes: [], op: :&}
+ when action in [:index]
+ )
+
+ # Same as in MastodonAPI specs
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:accounts"]}
+ when action in [:mark_read]
+ )
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AnnouncementOperation
+
+ @doc "GET /api/v1/announcements"
+ def index(%{assigns: %{user: user}} = conn, _params) do
+ render(conn, "index.json", announcements: all_visible(), user: user)
+ end
+
+ def index(conn, _params) do
+ render(conn, "index.json", announcements: all_visible(), user: nil)
+ end
+
+ defp all_visible do
+ Announcement.list_all_visible()
+ end
+
+ @doc "POST /api/v1/announcements/:id/dismiss"
+ def mark_read(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
+ with announcement when not is_nil(announcement) <- Announcement.get_by_id(id),
+ {:ok, _} <- Announcement.mark_read_by(announcement, user) do
+ json_response(conn, :ok, %{})
+ else
+ _ ->
+ {:error, :not_found}
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index 932bc6423..a490e8319 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -51,11 +51,12 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
move
pleroma:emoji_reaction
poll
+ update
}
def index(%{assigns: %{user: user}} = conn, params) do
params =
Map.new(params, fn {k, v} -> {to_string(k), v} end)
- |> Map.put_new("include_types", @default_notification_types)
+ |> Map.put_new("types", Map.get(params, :include_types, @default_notification_types))
notifications = MastodonAPI.get_notifications(user, params)
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index 42a95bdc5..e594ea491 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -38,7 +38,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
:index,
:show,
:card,
- :context
+ :context,
+ :show_history,
+ :show_source
]
)
@@ -49,7 +51,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
:create,
:delete,
:reblog,
- :unreblog
+ :unreblog,
+ :update
]
)
@@ -191,6 +194,59 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
create(%Plug.Conn{conn | body_params: params}, %{})
end
+ @doc "GET /api/v1/statuses/:id/history"
+ def show_history(%{assigns: assigns} = conn, %{id: id} = params) do
+ with user = assigns[:user],
+ %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ true <- Visibility.visible_for_user?(activity, user) do
+ try_render(conn, "history.json",
+ activity: activity,
+ for: user,
+ with_direct_conversation_id: true,
+ with_muted: Map.get(params, :with_muted, false)
+ )
+ else
+ _ -> {:error, :not_found}
+ end
+ end
+
+ @doc "GET /api/v1/statuses/:id/source"
+ def show_source(%{assigns: assigns} = conn, %{id: id} = _params) do
+ with user = assigns[:user],
+ %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ true <- Visibility.visible_for_user?(activity, user) do
+ try_render(conn, "source.json",
+ activity: activity,
+ for: user
+ )
+ else
+ _ -> {:error, :not_found}
+ end
+ end
+
+ @doc "PUT /api/v1/statuses/:id"
+ 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"
def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index ba7239476..293c61b41 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -112,6 +112,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:muting_user, user)
|> Map.put(:reply_filtering_user, user)
|> Map.put(:instance, params[:instance])
+ # Restricts unfederated content to authenticated users
+ |> Map.put(:includes_local_public, not is_nil(user))
|> ActivityPub.fetch_public_activities()
conn
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
index 5e32b9611..b4d092eed 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -65,7 +65,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
user
|> Notification.for_user_query(options)
- |> restrict(:include_types, options)
+ |> restrict(:types, options)
|> restrict(:exclude_types, options)
|> restrict(:account_ap_id, options)
|> Pagination.fetch_paginated(params)
@@ -80,7 +80,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
defp cast_params(params) do
param_types = %{
exclude_types: {:array, :string},
- include_types: {:array, :string},
+ types: {:array, :string},
exclude_visibilities: {:array, :string},
reblogs: :boolean,
with_muted: :boolean,
@@ -92,7 +92,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
changeset.changes
end
- defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do
+ defp restrict(query, :types, %{types: mastodon_types = [_ | _]}) do
where(query, [n], n.type in ^mastodon_types)
end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 988eedbb1..2260bf5da 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -311,6 +311,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_unread_conversation_count(user, opts[:for])
|> maybe_put_unread_notification_count(user, opts[:for])
|> maybe_put_email_address(user, opts[:for])
+ |> maybe_put_mute_expires_at(user, opts[:for], opts)
|> maybe_show_birthday(user, opts[:for])
end
@@ -434,6 +435,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_email_address(data, _, _), do: data
+ defp maybe_put_mute_expires_at(data, %User{} = user, target, %{mutes: true}) do
+ Map.put(
+ data,
+ :mute_expires_at,
+ UserRelationship.get_mute_expire_date(target, user)
+ )
+ end
+
+ defp maybe_put_mute_expires_at(data, _, _, _), do: data
+
defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do
data
|> Kernel.put_in([:pleroma, :birthday], user.birthday)
diff --git a/lib/pleroma/web/mastodon_api/views/announcement_view.ex b/lib/pleroma/web/mastodon_api/views/announcement_view.ex
new file mode 100644
index 000000000..93fdfb1f1
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/announcement_view.ex
@@ -0,0 +1,15 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.AnnouncementView do
+ use Pleroma.Web, :view
+
+ def render("index.json", %{announcements: announcements, user: user}) do
+ render_many(announcements, __MODULE__, "show.json", user: user)
+ end
+
+ def render("show.json", %{announcement: announcement, user: user}) do
+ Pleroma.Announcement.render_json(announcement, for: user)
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index ee52475d5..1cc230316 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
uri: Pleroma.Web.Endpoint.url(),
title: Keyword.get(instance, :name),
description: Keyword.get(instance, :description),
+ short_description: Keyword.get(instance, :short_description),
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
email: Keyword.get(instance, :email),
urls: %{
@@ -68,6 +69,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
"shareable_emoji_packs",
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
+ "editing",
if Config.get([:activitypub, :blockers_visible]) do
"blockers_visible"
end,
@@ -97,7 +99,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
end,
if Config.get([:instance, :profile_directory]) do
"profile_directory"
- end
+ end,
+ "pleroma:get:main/ostatus"
]
|> Enum.filter(& &1)
end
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index 0dc7f3beb..b5b5b2376 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -19,7 +19,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
- @parent_types ~w{Like Announce EmojiReact}
+ defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id
+
+ defp object_id_for(%{data: %{"object" => id}}) when is_binary(id), do: id
+
+ @parent_types ~w{Like Announce EmojiReact Update}
def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
activities = Enum.map(notifications, & &1.activity)
@@ -30,7 +34,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
%{data: %{"type" => type}} ->
type in @parent_types
end)
- |> Enum.map(& &1.data["object"])
+ |> Enum.map(&object_id_for/1)
|> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left)
|> Pleroma.Repo.all()
@@ -78,9 +82,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
parent_activity_fn = fn ->
if opts[:parent_activities] do
- Activity.Queries.find_by_object_ap_id(opts[:parent_activities], activity.data["object"])
+ Activity.Queries.find_by_object_ap_id(opts[:parent_activities], object_id_for(activity))
else
- Activity.get_create_by_object_ap_id(activity.data["object"])
+ Activity.get_create_by_object_ap_id(object_id_for(activity))
end
end
@@ -109,6 +113,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
"reblog" ->
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
+ "update" ->
+ put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
+
"move" ->
put_target(response, activity, reading_user, %{})
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 1ebfd6740..b949d8f9a 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -57,11 +57,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end)
end
- defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
- do: context_id
-
- defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
- do: Utils.context_to_conversation_id(context)
+ # DEPRECATED This field seems to be a left-over from the StatusNet era.
+ # If your application uses `pleroma.conversation_id`: this field is deprecated.
+ # It is currently stubbed instead by doing a CRC32 of the context, and
+ # clearing the MSB to avoid overflow exceptions with signed integers on the
+ # different clients using this field (Java/Kotlin code, mostly; see Husky.)
+ # This should be removed in a future version of Pleroma. Pleroma-FE currently
+ # depends on this field, as well.
+ defp get_context_id(%{data: %{"context" => context}}) when is_binary(context) do
+ use Bitwise
+
+ :erlang.crc32(context)
+ |> band(bnot(0x8000_0000))
+ end
defp get_context_id(_), do: nil
@@ -258,10 +266,30 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
created_at = Utils.to_masto_date(object.data["published"])
+ edited_at =
+ with %{"updated" => updated} <- object.data,
+ date <- Utils.to_masto_date(updated),
+ true <- date != "" do
+ date
+ else
+ _ ->
+ nil
+ end
+
reply_to = get_reply_to(activity, opts)
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
+ history_len =
+ 1 +
+ (Object.Updater.history_for(object.data)
+ |> Map.get("orderedItems")
+ |> length())
+
+ # See render("history.json", ...) for more details
+ # Here the implicit index of the current content is 0
+ chrono_order = history_len - 1
+
content =
object
|> render_content()
@@ -271,14 +299,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|> Activity.HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
- "mastoapi:content"
+ "mastoapi:content:#{chrono_order}"
)
content_plaintext =
content
|> Activity.HTML.get_cached_stripped_html_for_activity(
activity,
- "mastoapi:content"
+ "mastoapi:content:#{chrono_order}"
)
summary = object.data["summary"] || ""
@@ -344,8 +372,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
reblog: nil,
card: card,
content: content_html,
- text: opts[:with_source] && object.data["source"],
+ text: opts[:with_source] && get_source_text(object.data["source"]),
created_at: created_at,
+ edited_at: edited_at,
reblogs_count: announcement_count,
replies_count: object.data["repliesCount"] || 0,
favourites_count: like_count,
@@ -367,6 +396,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
pleroma: %{
local: activity.local,
conversation_id: get_context_id(activity),
+ context: object.data["context"],
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary},
@@ -384,6 +414,100 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
nil
end
+ def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
+ object = Object.normalize(activity, fetch: false)
+
+ hashtags = Object.hashtags(object)
+
+ user = CommonAPI.get_user(activity.data["actor"])
+
+ past_history =
+ Object.Updater.history_for(object.data)
+ |> Map.get("orderedItems")
+ |> Enum.map(&Map.put(&1, "id", object.data["id"]))
+ |> Enum.map(&%Object{data: &1, id: object.id})
+
+ history =
+ [object | past_history]
+ # Mastodon expects the original to be at the first
+ |> Enum.reverse()
+ |> Enum.with_index()
+ |> Enum.map(fn {object, chrono_order} ->
+ %{
+ # The history is prepended every time there is a new edit.
+ # In chrono_order, the oldest item is always at 0, and so on.
+ # The chrono_order is an invariant kept between edits.
+ chrono_order: chrono_order,
+ object: object
+ }
+ end)
+
+ individual_opts =
+ opts
+ |> Map.put(:as, :item)
+ |> Map.put(:user, user)
+ |> Map.put(:hashtags, hashtags)
+
+ render_many(history, StatusView, "history_item.json", individual_opts)
+ end
+
+ def render(
+ "history_item.json",
+ %{
+ activity: activity,
+ user: user,
+ item: %{object: object, chrono_order: chrono_order},
+ hashtags: hashtags
+ } = opts
+ ) do
+ sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
+
+ attachment_data = object.data["attachment"] || []
+ attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
+
+ created_at = Utils.to_masto_date(object.data["updated"] || object.data["published"])
+
+ content =
+ object
+ |> render_content()
+
+ content_html =
+ content
+ |> Activity.HTML.get_cached_scrubbed_html_for_activity(
+ User.html_filter_policy(opts[:for]),
+ activity,
+ "mastoapi:content:#{chrono_order}"
+ )
+
+ summary = object.data["summary"] || ""
+
+ %{
+ account:
+ AccountView.render("show.json", %{
+ user: user,
+ for: opts[:for]
+ }),
+ content: content_html,
+ sensitive: sensitive,
+ spoiler_text: summary,
+ created_at: created_at,
+ media_attachments: attachments,
+ emojis: build_emojis(object.data["emoji"]),
+ poll: render(PollView, "show.json", object: object, for: opts[:for])
+ }
+ end
+
+ def render("source.json", %{activity: %{data: %{"object" => _object}} = activity} = _opts) do
+ object = Object.normalize(activity, fetch: false)
+
+ %{
+ id: activity.id,
+ text: get_source_text(Map.get(object.data, "source", "")),
+ spoiler_text: Map.get(object.data, "summary", ""),
+ content_type: get_source_content_type(object.data["source"])
+ }
+ end
+
def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
page_url_data = URI.parse(page_url)
@@ -436,10 +560,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
true -> "unknown"
end
- <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
+ attachment_id =
+ with {_, ap_id} when is_binary(ap_id) <- {:ap_id, attachment["id"]},
+ {_, %Object{data: _object_data, id: object_id}} <-
+ {:object, Object.get_by_ap_id(ap_id)} do
+ to_string(object_id)
+ else
+ _ ->
+ <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
+ to_string(attachment["id"] || hash_id)
+ end
%{
- id: to_string(attachment["id"] || hash_id),
+ id: attachment_id,
url: href,
remote_url: href,
preview_url: href_preview,
@@ -601,4 +734,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
defp build_image_url(_, _), do: nil
+
+ defp get_source_text(%{"content" => content} = _source) do
+ content
+ end
+
+ defp get_source_text(source) when is_binary(source) do
+ source
+ end
+
+ defp get_source_text(_) do
+ ""
+ end
+
+ defp get_source_content_type(%{"mediaType" => type} = _source) do
+ type
+ end
+
+ defp get_source_content_type(_source) do
+ Utils.get_content_type(nil)
+ end
end
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index 684a2b800..d2ad62c13 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -54,7 +54,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
media_proxy_url = MediaProxy.url(url)
with {:ok, %{status: status} = head_response} when status in 200..299 <-
- Pleroma.HTTP.request("head", media_proxy_url, [], [], name: MyFinch) do
+ Pleroma.HTTP.request("HEAD", media_proxy_url, [], [], pool: :media) do
content_type = Tesla.get_header(head_response, "content-type")
content_length = Tesla.get_header(head_response, "content-length")
content_length = content_length && String.to_integer(content_length)
diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
index 8052eaa44..15414a988 100644
--- a/lib/pleroma/web/metadata/utils.ex
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -8,8 +8,8 @@ defmodule Pleroma.Web.Metadata.Utils do
alias Pleroma.Formatter
alias Pleroma.HTML
- def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
- content
+ defp scrub_html_and_truncate_object_field(field, object) do
+ field
# html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
@@ -19,6 +19,17 @@ defmodule Pleroma.Web.Metadata.Utils do
|> Formatter.truncate()
end
+ def scrub_html_and_truncate(%{data: %{"summary" => summary}} = object)
+ when is_binary(summary) and summary != "" do
+ summary
+ |> scrub_html_and_truncate_object_field(object)
+ end
+
+ def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
+ content
+ |> scrub_html_and_truncate_object_field(object)
+ end
+
def scrub_html_and_truncate(content, max_length \\ 200) when is_binary(content) do
content
|> scrub_html
diff --git a/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex
index 1a0548295..b9daed22b 100644
--- a/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.BackupController do
alias Pleroma.Web.Plugs.OAuthScopesPlug
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action in [:index, :create])
+ plug(OAuthScopesPlug, %{scopes: ["read:backups"]} when action in [:index, :create])
plug(Pleroma.Web.ApiSpec.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaBackupOperation
diff --git a/lib/pleroma/web/pleroma_api/controllers/settings_controller.ex b/lib/pleroma/web/pleroma_api/controllers/settings_controller.ex
new file mode 100644
index 000000000..1136575b6
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/settings_controller.ex
@@ -0,0 +1,79 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.SettingsController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:accounts"]} when action in [:update]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:accounts"]} when action in [:show]
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaSettingsOperation
+
+ @doc "GET /api/v1/pleroma/settings/:app"
+ def show(%{assigns: %{user: user}} = conn, %{app: app} = _params) do
+ conn
+ |> json(get_settings(user, app))
+ end
+
+ @doc "PATCH /api/v1/pleroma/settings/:app"
+ def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{app: app} = _params) do
+ settings =
+ get_settings(user, app)
+ |> merge_recursively(body_params)
+
+ with changeset <-
+ Pleroma.User.update_changeset(
+ user,
+ %{pleroma_settings_store: %{app => settings}}
+ ),
+ {:ok, _} <- Pleroma.Repo.update(changeset) do
+ conn
+ |> json(settings)
+ end
+ end
+
+ defp merge_recursively(old, %{} = new) do
+ old = ensure_object(old)
+
+ Enum.reduce(
+ new,
+ old,
+ fn
+ {k, nil}, acc ->
+ Map.drop(acc, [k])
+
+ {k, %{} = new_child}, acc ->
+ Map.put(acc, k, merge_recursively(acc[k], new_child))
+
+ {k, v}, acc ->
+ Map.put(acc, k, v)
+ end
+ )
+ end
+
+ defp get_settings(user, app) do
+ user.pleroma_settings_store
+ |> Map.get(app, %{})
+ |> ensure_object()
+ end
+
+ defp ensure_object(%{} = object) do
+ object
+ end
+
+ defp ensure_object(_) do
+ %{}
+ end
+end
diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex
index d023754a6..4bf325218 100644
--- a/lib/pleroma/web/plugs/http_signature_plug.ex
+++ b/lib/pleroma/web/plugs/http_signature_plug.ex
@@ -25,21 +25,58 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
end
end
+ defp validate_signature(conn, request_target) do
+ # Newer drafts for HTTP signatures now use @request-target instead of the
+ # old (request-target). We'll now support both for incoming signatures.
+ conn =
+ conn
+ |> put_req_header("(request-target)", request_target)
+ |> put_req_header("@request-target", request_target)
+
+ HTTPSignatures.validate_conn(conn)
+ end
+
+ defp validate_signature(conn) do
+ # This (request-target) is non-standard, but many implementations do it
+ # this way due to a misinterpretation of
+ # https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-06
+ # "path" was interpreted as not having the query, though later examples
+ # show that it must be the absolute path + query. This behavior is kept to
+ # make sure most software (Pleroma itself, Mastodon, and probably others)
+ # do not break.
+ request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}"
+
+ # This is the proper way to build the @request-target, as expected by
+ # many HTTP signature libraries, clarified in the following draft:
+ # https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html#section-2.2.6
+ # It is the same as before, but containing the query part as well.
+ proper_target = request_target <> "?#{conn.query_string}"
+
+ cond do
+ # Normal, non-standard behavior but expected by Pleroma and more.
+ validate_signature(conn, request_target) ->
+ true
+
+ # Has query string and the previous one failed: let's try the standard.
+ conn.query_string != "" ->
+ validate_signature(conn, proper_target)
+
+ # If there's no query string and signature fails, it's rotten.
+ true ->
+ false
+ end
+ end
+
defp maybe_assign_valid_signature(conn) do
if has_signature_header?(conn) do
- # set (request-target) header to the appropriate value
- # we also replace the digest header with the one we computed
- request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}"
-
+ # we replace the digest header with the one we computed in DigestPlug
conn =
- conn
- |> put_req_header("(request-target)", request_target)
- |> case do
+ case conn do
%{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest)
conn -> conn
end
- assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
+ assign(conn, :valid_signature, validate_signature(conn))
else
Logger.debug("No signature header!")
conn
diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex
index 0f74d626b..ba04ddb72 100644
--- a/lib/pleroma/web/plugs/o_auth_plug.ex
+++ b/lib/pleroma/web/plugs/o_auth_plug.ex
@@ -47,15 +47,17 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
#
@spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil
defp fetch_user_and_token(token) do
- query =
+ token_query =
from(t in Token,
- where: t.token == ^token,
- join: user in assoc(t, :user),
- preload: [user: user]
+ where: t.token == ^token
)
- with %Token{user: user} = token_record <- Repo.one(query) do
+ with %Token{user_id: user_id} = token_record <- Repo.one(token_query),
+ false <- is_nil(user_id),
+ %User{} = user <- User.get_cached_by_id(user_id) do
{:ok, user, token_record}
+ else
+ _ -> nil
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index ceb6c3cfd..2ea3ea7c1 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -229,6 +229,12 @@ defmodule Pleroma.Web.Router do
post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup)
+
+ get("/announcements", AnnouncementController, :index)
+ post("/announcements", AnnouncementController, :create)
+ get("/announcements/:id", AnnouncementController, :show)
+ patch("/announcements/:id", AnnouncementController, :change)
+ delete("/announcements/:id", AnnouncementController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if enabled by config)
@@ -331,6 +337,7 @@ defmodule Pleroma.Web.Router do
pipe_through(:pleroma_html)
post("/main/ostatus", UtilController, :remote_subscribe)
+ get("/main/ostatus", UtilController, :show_subscribe_form)
get("/ostatus_subscribe", RemoteFollowController, :follow)
post("/ostatus_subscribe", RemoteFollowController, :do_follow)
end
@@ -343,6 +350,11 @@ defmodule Pleroma.Web.Router do
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notificaton_settings)
post("/disable_account", UtilController, :disable_account)
+ post("/move_account", UtilController, :move_account)
+
+ put("/aliases", UtilController, :add_alias)
+ get("/aliases", UtilController, :list_aliases)
+ delete("/aliases", UtilController, :delete_alias)
end
scope "/api/pleroma", Pleroma.Web.PleromaAPI do
@@ -452,6 +464,13 @@ defmodule Pleroma.Web.Router do
get("/birthdays", AccountController, :birthdays)
end
+ scope [] do
+ pipe_through(:authenticated_api)
+
+ get("/settings/:app", SettingsController, :show)
+ patch("/settings/:app", SettingsController, :update)
+ end
+
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
end
@@ -491,6 +510,7 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/note", AccountController, :note)
post("/accounts/:id/pin", AccountController, :endorse)
post("/accounts/:id/unpin", AccountController, :unendorse)
+ post("/accounts/:id/remove_from_followers", AccountController, :remove_from_followers)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :mark_as_read)
@@ -552,6 +572,7 @@ defmodule Pleroma.Web.Router do
get("/bookmarks", StatusController, :bookmarks)
post("/statuses", StatusController, :create)
+ put("/statuses/:id", StatusController, :update)
delete("/statuses/:id", StatusController, :delete)
post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog)
@@ -575,6 +596,9 @@ defmodule Pleroma.Web.Router do
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
get("/timelines/list/:list_id", TimelineController, :list)
+
+ get("/announcements", AnnouncementController, :index)
+ post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
@@ -608,6 +632,8 @@ defmodule Pleroma.Web.Router do
get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
+ get("/statuses/:id/history", StatusController, :show_history)
+ get("/statuses/:id/source", StatusController, :show_source)
get("/custom_emojis", CustomEmojiController, :index)
@@ -669,11 +695,6 @@ defmodule Pleroma.Web.Router do
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
- # Notice compatibility routes for other frontends
- get("/@:nickname/:id", OStatus.OStatusController, :notice)
- get("/@:nickname/posts/:id", OStatus.OStatusController, :notice)
- get("/:nickname/status/:id", OStatus.OStatusController, :notice)
-
# Mastodon compatibility routes
get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
index b20a3689c..97c41c6f9 100644
--- a/lib/pleroma/web/static_fe/static_fe_controller.ex
+++ b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -167,15 +167,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id)
- defp assign_id(%{path_info: ["@" <> _nickname, notice_id]} = conn, _opts),
- do: assign(conn, :notice_id, notice_id)
-
- defp assign_id(%{path_info: ["@" <> _nickname, "posts", notice_id]} = conn, _opts),
- do: assign(conn, :notice_id, notice_id)
-
- defp assign_id(%{path_info: [_nickname, "status", notice_id]} = conn, _opts),
- do: assign(conn, :notice_id, notice_id)
-
defp assign_id(%{path_info: ["users", user_id]} = conn, _opts),
do: assign(conn, :username_or_id, user_id)
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index ff7f62a1e..fe909df0a 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -296,6 +296,24 @@ defmodule Pleroma.Web.Streamer do
defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop
+ defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do
+ create_activity =
+ Pleroma.Activity.get_create_by_object_ap_id(item.object.data["id"])
+ |> Map.put(:object, item.object)
+
+ anon_render = StreamerView.render("status_update.json", create_activity)
+
+ Registry.dispatch(@registry, topic, fn list ->
+ Enum.each(list, fn {pid, auth?} ->
+ if auth? do
+ send(pid, {:render_with_user, StreamerView, "status_update.json", create_activity})
+ else
+ send(pid, {:text, anon_render})
+ end
+ end)
+ end)
+ end
+
defp push_to_socket(topic, item) do
anon_render = StreamerView.render("update.json", item)
diff --git a/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex b/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex
new file mode 100644
index 000000000..d77174967
--- /dev/null
+++ b/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex
@@ -0,0 +1,10 @@
+<%= if @error do %>
+ <h2><%= Gettext.dpgettext("static_pages", "status interact error", "Error: %{error}", error: @error) %></h2>
+<% else %>
+ <h2><%= raw Gettext.dpgettext("static_pages", "status interact header", "Interacting with %{nickname}'s %{status_link}", nickname: safe_to_string(html_escape(@nickname)), status_link: safe_to_string(link(Gettext.dpgettext("static_pages", "status interact header - status link text", "status"), to: @status_link))) %></h2>
+ <%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "status"], fn f -> %>
+ <%= hidden_input f, :status_id, value: @status_id %>
+ <%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %>
+ <%= submit Gettext.dpgettext("static_pages", "status interact authorization button", "Interact") %>
+ <% end %>
+<% end %>
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index 53465b124..d5a24ae6c 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -7,16 +7,26 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
require Logger
+ alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Emoji
alias Pleroma.Healthcheck
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.WebFinger
- plug(Pleroma.Web.ApiSpec.CastAndValidate when action != :remote_subscribe)
- plug(Pleroma.Web.Plugs.FederatingPlug when action == :remote_subscribe)
+ plug(
+ Pleroma.Web.ApiSpec.CastAndValidate
+ when action != :remote_subscribe and action != :show_subscribe_form
+ )
+
+ plug(
+ Pleroma.Web.Plugs.FederatingPlug
+ when action == :remote_subscribe
+ when action == :show_subscribe_form
+ )
plug(
OAuthScopesPlug,
@@ -26,13 +36,24 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
:change_password,
:delete_account,
:update_notificaton_settings,
- :disable_account
+ :disable_account,
+ :move_account,
+ :add_alias,
+ :delete_alias
+ ]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:accounts"]}
+ when action in [
+ :list_aliases
]
)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TwitterUtilOperation
- def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
+ def show_subscribe_form(conn, %{"nickname" => nick}) do
with %User{} = user <- User.get_cached_by_nickname(nick),
avatar = User.avatar_url(user) do
conn
@@ -42,11 +63,52 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
- error: "Could not find user"
+ error:
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "remote follow error message - user not found",
+ "Could not find user"
+ )
})
end
end
+ def show_subscribe_form(conn, %{"status_id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id(id),
+ {:ok, ap_id} <- get_ap_id(activity),
+ %User{} = user <- User.get_cached_by_ap_id(activity.actor),
+ avatar = User.avatar_url(user) do
+ conn
+ |> render("status_interact.html", %{
+ status_link: ap_id,
+ status_id: id,
+ nickname: user.nickname,
+ avatar: avatar,
+ error: false
+ })
+ else
+ _e ->
+ render(conn, "status_interact.html", %{
+ status_id: id,
+ avatar: nil,
+ error:
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "status interact error message - status not found",
+ "Could not find status"
+ )
+ })
+ end
+ end
+
+ def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
+ show_subscribe_form(conn, %{"nickname" => nick})
+ end
+
+ def remote_subscribe(conn, %{"status_id" => id, "profile" => _}) do
+ show_subscribe_form(conn, %{"status_id" => id})
+ end
+
def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
%User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do
@@ -57,7 +119,33 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
- error: "Something went wrong."
+ error:
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "remote follow error message - unknown error",
+ "Something went wrong."
+ )
+ })
+ end
+ end
+
+ def remote_subscribe(conn, %{"status" => %{"status_id" => id, "profile" => profile}}) do
+ with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
+ %Activity{} = activity <- Activity.get_by_id(id),
+ {:ok, ap_id} <- get_ap_id(activity) do
+ conn
+ |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
+ else
+ _e ->
+ render(conn, "status_interact.html", %{
+ status_id: id,
+ avatar: nil,
+ error:
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "status interact error message - unknown error",
+ "Something went wrong."
+ )
})
end
end
@@ -71,6 +159,15 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
+ defp get_ap_id(activity) do
+ object = Pleroma.Object.normalize(activity, fetch: false)
+
+ case object do
+ %{data: %{"id" => ap_id}} -> {:ok, ap_id}
+ _ -> {:no_ap_id, nil}
+ end
+ end
+
def frontend_configurations(conn, _params) do
render(conn, "frontend_configurations.json")
end
@@ -158,6 +255,91 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
+ def move_account(%{assigns: %{user: user}, body_params: body_params} = conn, %{}) do
+ case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
+ {:ok, user} ->
+ with {:ok, target_user} <- find_or_fetch_user_by_nickname(body_params.target_account),
+ {:ok, _user} <- ActivityPub.move(user, target_user) do
+ json(conn, %{status: "success"})
+ else
+ {:not_found, _} ->
+ conn
+ |> put_status(404)
+ |> json(%{error: "Target account not found."})
+
+ {:error, error} ->
+ json(conn, %{error: error})
+ end
+
+ {:error, msg} ->
+ json(conn, %{error: msg})
+ end
+ end
+
+ def add_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do
+ with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
+ {:ok, _user} <- user |> User.add_alias(alias_user) do
+ json(conn, %{status: "success"})
+ else
+ {:not_found, _} ->
+ conn
+ |> put_status(404)
+ |> json(%{error: "Target account does not exist."})
+
+ {:error, error} ->
+ json(conn, %{error: error})
+ end
+ end
+
+ def delete_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do
+ with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
+ {:ok, _user} <- user |> User.delete_alias(alias_user) do
+ json(conn, %{status: "success"})
+ else
+ {:error, :no_such_alias} ->
+ conn
+ |> put_status(404)
+ |> json(%{error: "Account has no such alias."})
+
+ {:error, error} ->
+ json(conn, %{error: error})
+ end
+ end
+
+ def list_aliases(%{assigns: %{user: user}} = conn, %{}) do
+ alias_nicks =
+ user
+ |> User.alias_users()
+ |> Enum.map(&User.full_nickname/1)
+
+ json(conn, %{aliases: alias_nicks})
+ end
+
+ defp find_user_by_nickname(nickname) do
+ user = User.get_cached_by_nickname(nickname)
+
+ if user == nil do
+ {:not_found, nil}
+ else
+ {:ok, user}
+ end
+ end
+
+ defp find_or_fetch_user_by_nickname(nickname) do
+ user = User.get_by_nickname(nickname)
+
+ if user != nil and user.local do
+ {:ok, user}
+ else
+ with {:ok, user} <- User.fetch_by_nickname(nickname) do
+ {:ok, user}
+ else
+ _ ->
+ {:not_found, nil}
+ end
+ end
+ end
+
def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new())
end
diff --git a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex b/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
index bd33d4c0a..8902261b0 100644
--- a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
+++ b/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
@@ -7,5 +7,9 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do
import Phoenix.HTML.Form
alias Pleroma.Web.Gettext
- defdelegate avatar_url(user), to: Pleroma.User
+ def avatar_url(user) do
+ user
+ |> Pleroma.User.avatar_url()
+ |> Pleroma.Web.MediaProxy.url()
+ end
end
diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex
index 69f243097..31b7c0c0c 100644
--- a/lib/pleroma/web/twitter_api/views/util_view.ex
+++ b/lib/pleroma/web/twitter_api/views/util_view.ex
@@ -4,7 +4,9 @@
defmodule Pleroma.Web.TwitterAPI.UtilView do
use Pleroma.Web, :view
+ import Phoenix.HTML
import Phoenix.HTML.Form
+ import Phoenix.HTML.Link
alias Pleroma.Config
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Gettext
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index 16c2b7d61..6a55242b0 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -25,6 +25,20 @@ defmodule Pleroma.Web.StreamerView do
|> Jason.encode!()
end
+ def render("status_update.json", %Activity{} = activity, %User{} = user) do
+ %{
+ event: "status.update",
+ payload:
+ Pleroma.Web.MastodonAPI.StatusView.render(
+ "show.json",
+ activity: activity,
+ for: user
+ )
+ |> Jason.encode!()
+ }
+ |> Jason.encode!()
+ end
+
def render("notification.json", %Notification{} = notify, %User{} = user) do
%{
event: "notification",
@@ -51,6 +65,19 @@ defmodule Pleroma.Web.StreamerView do
|> Jason.encode!()
end
+ def render("status_update.json", %Activity{} = activity) do
+ %{
+ event: "status.update",
+ payload:
+ Pleroma.Web.MastodonAPI.StatusView.render(
+ "show.json",
+ activity: activity
+ )
+ |> Jason.encode!()
+ }
+ |> Jason.encode!()
+ end
+
def render("chat_update.json", %{chat_message_reference: cm_ref}) do
# Explicitly giving the cmr for the object here, so we don't accidentally
# send a later 'last_message' that was inserted between inserting this and
diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex
index 6cd9962ce..77ff40f46 100644
--- a/lib/pleroma/web/web_finger.ex
+++ b/lib/pleroma/web/web_finger.ex
@@ -63,8 +63,6 @@ defmodule Pleroma.Web.WebFinger do
end
def represent_user(user, "JSON") do
- {:ok, user} = User.ensure_keys_present(user)
-
%{
"subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
"aliases" => gather_aliases(user),
@@ -73,8 +71,6 @@ defmodule Pleroma.Web.WebFinger do
end
def represent_user(user, "XML") do
- {:ok, user} = User.ensure_keys_present(user)
-
aliases =
user
|> gather_aliases()
diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex
index 3caef85b7..7657fa9ce 100644
--- a/lib/pleroma/workers/backup_worker.ex
+++ b/lib/pleroma/workers/backup_worker.ex
@@ -37,10 +37,7 @@ defmodule Pleroma.Workers.BackupWorker do
backup_id |> Backup.get() |> Backup.process(),
{:ok, _job} <- schedule_deletion(backup),
:ok <- Backup.remove_outdated(backup),
- {:ok, _} <-
- backup
- |> Pleroma.Emails.UserEmail.backup_is_ready_email(admin_user_id)
- |> Pleroma.Emails.Mailer.deliver() do
+ :ok <- maybe_deliver_email(backup, admin_user_id) do
{:ok, backup}
end
end
@@ -51,4 +48,23 @@ defmodule Pleroma.Workers.BackupWorker do
nil -> :ok
end
end
+
+ defp has_email?(user) do
+ not is_nil(user.email) and user.email != ""
+ end
+
+ defp maybe_deliver_email(backup, admin_user_id) do
+ has_mailer = Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled])
+ backup = backup |> Pleroma.Repo.preload(:user)
+
+ if has_email?(backup.user) and has_mailer do
+ backup
+ |> Pleroma.Emails.UserEmail.backup_is_ready_email(admin_user_id)
+ |> Pleroma.Emails.Mailer.deliver()
+
+ :ok
+ else
+ :ok
+ end
+ end
end
diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex
index 268b5f30f..c41b44e14 100644
--- a/lib/pleroma/workers/receiver_worker.ex
+++ b/lib/pleroma/workers/receiver_worker.ex
@@ -9,6 +9,12 @@ defmodule Pleroma.Workers.ReceiverWorker do
@impl Oban.Worker
def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do
- Federator.perform(:incoming_ap_doc, params)
+ with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
+ {:ok, res}
+ else
+ {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
+ {:error, {:reject, reason}} -> {:cancel, reason}
+ e -> e
+ end
end
end