aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorRoman Chvanikov <chvanikoff@pm.me>2020-06-23 20:56:55 +0300
committerRoman Chvanikov <chvanikoff@pm.me>2020-06-23 20:56:55 +0300
commit1471b70ef1550df91687276968108231f1bab604 (patch)
tree3a59e7497d665d5ce3d6445132453518bc50601d /lib
parent604a83ae3efe21b231fb524203a883a062102546 (diff)
parentc7d69e925664ce7125c3938c298d72e94a2a1349 (diff)
downloadpleroma-1471b70ef1550df91687276968108231f1bab604.tar.gz
Merge develop
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/tasks/pleroma/config.ex11
-rw-r--r--lib/mix/tasks/pleroma/emoji.ex6
-rw-r--r--lib/mix/tasks/pleroma/user.ex12
-rw-r--r--lib/pleroma/activity.ex40
-rw-r--r--lib/pleroma/application.ex5
-rw-r--r--lib/pleroma/application_requirements.ex107
-rw-r--r--lib/pleroma/chat.ex72
-rw-r--r--lib/pleroma/chat/message_reference.ex117
-rw-r--r--lib/pleroma/config/config_db.ex285
-rw-r--r--lib/pleroma/config/deprecation_warnings.ex69
-rw-r--r--lib/pleroma/config/transfer_task.ex12
-rw-r--r--lib/pleroma/conversation/participation.ex11
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex (renamed from lib/pleroma/web/activity_pub/object_validators/types/date_time.ex)6
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex (renamed from lib/pleroma/web/activity_pub/object_validators/types/object_id.ex)6
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex40
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex25
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex (renamed from lib/pleroma/web/activity_pub/object_validators/types/uri.ex)6
-rw-r--r--lib/pleroma/ecto_type/config/atom.ex26
-rw-r--r--lib/pleroma/ecto_type/config/binary_value.ex27
-rw-r--r--lib/pleroma/emoji/pack.ex54
-rw-r--r--lib/pleroma/migration_helper/notification_backfill.ex85
-rw-r--r--lib/pleroma/notification.ex129
-rw-r--r--lib/pleroma/pagination.ex6
-rw-r--r--lib/pleroma/plugs/http_security_plug.ex5
-rw-r--r--lib/pleroma/plugs/uploaded_media.ex16
-rw-r--r--lib/pleroma/repo.ex59
-rw-r--r--lib/pleroma/signature.ex4
-rw-r--r--lib/pleroma/upload.ex1
-rw-r--r--lib/pleroma/user.ex117
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex101
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex3
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex37
-rw-r--r--lib/pleroma/web/activity_pub/mrf.ex11
-rw-r--r--lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex43
-rw-r--r--lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex7
-rw-r--r--lib/pleroma/web/activity_pub/mrf/simple_policy.ex28
-rw-r--r--lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex51
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/announce_validator.ex14
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex80
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex123
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex91
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex (renamed from lib/pleroma/web/activity_pub/object_validators/create_validator.ex)6
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/delete_validator.ex17
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex8
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/like_validator.ex14
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/note_validator.ex13
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/types/recipients.ex34
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/undo_validator.ex8
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex24
-rw-r--r--lib/pleroma/web/activity_pub/pipeline.ex4
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex97
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex36
-rw-r--r--lib/pleroma/web/admin_api/controllers/admin_api_controller.ex26
-rw-r--r--lib/pleroma/web/admin_api/controllers/config_controller.ex34
-rw-r--r--lib/pleroma/web/admin_api/controllers/fallback_controller.ex6
-rw-r--r--lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex63
-rw-r--r--lib/pleroma/web/admin_api/views/account_view.ex3
-rw-r--r--lib/pleroma/web/admin_api/views/config_view.ex19
-rw-r--r--lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex11
-rw-r--r--lib/pleroma/web/api_spec/helpers.ex6
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex2
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex109
-rw-r--r--lib/pleroma/web/api_spec/operations/chat_operation.ex355
-rw-r--r--lib/pleroma/web/api_spec/operations/notification_operation.ex12
-rw-r--r--lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex30
-rw-r--r--lib/pleroma/web/api_spec/operations/status_operation.ex3
-rw-r--r--lib/pleroma/web/api_spec/operations/subscription_operation.ex5
-rw-r--r--lib/pleroma/web/api_spec/schemas/chat.ex75
-rw-r--r--lib/pleroma/web/api_spec/schemas/chat_message.ex41
-rw-r--r--lib/pleroma/web/common_api/activity_draft.ex9
-rw-r--r--lib/pleroma/web/common_api/common_api.ex70
-rw-r--r--lib/pleroma/web/common_api/utils.ex2
-rw-r--r--lib/pleroma/web/controller_helper.ex49
-rw-r--r--lib/pleroma/web/masto_fe_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex22
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex14
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex42
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex1
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api.ex15
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex18
-rw-r--r--lib/pleroma/web/mastodon_api/views/conversation_view.ex11
-rw-r--r--lib/pleroma/web/mastodon_api/views/instance_view.ex12
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex94
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex14
-rw-r--r--lib/pleroma/web/media_proxy/invalidation.ex26
-rw-r--r--lib/pleroma/web/media_proxy/invalidations/http.ex10
-rw-r--r--lib/pleroma/web/media_proxy/invalidations/script.ex36
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy.ex35
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy_controller.ex3
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/chat_controller.ex174
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex10
-rw-r--r--lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex45
-rw-r--r--lib/pleroma/web/pleroma_api/views/chat_view.ex33
-rw-r--r--lib/pleroma/web/push/impl.ex24
-rw-r--r--lib/pleroma/web/push/subscription.ex2
-rw-r--r--lib/pleroma/web/rich_media/helpers.ex6
-rw-r--r--lib/pleroma/web/rich_media/parser.ex16
-rw-r--r--lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex33
-rw-r--r--lib/pleroma/web/rich_media/parsers/oembed_parser.ex22
-rw-r--r--lib/pleroma/web/rich_media/parsers/ogp.ex11
-rw-r--r--lib/pleroma/web/rich_media/parsers/twitter_card.ex15
-rw-r--r--lib/pleroma/web/router.ex14
-rw-r--r--lib/pleroma/web/streamer/streamer.ex44
-rw-r--r--lib/pleroma/web/views/streamer_view.ex23
-rw-r--r--lib/pleroma/workers/attachments_cleanup_worker.ex134
106 files changed, 3236 insertions, 866 deletions
diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex
index 5c9ef6904..d5129d410 100644
--- a/lib/mix/tasks/pleroma/config.ex
+++ b/lib/mix/tasks/pleroma/config.ex
@@ -52,6 +52,7 @@ defmodule Mix.Tasks.Pleroma.Config do
defp do_migrate_to_db(config_file) do
if File.exists?(config_file) do
+ shell_info("Migrating settings from file: #{Path.expand(config_file)}")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;")
Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
@@ -72,8 +73,7 @@ defmodule Mix.Tasks.Pleroma.Config do
group
|> Pleroma.Config.Loader.filter_group(settings)
|> Enum.each(fn {key, value} ->
- key = inspect(key)
- {:ok, _} = ConfigDB.update_or_create(%{group: inspect(group), key: key, value: value})
+ {:ok, _} = ConfigDB.update_or_create(%{group: group, key: key, value: value})
shell_info("Settings for key #{key} migrated.")
end)
@@ -131,12 +131,9 @@ defmodule Mix.Tasks.Pleroma.Config do
end
defp write(config, file) do
- value =
- config.value
- |> ConfigDB.from_binary()
- |> inspect(limit: :infinity)
+ value = inspect(config.value, limit: :infinity)
- IO.write(file, "config #{config.group}, #{config.key}, #{value}\r\n\r\n")
+ IO.write(file, "config #{inspect(config.group)}, #{inspect(config.key)}, #{value}\r\n\r\n")
config
end
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
index 29a5fa99c..f4eaeac98 100644
--- a/lib/mix/tasks/pleroma/emoji.ex
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -237,6 +237,12 @@ defmodule Mix.Tasks.Pleroma.Emoji do
end
end
+ def run(["reload"]) do
+ start_pleroma()
+ Pleroma.Emoji.reload()
+ IO.puts("Emoji packs have been reloaded.")
+ end
+
defp fetch_and_decode(from) do
with {:ok, json} <- fetch(from) do
Jason.decode!(json)
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index 3635c02bc..bca7e87bf 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -144,6 +144,18 @@ defmodule Mix.Tasks.Pleroma.User do
end
end
+ def run(["reset_mfa", nickname]) do
+ start_pleroma()
+
+ with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
+ {:ok, _token} <- Pleroma.MFA.disable(user) do
+ shell_info("Multi-Factor Authentication disabled for #{user.nickname}")
+ else
+ _ ->
+ shell_error("No local user #{nickname}")
+ end
+ end
+
def run(["deactivate", nickname]) do
start_pleroma()
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 6213d0eb7..c3cea8d2a 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -24,16 +24,6 @@ defmodule Pleroma.Activity do
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
- # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
- @mastodon_notification_types %{
- "Create" => "mention",
- "Follow" => ["follow", "follow_request"],
- "Announce" => "reblog",
- "Like" => "favourite",
- "Move" => "move",
- "EmojiReact" => "pleroma:emoji_reaction"
- }
-
schema "activities" do
field(:data, :map)
field(:local, :boolean, default: true)
@@ -41,6 +31,10 @@ defmodule Pleroma.Activity do
field(:recipients, {:array, :string}, default: [])
field(:thread_muted?, :boolean, virtual: true)
+ # A field that can be used if you need to join some kind of other
+ # id to order / paginate this field by
+ field(:pagination_id, :string, virtual: true)
+
# This is a fake relation,
# do not use outside of with_preloaded_user_actor/with_joined_user_actor
has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)
@@ -300,32 +294,6 @@ defmodule Pleroma.Activity do
def follow_accepted?(_), do: false
- @spec mastodon_notification_type(Activity.t()) :: String.t() | nil
-
- for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
- def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
- do: unquote(type)
- end
-
- def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
- if follow_accepted?(activity) do
- "follow"
- else
- "follow_request"
- end
- end
-
- def mastodon_notification_type(%Activity{}), do: nil
-
- @spec from_mastodon_notification_type(String.t()) :: String.t() | nil
- @doc "Converts Mastodon notification type to AR activity type"
- def from_mastodon_notification_type(type) do
- with {k, _v} <-
- Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do
- k
- end
- end
-
def all_by_actor_and_id(actor, status_ids \\ [])
def all_by_actor_and_id(_actor, []), do: []
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 9d3d92b38..9615af122 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -39,7 +39,7 @@ defmodule Pleroma.Application do
Pleroma.HTML.compile_scrubbers()
Config.DeprecationWarnings.warn()
Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
- Pleroma.Repo.check_migrations_applied!()
+ Pleroma.ApplicationRequirements.verify!()
setup_instrumenters()
load_custom_modules()
@@ -148,7 +148,8 @@ defmodule Pleroma.Application do
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
- build_cachex("failed_proxy_url", limit: 2500)
+ build_cachex("failed_proxy_url", limit: 2500),
+ build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000)
]
end
diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex
new file mode 100644
index 000000000..88575a498
--- /dev/null
+++ b/lib/pleroma/application_requirements.ex
@@ -0,0 +1,107 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ApplicationRequirements do
+ @moduledoc """
+ The module represents the collection of validations to runs before start server.
+ """
+
+ defmodule VerifyError, do: defexception([:message])
+
+ import Ecto.Query
+
+ require Logger
+
+ @spec verify!() :: :ok | VerifyError.t()
+ def verify! do
+ :ok
+ |> check_migrations_applied!()
+ |> check_rum!()
+ |> handle_result()
+ end
+
+ defp handle_result(:ok), do: :ok
+ defp handle_result({:error, message}), do: raise(VerifyError, message: message)
+
+ # Checks for pending migrations.
+ #
+ def check_migrations_applied!(:ok) do
+ unless Pleroma.Config.get(
+ [:i_am_aware_this_may_cause_data_loss, :disable_migration_check],
+ false
+ ) do
+ {_, res, _} =
+ Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
+ down_migrations =
+ Ecto.Migrator.migrations(repo)
+ |> Enum.reject(fn
+ {:up, _, _} -> true
+ {:down, _, _} -> false
+ end)
+
+ if length(down_migrations) > 0 do
+ down_migrations_text =
+ Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end)
+
+ Logger.error(
+ "The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true"
+ )
+
+ {:error, "Unapplied Migrations detected"}
+ else
+ :ok
+ end
+ end)
+
+ res
+ else
+ :ok
+ end
+ end
+
+ def check_migrations_applied!(result), do: result
+
+ # Checks for settings of RUM indexes.
+ #
+ defp check_rum!(:ok) do
+ {_, res, _} =
+ Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
+ migrate =
+ from(o in "columns",
+ where: o.table_name == "objects",
+ where: o.column_name == "fts_content"
+ )
+ |> repo.exists?(prefix: "information_schema")
+
+ setting = Pleroma.Config.get([:database, :rum_enabled], false)
+
+ do_check_rum!(setting, migrate)
+ end)
+
+ res
+ end
+
+ defp check_rum!(result), do: result
+
+ defp do_check_rum!(setting, migrate) do
+ case {setting, migrate} do
+ {true, false} ->
+ Logger.error(
+ "Use `RUM` index is enabled, but were not applied migrations for it.\nIf you want to start Pleroma anyway, set\nconfig :pleroma, :database, rum_enabled: false\nOtherwise apply the following migrations:\n`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`"
+ )
+
+ {:error, "Unapplied RUM Migrations detected"}
+
+ {false, true} ->
+ Logger.error(
+ "Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\nIf you want to use `RUM`, set\nconfig :pleroma, :database, rum_enabled: true\nOtherwise roll `RUM` migrations back.\n`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`"
+ )
+
+ {:error, "RUM Migrations detected"}
+
+ _ ->
+ :ok
+ end
+ end
+end
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
new file mode 100644
index 000000000..24a86371e
--- /dev/null
+++ b/lib/pleroma/chat.ex
@@ -0,0 +1,72 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Chat do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ @moduledoc """
+ Chat keeps a reference to ChatMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet).
+
+ It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages.
+ """
+
+ @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
+
+ schema "chats" do
+ belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+ field(:recipient, :string)
+
+ timestamps()
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, [:user_id, :recipient])
+ |> validate_change(:recipient, fn
+ :recipient, recipient ->
+ case User.get_cached_by_ap_id(recipient) do
+ nil -> [recipient: "must be an existing user"]
+ _ -> []
+ end
+ end)
+ |> validate_required([:user_id, :recipient])
+ |> unique_constraint(:user_id, name: :chats_user_id_recipient_index)
+ end
+
+ def get_by_id(id) do
+ __MODULE__
+ |> Repo.get(id)
+ end
+
+ def get(user_id, recipient) do
+ __MODULE__
+ |> Repo.get_by(user_id: user_id, recipient: recipient)
+ end
+
+ def get_or_create(user_id, recipient) do
+ %__MODULE__{}
+ |> changeset(%{user_id: user_id, recipient: recipient})
+ |> Repo.insert(
+ # Need to set something, otherwise we get nothing back at all
+ on_conflict: [set: [recipient: recipient]],
+ returning: true,
+ conflict_target: [:user_id, :recipient]
+ )
+ end
+
+ def bump_or_create(user_id, recipient) do
+ %__MODULE__{}
+ |> changeset(%{user_id: user_id, recipient: recipient})
+ |> Repo.insert(
+ on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]],
+ returning: true,
+ conflict_target: [:user_id, :recipient]
+ )
+ end
+end
diff --git a/lib/pleroma/chat/message_reference.ex b/lib/pleroma/chat/message_reference.ex
new file mode 100644
index 000000000..131ae0186
--- /dev/null
+++ b/lib/pleroma/chat/message_reference.ex
@@ -0,0 +1,117 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Chat.MessageReference do
+ @moduledoc """
+ A reference that builds a relation between an AP chat message that a user can see and whether it has been seen
+ by them, or should be displayed to them. Used to build the chat view that is presented to the user.
+ """
+
+ use Ecto.Schema
+
+ alias Pleroma.Chat
+ alias Pleroma.Object
+ alias Pleroma.Repo
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ @primary_key {:id, FlakeId.Ecto.Type, autogenerate: true}
+
+ schema "chat_message_references" do
+ belongs_to(:object, Object)
+ belongs_to(:chat, Chat, type: FlakeId.Ecto.CompatType)
+
+ field(:unread, :boolean, default: true)
+
+ timestamps()
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, [:object_id, :chat_id, :unread])
+ |> validate_required([:object_id, :chat_id, :unread])
+ end
+
+ def get_by_id(id) do
+ __MODULE__
+ |> Repo.get(id)
+ |> Repo.preload(:object)
+ end
+
+ def delete(cm_ref) do
+ cm_ref
+ |> Repo.delete()
+ end
+
+ def delete_for_object(%{id: object_id}) do
+ from(cr in __MODULE__,
+ where: cr.object_id == ^object_id
+ )
+ |> Repo.delete_all()
+ end
+
+ def for_chat_and_object(%{id: chat_id}, %{id: object_id}) do
+ __MODULE__
+ |> Repo.get_by(chat_id: chat_id, object_id: object_id)
+ |> Repo.preload(:object)
+ end
+
+ def for_chat_query(chat) do
+ from(cr in __MODULE__,
+ where: cr.chat_id == ^chat.id,
+ order_by: [desc: :id],
+ preload: [:object]
+ )
+ end
+
+ def last_message_for_chat(chat) do
+ chat
+ |> for_chat_query()
+ |> limit(1)
+ |> Repo.one()
+ end
+
+ def create(chat, object, unread) do
+ params = %{
+ chat_id: chat.id,
+ object_id: object.id,
+ unread: unread
+ }
+
+ %__MODULE__{}
+ |> changeset(params)
+ |> Repo.insert()
+ end
+
+ def unread_count_for_chat(chat) do
+ chat
+ |> for_chat_query()
+ |> where([cmr], cmr.unread == true)
+ |> Repo.aggregate(:count)
+ end
+
+ def mark_as_read(cm_ref) do
+ cm_ref
+ |> changeset(%{unread: false})
+ |> Repo.update()
+ end
+
+ def set_all_seen_for_chat(chat, last_read_id \\ nil) do
+ query =
+ chat
+ |> for_chat_query()
+ |> exclude(:order_by)
+ |> exclude(:preload)
+ |> where([cmr], cmr.unread == true)
+
+ if last_read_id do
+ query
+ |> where([cmr], cmr.id <= ^last_read_id)
+ else
+ query
+ end
+ |> Repo.update_all(set: [unread: false])
+ end
+end
diff --git a/lib/pleroma/config/config_db.ex b/lib/pleroma/config/config_db.ex
index 2b43d4c36..1a89d8895 100644
--- a/lib/pleroma/config/config_db.ex
+++ b/lib/pleroma/config/config_db.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.ConfigDB do
use Ecto.Schema
import Ecto.Changeset
- import Ecto.Query
+ import Ecto.Query, only: [select: 3]
import Pleroma.Web.Gettext
alias __MODULE__
@@ -14,16 +14,6 @@ defmodule Pleroma.ConfigDB do
@type t :: %__MODULE__{}
- @full_key_update [
- {:pleroma, :ecto_repos},
- {:quack, :meta},
- {:mime, :types},
- {:cors_plug, [:max_age, :methods, :expose, :headers]},
- {:auto_linker, :opts},
- {:swarm, :node_blacklist},
- {:logger, :backends}
- ]
-
@full_subkey_update [
{:pleroma, :assets, :mascots},
{:pleroma, :emoji, :groups},
@@ -32,14 +22,10 @@ defmodule Pleroma.ConfigDB do
{:pleroma, :mrf_keyword, :replace}
]
- @regex ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
-
- @delimiters ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
-
schema "config" do
- field(:key, :string)
- field(:group, :string)
- field(:value, :binary)
+ field(:key, Pleroma.EctoType.Config.Atom)
+ field(:group, Pleroma.EctoType.Config.Atom)
+ field(:value, Pleroma.EctoType.Config.BinaryValue)
field(:db, {:array, :string}, virtual: true, default: [])
timestamps()
@@ -51,10 +37,6 @@ defmodule Pleroma.ConfigDB do
|> select([c], {c.group, c.key, c.value})
|> Repo.all()
|> Enum.reduce([], fn {group, key, value}, acc ->
- group = ConfigDB.from_string(group)
- key = ConfigDB.from_string(key)
- value = from_binary(value)
-
Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}]))
end)
end
@@ -64,50 +46,41 @@ defmodule Pleroma.ConfigDB do
@spec changeset(ConfigDB.t(), map()) :: Changeset.t()
def changeset(config, params \\ %{}) do
- params = Map.put(params, :value, transform(params[:value]))
-
config
|> cast(params, [:key, :group, :value])
|> validate_required([:key, :group, :value])
|> unique_constraint(:key, name: :config_group_key_index)
end
- @spec create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
- def create(params) do
+ defp create(params) do
%ConfigDB{}
|> changeset(params)
|> Repo.insert()
end
- @spec update(ConfigDB.t(), map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
- def update(%ConfigDB{} = config, %{value: value}) do
+ defp update(%ConfigDB{} = config, %{value: value}) do
config
|> changeset(%{value: value})
|> Repo.update()
end
- @spec get_db_keys(ConfigDB.t()) :: [String.t()]
- def get_db_keys(%ConfigDB{} = config) do
- config.value
- |> ConfigDB.from_binary()
- |> get_db_keys(config.key)
- end
-
@spec get_db_keys(keyword(), any()) :: [String.t()]
def get_db_keys(value, key) do
- if Keyword.keyword?(value) do
- value |> Keyword.keys() |> Enum.map(&convert(&1))
- else
- [convert(key)]
- end
+ keys =
+ if Keyword.keyword?(value) do
+ Keyword.keys(value)
+ else
+ [key]
+ end
+
+ Enum.map(keys, &to_json_types(&1))
end
@spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword()
def merge_group(group, key, old_value, new_value) do
- new_keys = to_map_set(new_value)
+ new_keys = to_mapset(new_value)
- intersect_keys =
- old_value |> to_map_set() |> MapSet.intersection(new_keys) |> MapSet.to_list()
+ intersect_keys = old_value |> to_mapset() |> MapSet.intersection(new_keys) |> MapSet.to_list()
merged_value = ConfigDB.merge(old_value, new_value)
@@ -120,12 +93,10 @@ defmodule Pleroma.ConfigDB do
[]
end)
|> List.flatten()
- |> Enum.reduce(merged_value, fn subkey, acc ->
- Keyword.put(acc, subkey, new_value[subkey])
- end)
+ |> Enum.reduce(merged_value, &Keyword.put(&2, &1, new_value[&1]))
end
- defp to_map_set(keyword) do
+ defp to_mapset(keyword) do
keyword
|> Keyword.keys()
|> MapSet.new()
@@ -159,57 +130,55 @@ defmodule Pleroma.ConfigDB do
@spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def update_or_create(params) do
+ params = Map.put(params, :value, to_elixir_types(params[:value]))
search_opts = Map.take(params, [:group, :key])
with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
- {:partial_update, true, config} <-
- {:partial_update, can_be_partially_updated?(config), config},
- old_value <- from_binary(config.value),
- transformed_value <- do_transform(params[:value]),
- {:can_be_merged, true, config} <- {:can_be_merged, is_list(transformed_value), config},
- new_value <-
- merge_group(
- ConfigDB.from_string(config.group),
- ConfigDB.from_string(config.key),
- old_value,
- transformed_value
- ) do
- ConfigDB.update(config, %{value: new_value})
+ {_, true, config} <- {:partial_update, can_be_partially_updated?(config), config},
+ {_, true, config} <-
+ {:can_be_merged, is_list(params[:value]) and is_list(config.value), config} do
+ new_value = merge_group(config.group, config.key, config.value, params[:value])
+ update(config, %{value: new_value})
else
{reason, false, config} when reason in [:partial_update, :can_be_merged] ->
- ConfigDB.update(config, params)
+ update(config, params)
nil ->
- ConfigDB.create(params)
+ create(params)
end
end
defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config)
- defp only_full_update?(%ConfigDB{} = config) do
- config_group = ConfigDB.from_string(config.group)
- config_key = ConfigDB.from_string(config.key)
-
- Enum.any?(@full_key_update, fn
- {group, key} when is_list(key) ->
- config_group == group and config_key in key
-
- {group, key} ->
- config_group == group and config_key == key
+ defp only_full_update?(%ConfigDB{group: group, key: key}) do
+ full_key_update = [
+ {:pleroma, :ecto_repos},
+ {:quack, :meta},
+ {:mime, :types},
+ {:cors_plug, [:max_age, :methods, :expose, :headers]},
+ {:auto_linker, :opts},
+ {:swarm, :node_blacklist},
+ {:logger, :backends}
+ ]
+
+ Enum.any?(full_key_update, fn
+ {s_group, s_key} ->
+ group == s_group and ((is_list(s_key) and key in s_key) or key == s_key)
end)
end
- @spec delete(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
+ @spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
+ def delete(%ConfigDB{} = config), do: Repo.delete(config)
+
def delete(params) do
search_opts = Map.delete(params, :subkeys)
with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
{config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]},
- old_value <- from_binary(config.value),
- keys <- Enum.map(sub_keys, &do_transform_string(&1)),
- {:partial_remove, config, new_value} when new_value != [] <-
- {:partial_remove, config, Keyword.drop(old_value, keys)} do
- ConfigDB.update(config, %{value: new_value})
+ keys <- Enum.map(sub_keys, &string_to_elixir_types(&1)),
+ {_, config, new_value} when new_value != [] <-
+ {:partial_remove, config, Keyword.drop(config.value, keys)} do
+ update(config, %{value: new_value})
else
{:partial_remove, config, []} ->
Repo.delete(config)
@@ -225,37 +194,32 @@ defmodule Pleroma.ConfigDB do
end
end
- @spec from_binary(binary()) :: term()
- def from_binary(binary), do: :erlang.binary_to_term(binary)
-
- @spec from_binary_with_convert(binary()) :: any()
- def from_binary_with_convert(binary) do
- binary
- |> from_binary()
- |> do_convert()
+ @spec to_json_types(term()) :: map() | list() | boolean() | String.t()
+ def to_json_types(entity) when is_list(entity) do
+ Enum.map(entity, &to_json_types/1)
end
- @spec from_string(String.t()) :: atom() | no_return()
- def from_string(string), do: do_transform_string(string)
+ def to_json_types(%Regex{} = entity), do: inspect(entity)
- @spec convert(any()) :: any()
- def convert(entity), do: do_convert(entity)
-
- defp do_convert(entity) when is_list(entity) do
- for v <- entity, into: [], do: do_convert(v)
+ def to_json_types(entity) when is_map(entity) do
+ Map.new(entity, fn {k, v} -> {to_json_types(k), to_json_types(v)} end)
end
- defp do_convert(%Regex{} = entity), do: inspect(entity)
+ def to_json_types({:args, args}) when is_list(args) do
+ arguments =
+ Enum.map(args, fn
+ arg when is_tuple(arg) -> inspect(arg)
+ arg -> to_json_types(arg)
+ end)
- defp do_convert(entity) when is_map(entity) do
- for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)}
+ %{"tuple" => [":args", arguments]}
end
- defp do_convert({:proxy_url, {type, :localhost, port}}) do
- %{"tuple" => [":proxy_url", %{"tuple" => [do_convert(type), "localhost", port]}]}
+ def to_json_types({:proxy_url, {type, :localhost, port}}) do
+ %{"tuple" => [":proxy_url", %{"tuple" => [to_json_types(type), "localhost", port]}]}
end
- defp do_convert({:proxy_url, {type, host, port}}) when is_tuple(host) do
+ def to_json_types({:proxy_url, {type, host, port}}) when is_tuple(host) do
ip =
host
|> :inet_parse.ntoa()
@@ -264,66 +228,64 @@ defmodule Pleroma.ConfigDB do
%{
"tuple" => [
":proxy_url",
- %{"tuple" => [do_convert(type), ip, port]}
+ %{"tuple" => [to_json_types(type), ip, port]}
]
}
end
- defp do_convert({:proxy_url, {type, host, port}}) do
+ def to_json_types({:proxy_url, {type, host, port}}) do
%{
"tuple" => [
":proxy_url",
- %{"tuple" => [do_convert(type), to_string(host), port]}
+ %{"tuple" => [to_json_types(type), to_string(host), port]}
]
}
end
- defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]}
+ def to_json_types({:partial_chain, entity}),
+ do: %{"tuple" => [":partial_chain", inspect(entity)]}
- defp do_convert(entity) when is_tuple(entity) do
+ def to_json_types(entity) when is_tuple(entity) do
value =
entity
|> Tuple.to_list()
- |> do_convert()
+ |> to_json_types()
%{"tuple" => value}
end
- defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
+ def to_json_types(entity) when is_binary(entity), do: entity
+
+ def to_json_types(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
entity
end
- defp do_convert(entity)
- when is_atom(entity) and entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
+ def to_json_types(entity) when entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
":#{entity}"
end
- defp do_convert(entity) when is_atom(entity), do: inspect(entity)
+ def to_json_types(entity) when is_atom(entity), do: inspect(entity)
- defp do_convert(entity) when is_binary(entity), do: entity
+ @spec to_elixir_types(boolean() | String.t() | map() | list()) :: term()
+ def to_elixir_types(%{"tuple" => [":args", args]}) when is_list(args) do
+ arguments =
+ Enum.map(args, fn arg ->
+ if String.contains?(arg, ["{", "}"]) do
+ {elem, []} = Code.eval_string(arg)
+ elem
+ else
+ to_elixir_types(arg)
+ end
+ end)
- @spec transform(any()) :: binary() | no_return()
- def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do
- entity
- |> do_transform()
- |> to_binary()
+ {:args, arguments}
end
- def transform(entity), do: to_binary(entity)
-
- @spec transform_with_out_binary(any()) :: any()
- def transform_with_out_binary(entity), do: do_transform(entity)
-
- @spec to_binary(any()) :: binary()
- def to_binary(entity), do: :erlang.term_to_binary(entity)
-
- defp do_transform(%Regex{} = entity), do: entity
-
- defp do_transform(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
- {:proxy_url, {do_transform_string(type), parse_host(host), port}}
+ def to_elixir_types(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
+ {:proxy_url, {string_to_elixir_types(type), parse_host(host), port}}
end
- defp do_transform(%{"tuple" => [":partial_chain", entity]}) do
+ def to_elixir_types(%{"tuple" => [":partial_chain", entity]}) do
{partial_chain, []} =
entity
|> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
@@ -332,25 +294,51 @@ defmodule Pleroma.ConfigDB do
{:partial_chain, partial_chain}
end
- defp do_transform(%{"tuple" => entity}) do
- Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
+ def to_elixir_types(%{"tuple" => entity}) do
+ Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1)))
end
- defp do_transform(entity) when is_map(entity) do
- for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)}
+ def to_elixir_types(entity) when is_map(entity) do
+ Map.new(entity, fn {k, v} -> {to_elixir_types(k), to_elixir_types(v)} end)
end
- defp do_transform(entity) when is_list(entity) do
- for v <- entity, into: [], do: do_transform(v)
+ def to_elixir_types(entity) when is_list(entity) do
+ Enum.map(entity, &to_elixir_types/1)
end
- defp do_transform(entity) when is_binary(entity) do
+ def to_elixir_types(entity) when is_binary(entity) do
entity
|> String.trim()
- |> do_transform_string()
+ |> string_to_elixir_types()
end
- defp do_transform(entity), do: entity
+ def to_elixir_types(entity), do: entity
+
+ @spec string_to_elixir_types(String.t()) ::
+ atom() | Regex.t() | module() | String.t() | no_return()
+ def string_to_elixir_types("~r" <> _pattern = regex) do
+ pattern =
+ ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
+
+ delimiters = ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
+
+ with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
+ Regex.named_captures(pattern, regex),
+ {:ok, {leading, closing}} <- find_valid_delimiter(delimiters, pattern, regex_delimiter),
+ {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
+ result
+ end
+ end
+
+ def string_to_elixir_types(":" <> atom), do: String.to_atom(atom)
+
+ def string_to_elixir_types(value) do
+ if module_name?(value) do
+ String.to_existing_atom("Elixir." <> value)
+ else
+ value
+ end
+ end
defp parse_host("localhost"), do: :localhost
@@ -387,27 +375,8 @@ defmodule Pleroma.ConfigDB do
end
end
- defp do_transform_string("~r" <> _pattern = regex) do
- with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
- Regex.named_captures(@regex, regex),
- {:ok, {leading, closing}} <- find_valid_delimiter(@delimiters, pattern, regex_delimiter),
- {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
- result
- end
- end
-
- defp do_transform_string(":" <> atom), do: String.to_atom(atom)
-
- defp do_transform_string(value) do
- if is_module_name?(value) do
- String.to_existing_atom("Elixir." <> value)
- else
- value
- end
- end
-
- @spec is_module_name?(String.t()) :: boolean()
- def is_module_name?(string) do
+ @spec module_name?(String.t()) :: boolean()
+ def module_name?(string) do
Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or
string in ["Oban", "Ueberauth", "ExSyslogger"]
end
diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
index c39a8984b..0a6c724fb 100644
--- a/lib/pleroma/config/deprecation_warnings.ex
+++ b/lib/pleroma/config/deprecation_warnings.ex
@@ -3,10 +3,25 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.DeprecationWarnings do
+ alias Pleroma.Config
+
require Logger
+ alias Pleroma.Config
+
+ @type config_namespace() :: [atom()]
+ @type config_map() :: {config_namespace(), config_namespace(), String.t()}
+
+ @mrf_config_map [
+ {[:instance, :rewrite_policy], [:mrf, :policies],
+ "\n* `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies`"},
+ {[:instance, :mrf_transparency], [:mrf, :transparency],
+ "\n* `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency`"},
+ {[:instance, :mrf_transparency_exclusions], [:mrf, :transparency_exclusions],
+ "\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"}
+ ]
def check_hellthread_threshold do
- if Pleroma.Config.get([:mrf_hellthread, :threshold]) do
+ if Config.get([:mrf_hellthread, :threshold]) do
Logger.warn("""
!!!DEPRECATION WARNING!!!
You are using the old configuration mechanism for the hellthread filter. Please check config.md.
@@ -14,7 +29,59 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end
+ def mrf_user_allowlist do
+ config = Config.get(:mrf_user_allowlist)
+
+ if config && Enum.any?(config, fn {k, _} -> is_atom(k) end) do
+ rewritten =
+ Enum.reduce(Config.get(:mrf_user_allowlist), Map.new(), fn {k, v}, acc ->
+ Map.put(acc, to_string(k), v)
+ end)
+
+ Config.put(:mrf_user_allowlist, rewritten)
+
+ Logger.error("""
+ !!!DEPRECATION WARNING!!!
+ As of Pleroma 2.0.7, the `mrf_user_allowlist` setting changed of format.
+ Pleroma 2.1 will remove support for the old format. Please change your configuration to match this:
+
+ config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)}
+ """)
+ end
+ end
+
def warn do
check_hellthread_threshold()
+ mrf_user_allowlist()
+ check_old_mrf_config()
+ end
+
+ def check_old_mrf_config do
+ warning_preface = """
+ !!!DEPRECATION WARNING!!!
+ Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later:
+ """
+
+ move_namespace_and_warn(@mrf_config_map, warning_preface)
+ end
+
+ @spec move_namespace_and_warn([config_map()], String.t()) :: :ok
+ def move_namespace_and_warn(config_map, warning_preface) do
+ warning =
+ Enum.reduce(config_map, "", fn
+ {old, new, err_msg}, acc ->
+ old_config = Config.get(old)
+
+ if old_config do
+ Config.put(new, old_config)
+ acc <> err_msg
+ else
+ acc
+ end
+ end)
+
+ if warning != "" do
+ Logger.warn(warning_preface <> warning)
+ end
end
end
diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex
index c02b70e96..eb86b8ff4 100644
--- a/lib/pleroma/config/transfer_task.ex
+++ b/lib/pleroma/config/transfer_task.ex
@@ -28,10 +28,6 @@ defmodule Pleroma.Config.TransferTask do
{:pleroma, Pleroma.Captcha, [:seconds_valid]},
{:pleroma, Pleroma.Upload, [:proxy_remote]},
{:pleroma, :instance, [:upload_limit]},
- {:pleroma, :email_notifications, [:digest]},
- {:pleroma, :oauth2, [:clean_expired_tokens]},
- {:pleroma, Pleroma.ActivityExpiration, [:enabled]},
- {:pleroma, Pleroma.ScheduledActivity, [:enabled]},
{:pleroma, :gopher, [:enabled]}
]
@@ -48,7 +44,7 @@ defmodule Pleroma.Config.TransferTask do
{logger, other} =
(Repo.all(ConfigDB) ++ deleted_settings)
- |> Enum.map(&transform_and_merge/1)
+ |> Enum.map(&merge_with_default/1)
|> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end)
logger
@@ -92,11 +88,7 @@ defmodule Pleroma.Config.TransferTask do
end
end
- defp transform_and_merge(%{group: group, key: key, value: value} = setting) do
- group = ConfigDB.from_string(group)
- key = ConfigDB.from_string(key)
- value = ConfigDB.from_binary(value)
-
+ defp merge_with_default(%{group: group, key: key, value: value} = setting) do
default = Config.Holder.default_config(group, key)
merged =
diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index ce7bd2396..8bc3e85d6 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -162,10 +162,13 @@ defmodule Pleroma.Conversation.Participation do
for_user(user, params)
|> Enum.map(fn participation ->
activity_id =
- ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
- user: user,
- blocking_user: user
- })
+ ActivityPub.fetch_latest_direct_activity_id_for_context(
+ participation.conversation.ap_id,
+ %{
+ user: user,
+ blocking_user: user
+ }
+ )
%{
participation
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex
index 4f412fcde..d852c0abd 100644
--- a/lib/pleroma/web/activity_pub/object_validators/types/date_time.ex
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/date_time.ex
@@ -1,4 +1,8 @@
-defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTime do
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime do
@moduledoc """
The AP standard defines the date fields in AP as xsd:DateTime. Elixir's
DateTime can't parse this, but it can parse the related iso8601. This
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex
index f71f76370..8034235b0 100644
--- a/lib/pleroma/web/activity_pub/object_validators/types/object_id.ex
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/object_id.ex
@@ -1,4 +1,8 @@
-defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID do
use Ecto.Type
def type, do: :string
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex
new file mode 100644
index 000000000..205527a96
--- /dev/null
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Recipients do
+ use Ecto.Type
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID
+
+ def type, do: {:array, ObjectID}
+
+ def cast(object) when is_binary(object) do
+ cast([object])
+ end
+
+ def cast(data) when is_list(data) do
+ data
+ |> Enum.reduce_while({:ok, []}, fn element, {:ok, list} ->
+ case ObjectID.cast(element) do
+ {:ok, id} ->
+ {:cont, {:ok, [id | list]}}
+
+ _ ->
+ {:halt, :error}
+ end
+ end)
+ end
+
+ def cast(_) do
+ :error
+ end
+
+ def dump(data) do
+ {:ok, data}
+ end
+
+ def load(data) do
+ {:ok, data}
+ end
+end
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex
new file mode 100644
index 000000000..7f0405c7b
--- /dev/null
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/safe_text.ex
@@ -0,0 +1,25 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.SafeText do
+ use Ecto.Type
+
+ alias Pleroma.HTML
+
+ def type, do: :string
+
+ def cast(str) when is_binary(str) do
+ {:ok, HTML.filter_tags(str)}
+ end
+
+ def cast(_), do: :error
+
+ def dump(data) do
+ {:ok, data}
+ end
+
+ def load(data) do
+ {:ok, data}
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/uri.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex
index 24845bcc0..2054c26be 100644
--- a/lib/pleroma/web/activity_pub/object_validators/types/uri.ex
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/uri.ex
@@ -1,4 +1,8 @@
-defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Uri do
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Uri do
use Ecto.Type
def type, do: :string
diff --git a/lib/pleroma/ecto_type/config/atom.ex b/lib/pleroma/ecto_type/config/atom.ex
new file mode 100644
index 000000000..df565d432
--- /dev/null
+++ b/lib/pleroma/ecto_type/config/atom.ex
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.Config.Atom do
+ use Ecto.Type
+
+ def type, do: :atom
+
+ def cast(key) when is_atom(key) do
+ {:ok, key}
+ end
+
+ def cast(key) when is_binary(key) do
+ {:ok, Pleroma.ConfigDB.string_to_elixir_types(key)}
+ end
+
+ def cast(_), do: :error
+
+ def load(key) do
+ {:ok, Pleroma.ConfigDB.string_to_elixir_types(key)}
+ end
+
+ def dump(key) when is_atom(key), do: {:ok, inspect(key)}
+ def dump(_), do: :error
+end
diff --git a/lib/pleroma/ecto_type/config/binary_value.ex b/lib/pleroma/ecto_type/config/binary_value.ex
new file mode 100644
index 000000000..bbd2608c5
--- /dev/null
+++ b/lib/pleroma/ecto_type/config/binary_value.ex
@@ -0,0 +1,27 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.Config.BinaryValue do
+ use Ecto.Type
+
+ def type, do: :term
+
+ def cast(value) when is_binary(value) do
+ if String.valid?(value) do
+ {:ok, value}
+ else
+ {:ok, :erlang.binary_to_term(value)}
+ end
+ end
+
+ def cast(value), do: {:ok, value}
+
+ def load(value) when is_binary(value) do
+ {:ok, :erlang.binary_to_term(value)}
+ end
+
+ def dump(value) do
+ {:ok, :erlang.term_to_binary(value)}
+ end
+end
diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
index 14a5185be..787ff8141 100644
--- a/lib/pleroma/emoji/pack.ex
+++ b/lib/pleroma/emoji/pack.ex
@@ -1,6 +1,7 @@
defmodule Pleroma.Emoji.Pack do
- @derive {Jason.Encoder, only: [:files, :pack]}
+ @derive {Jason.Encoder, only: [:files, :pack, :files_count]}
defstruct files: %{},
+ files_count: 0,
pack_file: nil,
path: nil,
pack: %{},
@@ -8,6 +9,7 @@ defmodule Pleroma.Emoji.Pack do
@type t() :: %__MODULE__{
files: %{String.t() => Path.t()},
+ files_count: non_neg_integer(),
pack_file: Path.t(),
path: Path.t(),
pack: map(),
@@ -16,7 +18,7 @@ defmodule Pleroma.Emoji.Pack do
alias Pleroma.Emoji
- @spec create(String.t()) :: :ok | {:error, File.posix()} | {:error, :empty_values}
+ @spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
def create(name) do
with :ok <- validate_not_empty([name]),
dir <- Path.join(emoji_path(), name),
@@ -26,10 +28,27 @@ defmodule Pleroma.Emoji.Pack do
end
end
- @spec show(String.t()) :: {:ok, t()} | {:error, atom()}
- def show(name) do
+ defp paginate(entities, 1, page_size), do: Enum.take(entities, page_size)
+
+ defp paginate(entities, page, page_size) do
+ entities
+ |> Enum.chunk_every(page_size)
+ |> Enum.at(page - 1)
+ end
+
+ @spec show(keyword()) :: {:ok, t()} | {:error, atom()}
+ def show(opts) do
+ name = opts[:name]
+
with :ok <- validate_not_empty([name]),
{:ok, pack} <- load_pack(name) do
+ shortcodes =
+ pack.files
+ |> Map.keys()
+ |> paginate(opts[:page], opts[:page_size])
+
+ pack = Map.put(pack, :files, Map.take(pack.files, shortcodes))
+
{:ok, validate_pack(pack)}
end
end
@@ -120,10 +139,10 @@ defmodule Pleroma.Emoji.Pack do
end
end
- @spec list_local() :: {:ok, map()}
- def list_local do
+ @spec list_local(keyword()) :: {:ok, map(), non_neg_integer()}
+ def list_local(opts) do
with {:ok, results} <- list_packs_dir() do
- packs =
+ all_packs =
results
|> Enum.map(fn name ->
case load_pack(name) do
@@ -132,9 +151,13 @@ defmodule Pleroma.Emoji.Pack do
end
end)
|> Enum.reject(&is_nil/1)
+
+ packs =
+ all_packs
+ |> paginate(opts[:page], opts[:page_size])
|> Map.new(fn pack -> {pack.name, validate_pack(pack)} end)
- {:ok, packs}
+ {:ok, packs, length(all_packs)}
end
end
@@ -146,7 +169,7 @@ defmodule Pleroma.Emoji.Pack do
end
end
- @spec download(String.t(), String.t(), String.t()) :: :ok | {:error, atom()}
+ @spec download(String.t(), String.t(), String.t()) :: {:ok, t()} | {:error, atom()}
def download(name, url, as) do
uri = url |> String.trim() |> URI.parse()
@@ -197,7 +220,12 @@ defmodule Pleroma.Emoji.Pack do
|> Map.put(:path, Path.dirname(pack_file))
|> Map.put(:name, name)
- {:ok, pack}
+ files_count =
+ pack.files
+ |> Map.keys()
+ |> length()
+
+ {:ok, Map.put(pack, :files_count, files_count)}
else
{:error, :not_found}
end
@@ -296,7 +324,9 @@ defmodule Pleroma.Emoji.Pack do
# Otherwise, they'd have to download it from external-src
pack.pack["share-files"] &&
Enum.all?(pack.files, fn {_, file} ->
- File.exists?(Path.join(pack.path, file))
+ pack.path
+ |> Path.join(file)
+ |> File.exists?()
end)
end
@@ -440,7 +470,7 @@ defmodule Pleroma.Emoji.Pack do
# with the API so it should be sufficient
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
- {:ok, results}
+ {:ok, Enum.sort(results)}
else
{:create_dir, {:error, e}} -> {:error, :create_dir, e}
{:ls, {:error, e}} -> {:error, :ls, e}
diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex
new file mode 100644
index 000000000..b3770307a
--- /dev/null
+++ b/lib/pleroma/migration_helper/notification_backfill.ex
@@ -0,0 +1,85 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MigrationHelper.NotificationBackfill do
+ alias Pleroma.Notification
+ alias Pleroma.Object
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ import Ecto.Query
+
+ def fill_in_notification_types do
+ query =
+ from(n in Pleroma.Notification,
+ where: is_nil(n.type),
+ preload: :activity
+ )
+
+ query
+ |> Repo.chunk_stream(100)
+ |> Enum.each(fn notification ->
+ type =
+ notification.activity
+ |> type_from_activity()
+
+ notification
+ |> Notification.changeset(%{type: type})
+ |> Repo.update()
+ end)
+ end
+
+ # This is copied over from Notifications to keep this stable.
+ defp type_from_activity(%{data: %{"type" => type}} = activity) do
+ case type do
+ "Follow" ->
+ accepted_function = fn activity ->
+ with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]),
+ %User{} = followed <- User.get_by_ap_id(activity.data["object"]) do
+ Pleroma.FollowingRelationship.following?(follower, followed)
+ end
+ end
+
+ if accepted_function.(activity) do
+ "follow"
+ else
+ "follow_request"
+ end
+
+ "Announce" ->
+ "reblog"
+
+ "Like" ->
+ "favourite"
+
+ "Move" ->
+ "move"
+
+ "EmojiReact" ->
+ "pleroma:emoji_reaction"
+
+ # Compatibility with old reactions
+ "EmojiReaction" ->
+ "pleroma:emoji_reaction"
+
+ "Create" ->
+ activity
+ |> type_from_activity_object()
+
+ t ->
+ raise "No notification type for activity type #{t}"
+ end
+ end
+
+ defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
+
+ defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
+ object = Object.get_by_ap_id(activity.data["object"])
+
+ case object && object.data["type"] do
+ "ChatMessage" -> "pleroma:chat_mention"
+ _ -> "mention"
+ end
+ end
+end
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 7eca55ac9..9ee9606be 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -30,12 +30,29 @@ defmodule Pleroma.Notification do
schema "notifications" do
field(:seen, :boolean, default: false)
+ # This is an enum type in the database. If you add a new notification type,
+ # remember to add a migration to add it to the `notifications_type` enum
+ # as well.
+ field(:type, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
timestamps()
end
+ def update_notification_type(user, activity) do
+ with %__MODULE__{} = notification <-
+ Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do
+ type =
+ activity
+ |> type_from_activity()
+
+ notification
+ |> changeset(%{type: type})
+ |> Repo.update()
+ end
+ end
+
@spec unread_notifications_count(User.t()) :: integer()
def unread_notifications_count(%User{id: user_id}) do
from(q in __MODULE__,
@@ -44,9 +61,21 @@ defmodule Pleroma.Notification do
|> Repo.aggregate(:count, :id)
end
+ @notification_types ~w{
+ favourite
+ follow
+ follow_request
+ mention
+ move
+ pleroma:chat_mention
+ pleroma:emoji_reaction
+ reblog
+ }
+
def changeset(%Notification{} = notification, attrs) do
notification
- |> cast(attrs, [:seen])
+ |> cast(attrs, [:seen, :type])
+ |> validate_inclusion(:type, @notification_types)
end
@spec last_read_query(User.t()) :: Ecto.Queryable.t()
@@ -137,8 +166,16 @@ defmodule Pleroma.Notification do
query
|> join(:left, [n, a], mutated_activity in Pleroma.Activity,
on:
- fragment("?->>'context'", a.data) ==
- fragment("?->>'context'", mutated_activity.data) and
+ fragment(
+ "COALESCE((?->'object')->>'id', ?->>'object')",
+ a.data,
+ a.data
+ ) ==
+ fragment(
+ "COALESCE((?->'object')->>'id', ?->>'object')",
+ mutated_activity.data,
+ mutated_activity.data
+ ) and
fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
fragment("?->>'type'", mutated_activity.data) == "Create",
as: :mutated_activity
@@ -300,42 +337,95 @@ defmodule Pleroma.Notification do
end
end
- def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
- object = Object.normalize(activity)
+ def create_notifications(activity, options \\ [])
+
+ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
+ object = Object.normalize(activity, false)
if object && object.data["type"] == "Answer" do
{:ok, []}
else
- do_create_notifications(activity)
+ do_create_notifications(activity, options)
end
end
- def create_notifications(%Activity{data: %{"type" => type}} = activity)
+ def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
- do_create_notifications(activity)
+ do_create_notifications(activity, options)
end
- def create_notifications(_), do: {:ok, []}
+ def create_notifications(_, _), do: {:ok, []}
+
+ defp do_create_notifications(%Activity{} = activity, options) do
+ do_send = Keyword.get(options, :do_send, true)
- defp do_create_notifications(%Activity{} = activity) do
{enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
potential_receivers = enabled_receivers ++ disabled_receivers
notifications =
Enum.map(potential_receivers, fn user ->
- do_send = user in enabled_receivers
+ do_send = do_send && user in enabled_receivers
create_notification(activity, user, do_send)
end)
{:ok, notifications}
end
+ defp type_from_activity(%{data: %{"type" => type}} = activity) do
+ case type do
+ "Follow" ->
+ if Activity.follow_accepted?(activity) do
+ "follow"
+ else
+ "follow_request"
+ end
+
+ "Announce" ->
+ "reblog"
+
+ "Like" ->
+ "favourite"
+
+ "Move" ->
+ "move"
+
+ "EmojiReact" ->
+ "pleroma:emoji_reaction"
+
+ # Compatibility with old reactions
+ "EmojiReaction" ->
+ "pleroma:emoji_reaction"
+
+ "Create" ->
+ activity
+ |> type_from_activity_object()
+
+ t ->
+ raise "No notification type for activity type #{t}"
+ end
+ end
+
+ defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
+
+ defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
+ object = Object.get_by_ap_id(activity.data["object"])
+
+ case object && object.data["type"] do
+ "ChatMessage" -> "pleroma:chat_mention"
+ _ -> "mention"
+ end
+ end
+
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
unless skip?(activity, user) do
{:ok, %{notification: notification}} =
Multi.new()
- |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
+ |> Multi.insert(:notification, %Notification{
+ user_id: user.id,
+ activity: activity,
+ type: type_from_activity(activity)
+ })
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
@@ -459,6 +549,7 @@ defmodule Pleroma.Notification do
def skip?(%Activity{} = activity, %User{} = user) do
[
:self,
+ :invisible,
:followers,
:follows,
:non_followers,
@@ -475,6 +566,12 @@ defmodule Pleroma.Notification do
activity.data["actor"] == user.ap_id
end
+ def skip?(:invisible, %Activity{} = activity, _) do
+ actor = activity.data["actor"]
+ user = User.get_cached_by_ap_id(actor)
+ User.invisible?(user)
+ end
+
def skip?(
:followers,
%Activity{} = activity,
@@ -527,4 +624,12 @@ defmodule Pleroma.Notification do
end
def skip?(_, _, _), do: false
+
+ def for_user_and_activity(user, activity) do
+ from(n in __MODULE__,
+ where: n.user_id == ^user.id,
+ where: n.activity_id == ^activity.id
+ )
+ |> Repo.one()
+ end
end
diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex
index 1b99e44f9..9a3795769 100644
--- a/lib/pleroma/pagination.ex
+++ b/lib/pleroma/pagination.ex
@@ -64,6 +64,12 @@ defmodule Pleroma.Pagination do
@spec paginate(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
def paginate(query, options, method \\ :keyset, table_binding \\ nil)
+ def paginate(list, options, _method, _table_binding) when is_list(list) do
+ offset = options[:offset] || 0
+ limit = options[:limit] || 0
+ Enum.slice(list, offset, limit)
+ end
+
def paginate(query, options, :keyset, table_binding) do
query
|> restrict(:min_id, options, table_binding)
diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex
index 6a339b32c..1420a9611 100644
--- a/lib/pleroma/plugs/http_security_plug.ex
+++ b/lib/pleroma/plugs/http_security_plug.ex
@@ -113,6 +113,10 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
add_source(acc, host)
end)
+ media_proxy_base_url =
+ if Config.get([:media_proxy, :base_url]),
+ do: URI.parse(Config.get([:media_proxy, :base_url])).host
+
upload_base_url =
if Config.get([Pleroma.Upload, :base_url]),
do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host
@@ -122,6 +126,7 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host
[]
+ |> add_source(media_proxy_base_url)
|> add_source(upload_base_url)
|> add_source(s3_endpoint)
|> add_source(media_proxy_whitelist)
diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex
index 94147e0c4..40984cfc0 100644
--- a/lib/pleroma/plugs/uploaded_media.ex
+++ b/lib/pleroma/plugs/uploaded_media.ex
@@ -10,6 +10,8 @@ defmodule Pleroma.Plugs.UploadedMedia do
import Pleroma.Web.Gettext
require Logger
+ alias Pleroma.Web.MediaProxy
+
@behaviour Plug
# no slashes
@path "media"
@@ -35,8 +37,7 @@ defmodule Pleroma.Plugs.UploadedMedia do
%{query_params: %{"name" => name}} = conn ->
name = String.replace(name, "\"", "\\\"")
- conn
- |> put_resp_header("content-disposition", "filename=\"#{name}\"")
+ put_resp_header(conn, "content-disposition", "filename=\"#{name}\"")
conn ->
conn
@@ -47,7 +48,8 @@ defmodule Pleroma.Plugs.UploadedMedia do
with uploader <- Keyword.fetch!(config, :uploader),
proxy_remote = Keyword.get(config, :proxy_remote, false),
- {:ok, get_method} <- uploader.get_file(file) do
+ {:ok, get_method} <- uploader.get_file(file),
+ false <- media_is_banned(conn, get_method) do
get_media(conn, get_method, proxy_remote, opts)
else
_ ->
@@ -59,6 +61,14 @@ defmodule Pleroma.Plugs.UploadedMedia do
def call(conn, _opts), do: conn
+ defp media_is_banned(%{request_path: path} = _conn, {:static_dir, _}) do
+ MediaProxy.in_banned_urls(Pleroma.Web.base_url() <> path)
+ end
+
+ defp media_is_banned(_, {:url, url}), do: MediaProxy.in_banned_urls(url)
+
+ defp media_is_banned(_, _), do: false
+
defp get_media(conn, {:static_dir, directory}, _, opts) do
static_opts =
Map.get(opts, :static_plug_opts)
diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex
index f62138466..f317e4d58 100644
--- a/lib/pleroma/repo.ex
+++ b/lib/pleroma/repo.ex
@@ -8,11 +8,10 @@ defmodule Pleroma.Repo do
adapter: Ecto.Adapters.Postgres,
migration_timestamps: [type: :naive_datetime_usec]
+ import Ecto.Query
require Logger
- defmodule Instrumenter do
- use Prometheus.EctoInstrumenter
- end
+ defmodule Instrumenter, do: use(Prometheus.EctoInstrumenter)
@doc """
Dynamically loads the repository url from the
@@ -50,36 +49,30 @@ defmodule Pleroma.Repo do
end
end
- def check_migrations_applied!() do
- unless Pleroma.Config.get(
- [:i_am_aware_this_may_cause_data_loss, :disable_migration_check],
- false
- ) do
- Ecto.Migrator.with_repo(__MODULE__, fn repo ->
- down_migrations =
- Ecto.Migrator.migrations(repo)
- |> Enum.reject(fn
- {:up, _, _} -> true
- {:down, _, _} -> false
- end)
-
- if length(down_migrations) > 0 do
- down_migrations_text =
- Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end)
-
- Logger.error(
- "The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true"
- )
+ def chunk_stream(query, chunk_size) do
+ # We don't actually need start and end funcitons of resource streaming,
+ # but it seems to be the only way to not fetch records one-by-one and
+ # have individual records be the elements of the stream, instead of
+ # lists of records
+ Stream.resource(
+ fn -> 0 end,
+ fn
+ last_id ->
+ query
+ |> order_by(asc: :id)
+ |> where([r], r.id > ^last_id)
+ |> limit(^chunk_size)
+ |> all()
+ |> case do
+ [] ->
+ {:halt, last_id}
- raise Pleroma.Repo.UnappliedMigrationsError
- end
- end)
- else
- :ok
- end
+ records ->
+ last_id = List.last(records).id
+ {records, last_id}
+ end
+ end,
+ fn _ -> :ok end
+ )
end
end
-
-defmodule Pleroma.Repo.UnappliedMigrationsError do
- defexception message: "Unapplied Migrations detected"
-end
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
index d01728361..3aa6909d2 100644
--- a/lib/pleroma/signature.ex
+++ b/lib/pleroma/signature.ex
@@ -5,10 +5,10 @@
defmodule Pleroma.Signature do
@behaviour HTTPSignatures.Adapter
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Keys
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.ObjectValidators.Types
def key_id_to_actor_id(key_id) do
uri =
@@ -24,7 +24,7 @@ defmodule Pleroma.Signature do
maybe_ap_id = URI.to_string(uri)
- case Types.ObjectID.cast(maybe_ap_id) do
+ case ObjectValidators.ObjectID.cast(maybe_ap_id) do
{:ok, ap_id} ->
{:ok, ap_id}
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 1be1a3a5b..797555bff 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -67,6 +67,7 @@ defmodule Pleroma.Upload do
{:ok,
%{
"type" => opts.activity_type,
+ "mediaType" => upload.content_type,
"url" => [
%{
"type" => "Link",
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index c5c74d132..1d70a37ef 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -14,6 +14,7 @@ defmodule Pleroma.User do
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
@@ -30,7 +31,6 @@ defmodule Pleroma.User do
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
- alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
@@ -79,6 +79,7 @@ defmodule Pleroma.User do
schema "users" do
field(:bio, :string)
+ field(:raw_bio, :string)
field(:email, :string)
field(:name, :string)
field(:nickname, :string)
@@ -115,7 +116,7 @@ defmodule Pleroma.User do
field(:is_admin, :boolean, default: false)
field(:show_role, :boolean, default: true)
field(:settings, :map, default: nil)
- field(:uri, Types.Uri, default: nil)
+ field(:uri, ObjectValidators.Uri, default: nil)
field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false)
field(:hide_followers, :boolean, default: false)
@@ -262,37 +263,60 @@ defmodule Pleroma.User do
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
def account_status(%User{confirmation_pending: true}) do
- case Config.get([:instance, :account_activation_required]) do
- true -> :confirmation_pending
- _ -> :active
+ if Config.get([:instance, :account_activation_required]) do
+ :confirmation_pending
+ else
+ :active
end
end
def account_status(%User{}), do: :active
- @spec visible_for?(User.t(), User.t() | nil) :: boolean()
- def visible_for?(user, for_user \\ nil)
+ @spec visible_for(User.t(), User.t() | nil) ::
+ :visible
+ | :invisible
+ | :restricted_unauthenticated
+ | :deactivated
+ | :confirmation_pending
+ def visible_for(user, for_user \\ nil)
- def visible_for?(%User{invisible: true}, _), do: false
+ def visible_for(%User{invisible: true}, _), do: :invisible
- def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true
+ def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
- def visible_for?(%User{local: local} = user, nil) do
- cfg_key =
- if local,
- do: :local,
- else: :remote
+ def visible_for(%User{} = user, nil) do
+ if restrict_unauthenticated?(user) do
+ :restrict_unauthenticated
+ else
+ visible_account_status(user)
+ end
+ end
- if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
- do: false,
- else: account_status(user) == :active
+ def visible_for(%User{} = user, for_user) do
+ if superuser?(for_user) do
+ :visible
+ else
+ visible_account_status(user)
+ end
end
- def visible_for?(%User{} = user, for_user) do
- account_status(user) == :active || superuser?(for_user)
+ def visible_for(_, _), do: :invisible
+
+ defp restrict_unauthenticated?(%User{local: local}) do
+ config_key = if local, do: :local, else: :remote
+
+ Config.get([:restrict_unauthenticated, :profiles, config_key], false)
end
- def visible_for?(_, _), do: false
+ defp visible_account_status(user) do
+ status = account_status(user)
+
+ if status in [:active, :password_reset_pending] do
+ :visible
+ else
+ status
+ end
+ end
@spec superuser?(User.t()) :: boolean()
def superuser?(%User{local: true, is_admin: true}), do: true
@@ -432,6 +456,7 @@ defmodule Pleroma.User do
params,
[
:bio,
+ :raw_bio,
:name,
:emoji,
:avatar,
@@ -463,6 +488,7 @@ defmodule Pleroma.User do
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
+ |> validate_inclusion(:actor_type, ["Person", "Service"])
|> put_fields()
|> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
@@ -607,7 +633,16 @@ defmodule Pleroma.User do
struct
|> confirmation_changeset(need_confirmation: need_confirmation?)
- |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation, :emoji])
+ |> cast(params, [
+ :bio,
+ :raw_bio,
+ :email,
+ :name,
+ :nickname,
+ :password,
+ :password_confirmation,
+ :emoji
+ ])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
@@ -747,7 +782,6 @@ defmodule Pleroma.User do
follower
|> update_following_count()
- |> set_cache()
end
end
@@ -776,7 +810,6 @@ defmodule Pleroma.User do
{:ok, follower} =
follower
|> update_following_count()
- |> set_cache()
{:ok, follower, followed}
@@ -1128,35 +1161,25 @@ defmodule Pleroma.User do
])
end
+ @spec update_follower_count(User.t()) :: {:ok, User.t()}
def update_follower_count(%User{} = user) do
if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
- follower_count_query =
- User.Query.build(%{followers: user, deactivated: false})
- |> select([u], %{count: count(u.id)})
-
- User
- |> where(id: ^user.id)
- |> join(:inner, [u], s in subquery(follower_count_query))
- |> update([u, s],
- set: [follower_count: s.count]
- )
- |> select([u], u)
- |> Repo.update_all([])
- |> case do
- {1, [user]} -> set_cache(user)
- _ -> {:error, user}
- end
+ follower_count = FollowingRelationship.follower_count(user)
+
+ user
+ |> follow_information_changeset(%{follower_count: follower_count})
+ |> update_and_set_cache
else
{:ok, maybe_fetch_follow_information(user)}
end
end
- @spec update_following_count(User.t()) :: User.t()
+ @spec update_following_count(User.t()) :: {:ok, User.t()}
def update_following_count(%User{local: false} = user) do
if Pleroma.Config.get([:instance, :external_user_synchronization]) do
- maybe_fetch_follow_information(user)
+ {:ok, maybe_fetch_follow_information(user)}
else
- user
+ {:ok, user}
end
end
@@ -1165,7 +1188,7 @@ defmodule Pleroma.User do
user
|> follow_information_changeset(%{following_count: following_count})
- |> Repo.update!()
+ |> update_and_set_cache()
end
def set_unread_conversation_count(%User{local: true} = user) do
@@ -1488,6 +1511,7 @@ defmodule Pleroma.User do
end)
delete_user_activities(user)
+ delete_notifications_from_user_activities(user)
delete_outgoing_pending_follow_requests(user)
@@ -1576,6 +1600,13 @@ defmodule Pleroma.User do
})
end
+ def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
+ Notification
+ |> join(:inner, [n], activity in assoc(n, :activity))
+ |> where([n, a], fragment("? = ?", a.actor, ^ap_id))
+ |> Repo.delete_all()
+ end
+
def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id
|> Activity.Queries.by_actor()
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index eb73c95fe..3e4d0a2be 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics
+ alias Pleroma.ActivityExpiration
alias Pleroma.Config
alias Pleroma.Constants
alias Pleroma.Conversation
@@ -31,25 +32,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
require Logger
require Pleroma.Constants
- # For Announce activities, we filter the recipients based on following status for any actors
- # that match actual users. See issue #164 for more information about why this is necessary.
- defp get_recipients(%{"type" => "Announce"} = data) do
- to = Map.get(data, "to", [])
- cc = Map.get(data, "cc", [])
- bcc = Map.get(data, "bcc", [])
- actor = User.get_cached_by_ap_id(data["actor"])
-
- recipients =
- Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->
- case User.get_cached_by_ap_id(recipient) do
- nil -> true
- user -> User.following?(user, actor)
- end
- end)
-
- {recipients, to, cc}
- end
-
defp get_recipients(%{"type" => "Create"} = data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
@@ -112,7 +94,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp increase_poll_votes_if_vote(_create_data), do: :noop
+ @object_types ["ChatMessage"]
@spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
+ def persist(%{"type" => type} = object, meta) when type in @object_types do
+ with {:ok, object} <- Object.create(object) do
+ {:ok, object, meta}
+ end
+ end
+
def persist(object, meta) do
with local <- Keyword.fetch!(meta, :local),
{recipients, _, _} <- get_recipients(object),
@@ -139,12 +128,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:containment, :ok} <- {:containment, Containment.contain_child(map)},
{:ok, map, object} <- insert_full_object(map) do
{:ok, activity} =
- Repo.insert(%Activity{
+ %Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients
- })
+ }
+ |> Repo.insert()
+ |> maybe_create_activity_expiration()
# Splice in the child object if we have one.
activity = Maps.put_if_present(activity, :object, object)
@@ -182,6 +173,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
stream_out_participations(participations)
end
+ defp maybe_create_activity_expiration({:ok, %{data: %{"expires_at" => expires_at}} = activity}) do
+ with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
+ {:ok, activity}
+ end
+ end
+
+ defp maybe_create_activity_expiration(result), do: result
+
defp create_or_bump_conversation(activity, actor) do
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
%User{} = user <- User.get_cached_by_ap_id(actor) do
@@ -211,7 +210,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
conversation = Repo.preload(conversation, :participations)
last_activity_id =
- fetch_latest_activity_id_for_context(conversation.ap_id, %{
+ fetch_latest_direct_activity_id_for_context(conversation.ap_id, %{
user: user,
blocking_user: user
})
@@ -344,20 +343,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- @spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
+ @spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) ::
{:ok, Activity.t()} | {:error, any()}
- def follow(follower, followed, activity_id \\ nil, local \\ true) do
+ def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do
with {:ok, result} <-
- Repo.transaction(fn -> do_follow(follower, followed, activity_id, local) end) do
+ Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do
result
end
end
- defp do_follow(follower, followed, activity_id, local) do
+ defp do_follow(follower, followed, activity_id, local, opts) do
+ skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false)
data = make_follow_data(follower, followed, activity_id)
with {:ok, activity} <- insert(data, local),
- _ <- notify_and_stream(activity),
+ _ <- skip_notify_and_stream || notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -517,11 +517,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Repo.all()
end
- @spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
+ @spec fetch_latest_direct_activity_id_for_context(String.t(), keyword() | map()) ::
FlakeId.Ecto.CompatType.t() | nil
- def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
+ def fetch_latest_direct_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts))
+ |> restrict_visibility(%{visibility: "direct"})
|> limit(1)
|> select([a], a.id)
|> Repo.one()
@@ -702,6 +703,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
+ defp restrict_announce_object_actor(_query, %{announce_filtering_user: _, skip_preload: true}) do
+ raise "Can't use the child object without preloading!"
+ end
+
+ defp restrict_announce_object_actor(query, %{announce_filtering_user: %{ap_id: actor}}) do
+ from(
+ [activity, object] in query,
+ where:
+ fragment(
+ "?->>'type' != ? or ?->>'actor' != ?",
+ activity.data,
+ "Announce",
+ object.data,
+ ^actor
+ )
+ )
+ end
+
+ defp restrict_announce_object_actor(query, _), do: query
+
defp restrict_since(query, %{since_id: ""}), do: query
defp restrict_since(query, %{since_id: since_id}) do
@@ -813,7 +834,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_media(query, %{only_media: true}) do
from(
- [_activity, object] in query,
+ [activity, object] in query,
+ where: fragment("(?)->>'type' = ?", activity.data, "Create"),
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
)
end
@@ -1000,6 +1022,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
+ defp exclude_chat_messages(query, %{include_chat_messages: true}), do: query
+
+ defp exclude_chat_messages(query, _) do
+ if has_named_binding?(query, :object) do
+ from([activity, object: o] in query,
+ where: fragment("not(?->>'type' = ?)", o.data, "ChatMessage")
+ )
+ else
+ query
+ end
+ end
+
defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query
defp exclude_invisible_actors(query, _opts) do
@@ -1113,8 +1147,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_pinned(opts)
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|> restrict_instance(opts)
+ |> restrict_announce_object_actor(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
+ |> exclude_chat_messages(opts)
|> exclude_invisible_actors(opts)
|> exclude_visibility(opts)
end
@@ -1138,12 +1174,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Activity.Queries.by_type("Like")
|> Activity.with_joined_object()
|> Object.with_joined_activity()
- |> select([_like, object, activity], %{activity | object: object})
+ |> select([like, object, activity], %{activity | object: object, pagination_id: like.id})
|> order_by([like, _, _], desc_nulls_last: like.id)
|> Pagination.fetch_paginated(
Map.merge(params, %{skip_order: true}),
- pagination,
- :object_activity
+ pagination
)
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index f0b5c6e93..220c4fe52 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -514,7 +514,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{new_user, for_user}
end
- # TODO: Add support for "object" field
@doc """
Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
@@ -525,6 +524,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
Response:
- HTTP Code: 201 Created
- HTTP Body: ActivityPub object to be inserted into another's `attachment` field
+
+ Note: Will not point to a URL with a `Location` header because no standalone Activity has been created.
"""
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index 51b74414a..1aac62c69 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
This module encodes our addressing policies and general shape of our objects.
"""
+ alias Pleroma.Emoji
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
@@ -65,6 +66,42 @@ defmodule Pleroma.Web.ActivityPub.Builder do
}, []}
end
+ def create(actor, object, recipients) do
+ {:ok,
+ %{
+ "id" => Utils.generate_activity_id(),
+ "actor" => actor.ap_id,
+ "to" => recipients,
+ "object" => object,
+ "type" => "Create",
+ "published" => DateTime.utc_now() |> DateTime.to_iso8601()
+ }, []}
+ end
+
+ def chat_message(actor, recipient, content, opts \\ []) do
+ basic = %{
+ "id" => Utils.generate_object_id(),
+ "actor" => actor.ap_id,
+ "type" => "ChatMessage",
+ "to" => [recipient],
+ "content" => content,
+ "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
+ "emoji" => Emoji.Formatter.get_emoji_map(content)
+ }
+
+ case opts[:attachment] do
+ %Object{data: attachment_data} ->
+ {
+ :ok,
+ Map.put(basic, "attachment", attachment_data),
+ []
+ }
+
+ _ ->
+ {:ok, basic, []}
+ end
+ end
+
@spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
def tombstone(actor, id) do
{:ok,
diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex
index a0b3af432..206d6af52 100644
--- a/lib/pleroma/web/activity_pub/mrf.ex
+++ b/lib/pleroma/web/activity_pub/mrf.ex
@@ -8,18 +8,15 @@ defmodule Pleroma.Web.ActivityPub.MRF do
def filter(policies, %{} = object) do
policies
|> Enum.reduce({:ok, object}, fn
- policy, {:ok, object} ->
- policy.filter(object)
-
- _, error ->
- error
+ policy, {:ok, object} -> policy.filter(object)
+ _, error -> error
end)
end
def filter(%{} = object), do: get_policies() |> filter(object)
def get_policies do
- Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies()
+ Pleroma.Config.get([:mrf, :policies], []) |> get_policies()
end
defp get_policies(policy) when is_atom(policy), do: [policy]
@@ -54,7 +51,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do
get_policies()
|> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)
- exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
+ exclusions = Pleroma.Config.get([:mrf, :transparency_exclusions])
base =
%{
diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex
new file mode 100644
index 000000000..8e47f1e02
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex
@@ -0,0 +1,43 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
+ @moduledoc "Adds expiration to all local Create activities"
+ @behaviour Pleroma.Web.ActivityPub.MRF
+
+ @impl true
+ def filter(activity) do
+ activity =
+ if note?(activity) and local?(activity) do
+ maybe_add_expiration(activity)
+ else
+ activity
+ end
+
+ {:ok, activity}
+ end
+
+ @impl true
+ def describe, do: {:ok, %{}}
+
+ defp local?(%{"id" => id}) do
+ String.starts_with?(id, Pleroma.Web.Endpoint.url())
+ end
+
+ defp note?(activity) do
+ match?(%{"type" => "Create", "object" => %{"type" => "Note"}}, activity)
+ end
+
+ defp maybe_add_expiration(activity) do
+ days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
+ expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: days)
+
+ with %{"expires_at" => existing_expires_at} <- activity,
+ :lt <- NaiveDateTime.compare(existing_expires_at, expires_at) do
+ activity
+ else
+ _ -> Map.put(activity, "expires_at", expires_at)
+ end
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
index 1764bc789..f6b2c4415 100644
--- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
@@ -13,8 +13,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
defp delist_message(message, threshold) when threshold > 0 do
follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address
+ to = message["to"] || []
+ cc = message["cc"] || []
- follower_collection? = Enum.member?(message["to"] ++ message["cc"], follower_collection)
+ follower_collection? = Enum.member?(to ++ cc, follower_collection)
message =
case get_recipient_count(message) do
@@ -71,7 +73,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
end
@impl true
- def filter(%{"type" => "Create"} = message) do
+ def filter(%{"type" => "Create", "object" => %{"type" => object_type}} = message)
+ when object_type in ~w{Note Article} do
reject_threshold =
Pleroma.Config.get(
[:mrf_hellthread, :reject_threshold],
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index b7dcb1b86..9cea6bcf9 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -3,21 +3,23 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.MRF
@moduledoc "Filter activities depending on their origin instance"
@behaviour Pleroma.Web.ActivityPub.MRF
+ alias Pleroma.Config
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.MRF
+
require Pleroma.Constants
defp check_accept(%{host: actor_host} = _actor_info, object) do
accepts =
- Pleroma.Config.get([:mrf_simple, :accept])
+ Config.get([:mrf_simple, :accept])
|> MRF.subdomains_regex()
cond do
accepts == [] -> {:ok, object}
- actor_host == Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
+ actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
true -> {:reject, nil}
end
@@ -25,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
defp check_reject(%{host: actor_host} = _actor_info, object) do
rejects =
- Pleroma.Config.get([:mrf_simple, :reject])
+ Config.get([:mrf_simple, :reject])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(rejects, actor_host) do
@@ -41,7 +43,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
)
when length(child_attachment) > 0 do
media_removal =
- Pleroma.Config.get([:mrf_simple, :media_removal])
+ Config.get([:mrf_simple, :media_removal])
|> MRF.subdomains_regex()
object =
@@ -65,7 +67,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
} = object
) do
media_nsfw =
- Pleroma.Config.get([:mrf_simple, :media_nsfw])
+ Config.get([:mrf_simple, :media_nsfw])
|> MRF.subdomains_regex()
object =
@@ -85,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
timeline_removal =
- Pleroma.Config.get([:mrf_simple, :federated_timeline_removal])
+ Config.get([:mrf_simple, :federated_timeline_removal])
|> MRF.subdomains_regex()
object =
@@ -108,7 +110,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do
report_removal =
- Pleroma.Config.get([:mrf_simple, :report_removal])
+ Config.get([:mrf_simple, :report_removal])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(report_removal, actor_host) do
@@ -122,7 +124,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do
avatar_removal =
- Pleroma.Config.get([:mrf_simple, :avatar_removal])
+ Config.get([:mrf_simple, :avatar_removal])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(avatar_removal, actor_host) do
@@ -136,7 +138,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do
banner_removal =
- Pleroma.Config.get([:mrf_simple, :banner_removal])
+ Config.get([:mrf_simple, :banner_removal])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(banner_removal, actor_host) do
@@ -197,10 +199,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
@impl true
def describe do
- exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
+ exclusions = Config.get([:mrf, :transparency_exclusions])
mrf_simple =
- Pleroma.Config.get(:mrf_simple)
+ Config.get(:mrf_simple)
|> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end)
|> Enum.into(%{})
diff --git a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
index a927a4ed8..651aed70f 100644
--- a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
allow_list =
Config.get(
- [:mrf_user_allowlist, String.to_atom(actor_info.host)],
+ [:mrf_user_allowlist, actor_info.host],
[]
)
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index 2599067a8..6a83a2c33 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -9,13 +9,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
the system.
"""
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
- alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
@@ -43,8 +45,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => "Like"} = object, meta) do
with {:ok, object} <-
- object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
- object = stringify_keys(object |> Map.from_struct())
+ object
+ |> LikeValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {:ok, object, meta}
+ end
+ end
+
+ def validate(%{"type" => "ChatMessage"} = object, meta) do
+ with {:ok, object} <-
+ object
+ |> ChatMessageValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
{:ok, object, meta}
end
end
@@ -59,6 +73,18 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end
+ def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do
+ with {:ok, object_data} <- cast_and_apply(object),
+ meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
+ {:ok, create_activity} <-
+ create_activity
+ |> CreateChatMessageValidator.cast_and_validate(meta)
+ |> Ecto.Changeset.apply_action(:insert) do
+ create_activity = stringify_keys(create_activity)
+ {:ok, create_activity, meta}
+ end
+ end
+
def validate(%{"type" => "Announce"} = object, meta) do
with {:ok, object} <-
object
@@ -69,19 +95,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end
+ def cast_and_apply(%{"type" => "ChatMessage"} = object) do
+ ChatMessageValidator.cast_and_apply(object)
+ end
+
+ def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
+
def stringify_keys(%{__struct__: _} = object) do
object
|> Map.from_struct()
|> stringify_keys
end
- def stringify_keys(object) do
+ def stringify_keys(object) when is_map(object) do
object
- |> Map.new(fn {key, val} -> {to_string(key), val} end)
+ |> Map.new(fn {key, val} -> {to_string(key), stringify_keys(val)} end)
end
+ def stringify_keys(object) when is_list(object) do
+ object
+ |> Enum.map(&stringify_keys/1)
+ end
+
+ def stringify_keys(object), do: object
+
def fetch_actor(object) do
- with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do
+ with {:ok, actor} <- ObjectValidators.ObjectID.cast(object["actor"]) do
User.get_or_fetch_by_ap_id(actor)
end
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
index 40f861f47..6f757f49c 100644
--- a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
@@ -5,9 +5,9 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
use Ecto.Schema
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@@ -19,14 +19,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
@primary_key false
embedded_schema do
- field(:id, Types.ObjectID, primary_key: true)
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:type, :string)
- field(:object, Types.ObjectID)
- field(:actor, Types.ObjectID)
+ field(:object, ObjectValidators.ObjectID)
+ field(:actor, ObjectValidators.ObjectID)
field(:context, :string, autogenerate: {Utils, :generate_context_id, []})
- field(:to, Types.Recipients, default: [])
- field(:cc, Types.Recipients, default: [])
- field(:published, Types.DateTime)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:published, ObjectValidators.DateTime)
end
def cast_and_validate(data) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
new file mode 100644
index 000000000..f53bb02be
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
@@ -0,0 +1,80 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
+ use Ecto.Schema
+
+ alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator
+
+ import Ecto.Changeset
+
+ @primary_key false
+ embedded_schema do
+ field(:type, :string)
+ field(:mediaType, :string, default: "application/octet-stream")
+ field(:name, :string)
+
+ embeds_many(:url, UrlObjectValidator)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data()
+ |> validate_data()
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> changeset(data)
+ end
+
+ def changeset(struct, data) do
+ data =
+ data
+ |> fix_media_type()
+ |> fix_url()
+
+ struct
+ |> cast(data, [:type, :mediaType, :name])
+ |> cast_embed(:url, required: true)
+ end
+
+ def fix_media_type(data) do
+ data =
+ data
+ |> Map.put_new("mediaType", data["mimeType"])
+
+ if MIME.valid?(data["mediaType"]) do
+ data
+ else
+ data
+ |> Map.put("mediaType", "application/octet-stream")
+ end
+ end
+
+ def fix_url(data) do
+ case data["url"] do
+ url when is_binary(url) ->
+ data
+ |> Map.put(
+ "url",
+ [
+ %{
+ "href" => url,
+ "type" => "Link",
+ "mediaType" => data["mediaType"]
+ }
+ ]
+ )
+
+ _ ->
+ data
+ end
+ end
+
+ def validate_data(cng) do
+ cng
+ |> validate_required([:mediaType, :url, :type])
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
new file mode 100644
index 000000000..c481d79e0
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
@@ -0,0 +1,123 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1]
+
+ @primary_key false
+ @derive Jason.Encoder
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:type, :string)
+ field(:content, ObjectValidators.SafeText)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:published, ObjectValidators.DateTime)
+ field(:emoji, :map, default: %{})
+
+ embeds_one(:attachment, AttachmentValidator)
+ end
+
+ def cast_and_apply(data) do
+ data
+ |> cast_data
+ |> apply_action(:insert)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data()
+ |> validate_data()
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> changeset(data)
+ end
+
+ def fix(data) do
+ data
+ |> fix_emoji()
+ |> fix_attachment()
+ |> Map.put_new("actor", data["attributedTo"])
+ end
+
+ # Throws everything but the first one away
+ def fix_attachment(%{"attachment" => [attachment | _]} = data) do
+ data
+ |> Map.put("attachment", attachment)
+ end
+
+ def fix_attachment(data), do: data
+
+ def changeset(struct, data) do
+ data = fix(data)
+
+ struct
+ |> cast(data, List.delete(__schema__(:fields), :attachment))
+ |> cast_embed(:attachment)
+ end
+
+ def validate_data(data_cng) do
+ data_cng
+ |> validate_inclusion(:type, ["ChatMessage"])
+ |> validate_required([:id, :actor, :to, :type, :published])
+ |> validate_content_or_attachment()
+ |> validate_length(:to, is: 1)
+ |> validate_length(:content, max: Pleroma.Config.get([:instance, :remote_limit]))
+ |> validate_local_concern()
+ end
+
+ def validate_content_or_attachment(cng) do
+ attachment = get_field(cng, :attachment)
+
+ if attachment do
+ cng
+ else
+ cng
+ |> validate_required([:content])
+ end
+ end
+
+ @doc """
+ Validates the following
+ - If both users are in our system
+ - If at least one of the users in this ChatMessage is a local user
+ - If the recipient is not blocking the actor
+ """
+ def validate_local_concern(cng) do
+ with actor_ap <- get_field(cng, :actor),
+ {_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)},
+ {_, %User{} = recipient} <-
+ {:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())},
+ {_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)},
+ {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do
+ cng
+ else
+ {:blocking_actor?, true} ->
+ cng
+ |> add_error(:actor, "actor is blocked by recipient")
+
+ {:local?, false} ->
+ cng
+ |> add_error(:actor, "actor and recipient are both remote")
+
+ {:find_actor, _} ->
+ cng
+ |> add_error(:actor, "can't find user")
+
+ {:find_recipient, _} ->
+ cng
+ |> add_error(:to, "can't find user")
+ end
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex
new file mode 100644
index 000000000..7269f9ff0
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex
@@ -0,0 +1,91 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+# NOTES
+# - Can probably be a generic create validator
+# - doesn't embed, will only get the object id
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do
+ use Ecto.Schema
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+
+ alias Pleroma.Object
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:type, :string)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:object, ObjectValidators.ObjectID)
+ end
+
+ def cast_and_apply(data) do
+ data
+ |> cast_data
+ |> apply_action(:insert)
+ end
+
+ def cast_data(data) do
+ cast(%__MODULE__{}, data, __schema__(:fields))
+ end
+
+ def cast_and_validate(data, meta \\ []) do
+ cast_data(data)
+ |> validate_data(meta)
+ end
+
+ def validate_data(cng, meta \\ []) do
+ cng
+ |> validate_required([:id, :actor, :to, :type, :object])
+ |> validate_inclusion(:type, ["Create"])
+ |> validate_actor_presence()
+ |> validate_recipients_match(meta)
+ |> validate_actors_match(meta)
+ |> validate_object_nonexistence()
+ end
+
+ def validate_object_nonexistence(cng) do
+ cng
+ |> validate_change(:object, fn :object, object_id ->
+ if Object.get_cached_by_ap_id(object_id) do
+ [{:object, "The object to create already exists"}]
+ else
+ []
+ end
+ end)
+ end
+
+ def validate_actors_match(cng, meta) do
+ object_actor = meta[:object_data]["actor"]
+
+ cng
+ |> validate_change(:actor, fn :actor, actor ->
+ if actor == object_actor do
+ []
+ else
+ [{:actor, "Actor doesn't match with object actor"}]
+ end
+ end)
+ end
+
+ def validate_recipients_match(cng, meta) do
+ object_recipients = meta[:object_data]["to"] || []
+
+ cng
+ |> validate_change(:to, fn :to, recipients ->
+ activity_set = MapSet.new(recipients)
+ object_set = MapSet.new(object_recipients)
+
+ if MapSet.equal?(activity_set, object_set) do
+ []
+ else
+ [{:to, "Recipients don't match with object recipients"}]
+ end
+ end)
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex
index 926804ce7..316bd0c07 100644
--- a/lib/pleroma/web/activity_pub/object_validators/create_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex
@@ -5,16 +5,16 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateNoteValidator do
use Ecto.Schema
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator
- alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
@primary_key false
embedded_schema do
- field(:id, Types.ObjectID, primary_key: true)
- field(:actor, Types.ObjectID)
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:actor, ObjectValidators.ObjectID)
field(:type, :string)
field(:to, {:array, :string})
field(:cc, {:array, :string})
diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
index f42c03510..93a7b0e0b 100644
--- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
@@ -6,8 +6,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
use Ecto.Schema
alias Pleroma.Activity
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@@ -15,13 +15,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
@primary_key false
embedded_schema do
- field(:id, Types.ObjectID, primary_key: true)
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:type, :string)
- field(:actor, Types.ObjectID)
- field(:to, Types.Recipients, default: [])
- field(:cc, Types.Recipients, default: [])
- field(:deleted_activity_id, Types.ObjectID)
- field(:object, Types.ObjectID)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:deleted_activity_id, ObjectValidators.ObjectID)
+ field(:object, ObjectValidators.ObjectID)
end
def cast_data(data) do
@@ -46,12 +46,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
Answer
Article
Audio
+ ChatMessage
Event
Note
Page
Question
- Video
Tombstone
+ Video
}
def validate_data(cng) do
cng
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 e87519c59..a543af1f8 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
@@ -5,8 +5,8 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
use Ecto.Schema
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object
- alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@@ -14,10 +14,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
@primary_key false
embedded_schema do
- field(:id, Types.ObjectID, primary_key: true)
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:type, :string)
- field(:object, Types.ObjectID)
- field(:actor, Types.ObjectID)
+ field(:object, ObjectValidators.ObjectID)
+ field(:actor, ObjectValidators.ObjectID)
field(:context, :string)
field(:content, :string)
field(:to, {:array, :string}, default: [])
diff --git a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
index 034f25492..493e4c247 100644
--- a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
@@ -5,8 +5,8 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
use Ecto.Schema
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object
- alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Utils
import Ecto.Changeset
@@ -15,13 +15,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
@primary_key false
embedded_schema do
- field(:id, Types.ObjectID, primary_key: true)
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:type, :string)
- field(:object, Types.ObjectID)
- field(:actor, Types.ObjectID)
+ field(:object, ObjectValidators.ObjectID)
+ field(:actor, ObjectValidators.ObjectID)
field(:context, :string)
- field(:to, Types.Recipients, default: [])
- field(:cc, Types.Recipients, default: [])
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
end
def cast_and_validate(data) do
@@ -67,7 +67,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
with {[], []} <- {to, cc},
%Object{data: %{"actor" => actor}} <- Object.get_cached_by_ap_id(object),
- {:ok, actor} <- Types.ObjectID.cast(actor) do
+ {:ok, actor} <- ObjectValidators.ObjectID.cast(actor) do
cng
|> put_change(:to, [actor])
else
diff --git a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex
index 462a5620a..56b93dde8 100644
--- a/lib/pleroma/web/activity_pub/object_validators/note_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/note_validator.ex
@@ -5,14 +5,14 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
use Ecto.Schema
- alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
import Ecto.Changeset
@primary_key false
embedded_schema do
- field(:id, Types.ObjectID, primary_key: true)
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}, default: [])
field(:bto, {:array, :string}, default: [])
@@ -22,10 +22,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
field(:type, :string)
field(:content, :string)
field(:context, :string)
- field(:actor, Types.ObjectID)
- field(:attributedTo, Types.ObjectID)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:attributedTo, ObjectValidators.ObjectID)
field(:summary, :string)
- field(:published, Types.DateTime)
+ field(:published, ObjectValidators.DateTime)
# TODO: Write type
field(:emoji, :map, default: %{})
field(:sensitive, :boolean, default: false)
@@ -35,13 +35,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
field(:inRepyTo, :string)
- field(:uri, Types.Uri)
+ field(:uri, ObjectValidators.Uri)
field(:likes, {:array, :string}, default: [])
field(:announcements, {:array, :string}, default: [])
# see if needed
- field(:conversation, :string)
field(:context_id, :string)
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
deleted file mode 100644
index 48fe61e1a..000000000
--- a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
+++ /dev/null
@@ -1,34 +0,0 @@
-defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do
- use Ecto.Type
-
- alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
-
- def type, do: {:array, ObjectID}
-
- def cast(object) when is_binary(object) do
- cast([object])
- end
-
- def cast(data) when is_list(data) do
- data
- |> Enum.reduce({:ok, []}, fn element, acc ->
- case {acc, ObjectID.cast(element)} do
- {:error, _} -> :error
- {_, :error} -> :error
- {{:ok, list}, {:ok, id}} -> {:ok, [id | list]}
- end
- end)
- end
-
- def cast(_) do
- :error
- end
-
- def dump(data) do
- {:ok, data}
- end
-
- def load(data) do
- {:ok, data}
- end
-end
diff --git a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex
index d0ba418e8..e8d2d39c1 100644
--- a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
use Ecto.Schema
alias Pleroma.Activity
- alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@@ -14,10 +14,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
@primary_key false
embedded_schema do
- field(:id, Types.ObjectID, primary_key: true)
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:type, :string)
- field(:object, Types.ObjectID)
- field(:actor, Types.ObjectID)
+ field(:object, ObjectValidators.ObjectID)
+ field(:actor, ObjectValidators.ObjectID)
field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}, default: [])
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex
new file mode 100644
index 000000000..f64fac46d
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex
@@ -0,0 +1,24 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+
+ import Ecto.Changeset
+ @primary_key false
+
+ embedded_schema do
+ field(:type, :string)
+ field(:href, ObjectValidators.Uri)
+ field(:mediaType, :string)
+ end
+
+ def changeset(struct, data) do
+ struct
+ |> cast(data, __schema__(:fields))
+ |> validate_required([:type, :href, :mediaType])
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex
index 0c54c4b23..6875c47f6 100644
--- a/lib/pleroma/web/activity_pub/pipeline.ex
+++ b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -17,6 +17,10 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do
+ {:ok, {:ok, activity, meta}} ->
+ SideEffects.handle_after_transaction(meta)
+ {:ok, activity, meta}
+
{:ok, value} ->
value
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index fb6275450..1a1cc675c 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -6,12 +6,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
collection, and so on.
"""
alias Pleroma.Activity
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.Push
+ alias Pleroma.Web.Streamer
def handle(object, meta \\ [])
@@ -27,6 +32,24 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
{:ok, object, meta}
end
+ # Tasks this handles
+ # - Actually create object
+ # - Rollback if we couldn't create it
+ # - Set up notifications
+ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
+ with {:ok, _object, meta} <- handle_object_creation(meta[:object_data], meta) do
+ {:ok, notifications} = Notification.create_notifications(activity, do_send: false)
+
+ meta =
+ meta
+ |> add_notifications(notifications)
+
+ {:ok, activity, meta}
+ else
+ e -> Repo.rollback(e)
+ end
+ end
+
# Tasks this handles:
# - Add announce to object
# - Set up notification
@@ -88,6 +111,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
Object.decrease_replies_count(in_reply_to)
end
+ MessageReference.delete_for_object(deleted_object)
+
ActivityPub.stream_out(object)
ActivityPub.stream_out_participations(deleted_object, user)
:ok
@@ -112,6 +137,39 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
{:ok, object, meta}
end
+ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do
+ with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
+ actor = User.get_cached_by_ap_id(object.data["actor"])
+ recipient = User.get_cached_by_ap_id(hd(object.data["to"]))
+
+ streamables =
+ [[actor, recipient], [recipient, actor]]
+ |> Enum.map(fn [user, other_user] ->
+ if user.local do
+ {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
+ {:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id)
+
+ {
+ ["user", "user:pleroma_chat"],
+ {user, %{cm_ref | chat: chat, object: object}}
+ }
+ end
+ end)
+ |> Enum.filter(& &1)
+
+ meta =
+ meta
+ |> add_streamables(streamables)
+
+ {:ok, object, meta}
+ end
+ end
+
+ # Nothing to do
+ def handle_object_creation(object) do
+ {:ok, object}
+ end
+
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_like_from_object(object, liked_object),
@@ -148,4 +206,43 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
+
+ defp send_notifications(meta) do
+ Keyword.get(meta, :notifications, [])
+ |> Enum.each(fn notification ->
+ Streamer.stream(["user", "user:notification"], notification)
+ Push.send(notification)
+ end)
+
+ meta
+ end
+
+ defp send_streamables(meta) do
+ Keyword.get(meta, :streamables, [])
+ |> Enum.each(fn {topics, items} ->
+ Streamer.stream(topics, items)
+ end)
+
+ meta
+ end
+
+ defp add_streamables(meta, streamables) do
+ existing = Keyword.get(meta, :streamables, [])
+
+ meta
+ |> Keyword.put(:streamables, streamables ++ existing)
+ end
+
+ defp add_notifications(meta, notifications) do
+ existing = Keyword.get(meta, :notifications, [])
+
+ meta
+ |> Keyword.put(:notifications, notifications ++ existing)
+ end
+
+ def handle_after_transaction(meta) do
+ meta
+ |> send_notifications()
+ |> send_streamables()
+ end
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index fda1c71df..1c60ef8f5 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -8,8 +8,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"""
alias Pleroma.Activity
alias Pleroma.EarmarkRenderer
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.FollowingRelationship
alias Pleroma.Maps
+ alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
@@ -17,7 +19,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
- alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@@ -171,8 +172,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
- |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
+ |> Map.drop(["conversation"])
else
e ->
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
@@ -206,7 +207,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object
|> Map.put("context", context)
- |> Map.put("conversation", context)
+ |> Map.drop(["conversation"])
end
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
@@ -221,9 +222,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
media_type =
cond do
- is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"]
- is_binary(data["mediaType"]) -> data["mediaType"]
- is_binary(data["mimeType"]) -> data["mimeType"]
+ is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"]
+ MIME.valid?(data["mediaType"]) -> data["mediaType"]
+ MIME.valid?(data["mimeType"]) -> data["mimeType"]
true -> nil
end
@@ -457,7 +458,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
to: data["to"],
object: object,
actor: user,
- context: object["conversation"],
+ context: object["context"],
local: false,
published: data["published"],
additional:
@@ -527,7 +528,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
{:ok, %User{} = follower} <-
User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
- {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
+ {:ok, activity} <-
+ ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
{_, false} <- {:user_locked, User.locked?(followed)},
@@ -570,6 +572,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
:noop
end
+ ActivityPub.notify_and_stream(activity)
{:ok, activity}
else
_e ->
@@ -590,6 +593,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
User.update_follower_count(followed)
User.update_following_count(follower)
+ Notification.update_notification_type(followed, follow_activity)
+
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
@@ -657,6 +662,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> handle_incoming(options)
end
+ def handle_incoming(
+ %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
+ _options
+ ) do
+ with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
+ {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
+ {:ok, activity}
+ end
+ end
+
def handle_incoming(%{"type" => type} = data, _options)
when type in ["Like", "EmojiReact", "Announce"] do
with :ok <- ObjectValidator.fetch_actor_and_object(data),
@@ -710,7 +725,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
else
{:error, {:validate_object, _}} = e ->
# Check if we have a create activity for this
- with {:ok, object_id} <- Types.ObjectID.cast(data["object"]),
+ with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
%Activity{data: %{"actor" => actor}} <-
Activity.create_by_object_ap_id(object_id) |> Repo.one(),
# We have one, insert a tombstone and retry
@@ -1108,6 +1123,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "attributedTo", attributed_to)
end
+ # TODO: Revisit this
+ def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
+
def prepare_attachments(object) do
attachments =
object
diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
index 335862340..f9545d895 100644
--- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -111,8 +111,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action: "delete"
})
- conn
- |> json(nicknames)
+ json(conn, nicknames)
end
def user_follow(%{assigns: %{user: admin}} = conn, %{
@@ -131,8 +130,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
end
- conn
- |> json("ok")
+ json(conn, "ok")
end
def user_unfollow(%{assigns: %{user: admin}} = conn, %{
@@ -151,8 +149,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
end
- conn
- |> json("ok")
+ json(conn, "ok")
end
def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
@@ -191,8 +188,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action: "create"
})
- conn
- |> json(res)
+ json(conn, res)
{:error, id, changeset, _} ->
res =
@@ -363,8 +359,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
filters
|> String.split(",")
|> Enum.filter(&Enum.member?(@filters, &1))
- |> Enum.map(&String.to_atom(&1))
- |> Enum.into(%{}, &{&1, true})
+ |> Enum.map(&String.to_atom/1)
+ |> Map.new(&{&1, true})
end
def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
@@ -568,10 +564,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
{:error, changeset} ->
errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
- json(conn, %{errors: errors})
+ {:errors, errors}
_ ->
- json(conn, %{error: "Unable to update user."})
+ {:error, :not_found}
end
end
@@ -616,7 +612,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
def reload_emoji(conn, _params) do
Pleroma.Emoji.reload()
- conn |> json("ok")
+ json(conn, "ok")
end
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
@@ -630,7 +626,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action: "confirm_email"
})
- conn |> json("")
+ json(conn, "")
end
def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
@@ -644,7 +640,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action: "resend_confirmation_email"
})
- conn |> json("")
+ json(conn, "")
end
def stats(conn, params) do
diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex
index d6e2019bc..7f60470cb 100644
--- a/lib/pleroma/web/admin_api/controllers/config_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex
@@ -33,7 +33,11 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
def show(conn, %{only_db: true}) do
with :ok <- configurable_from_database() do
configs = Pleroma.Repo.all(ConfigDB)
- render(conn, "index.json", %{configs: configs})
+
+ render(conn, "index.json", %{
+ configs: configs,
+ need_reboot: Restarter.Pleroma.need_reboot?()
+ })
end
end
@@ -61,17 +65,20 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
value
end
- %{
- group: ConfigDB.convert(group),
- key: ConfigDB.convert(key),
- value: ConfigDB.convert(merged_value)
+ %ConfigDB{
+ group: group,
+ key: key,
+ value: merged_value
}
|> Pleroma.Maps.put_if_present(:db, db)
end)
end)
|> List.flatten()
- json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
+ render(conn, "index.json", %{
+ configs: merged,
+ need_reboot: Restarter.Pleroma.need_reboot?()
+ })
end
end
@@ -91,24 +98,17 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
{deleted, updated} =
results
- |> Enum.map(fn {:ok, config} ->
- Map.put(config, :db, ConfigDB.get_db_keys(config))
- end)
- |> Enum.split_with(fn config ->
- Ecto.get_meta(config, :state) == :deleted
+ |> Enum.map(fn {:ok, %{key: key, value: value} = config} ->
+ Map.put(config, :db, ConfigDB.get_db_keys(value, key))
end)
+ |> Enum.split_with(&(Ecto.get_meta(&1, :state) == :deleted))
Config.TransferTask.load_and_update_env(deleted, false)
if not Restarter.Pleroma.need_reboot?() do
changed_reboot_settings? =
(updated ++ deleted)
- |> Enum.any?(fn config ->
- group = ConfigDB.from_string(config.group)
- key = ConfigDB.from_string(config.key)
- value = ConfigDB.from_binary(config.value)
- Config.TransferTask.pleroma_need_restart?(group, key, value)
- end)
+ |> Enum.any?(&Config.TransferTask.pleroma_need_restart?(&1.group, &1.key, &1.value))
if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
end
diff --git a/lib/pleroma/web/admin_api/controllers/fallback_controller.ex b/lib/pleroma/web/admin_api/controllers/fallback_controller.ex
index 82965936d..34d90db07 100644
--- a/lib/pleroma/web/admin_api/controllers/fallback_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/fallback_controller.ex
@@ -17,6 +17,12 @@ defmodule Pleroma.Web.AdminAPI.FallbackController do
|> json(%{error: reason})
end
+ def call(conn, {:errors, errors}) do
+ conn
+ |> put_status(:bad_request)
+ |> json(%{errors: errors})
+ end
+
def call(conn, {:param_cast, _}) do
conn
|> put_status(:bad_request)
diff --git a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex
new file mode 100644
index 000000000..e2759d59f
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex
@@ -0,0 +1,63 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.ApiSpec.Admin, as: Spec
+ alias Pleroma.Web.MediaProxy
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:media_proxy_caches"], admin: true} when action in [:index]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:media_proxy_caches"], admin: true} when action in [:purge, :delete]
+ )
+
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Spec.MediaProxyCacheOperation
+
+ def index(%{assigns: %{user: _}} = conn, params) do
+ cursor =
+ :banned_urls_cache
+ |> :ets.table([{:traverse, {:select, Cachex.Query.create(true, :key)}}])
+ |> :qlc.cursor()
+
+ urls =
+ case params.page do
+ 1 ->
+ :qlc.next_answers(cursor, params.page_size)
+
+ _ ->
+ :qlc.next_answers(cursor, (params.page - 1) * params.page_size)
+ :qlc.next_answers(cursor, params.page_size)
+ end
+
+ :qlc.delete_cursor(cursor)
+
+ render(conn, "index.json", urls: urls)
+ end
+
+ def delete(%{assigns: %{user: _}, body_params: %{urls: urls}} = conn, _) do
+ MediaProxy.remove_from_banned_urls(urls)
+ render(conn, "index.json", urls: urls)
+ end
+
+ def purge(%{assigns: %{user: _}, body_params: %{urls: urls, ban: ban}} = conn, _) do
+ MediaProxy.Invalidation.purge(urls)
+
+ if ban do
+ MediaProxy.put_in_banned_urls(urls)
+ end
+
+ render(conn, "index.json", urls: urls)
+ end
+end
diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex
index 120159527..e1e929632 100644
--- a/lib/pleroma/web/admin_api/views/account_view.ex
+++ b/lib/pleroma/web/admin_api/views/account_view.ex
@@ -76,7 +76,8 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
"local" => user.local,
"roles" => User.roles(user),
"tags" => user.tags || [],
- "confirmation_pending" => user.confirmation_pending
+ "confirmation_pending" => user.confirmation_pending,
+ "url" => user.uri || user.ap_id
}
end
diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex
index 587ef760e..d2d8b5907 100644
--- a/lib/pleroma/web/admin_api/views/config_view.ex
+++ b/lib/pleroma/web/admin_api/views/config_view.ex
@@ -5,23 +5,20 @@
defmodule Pleroma.Web.AdminAPI.ConfigView do
use Pleroma.Web, :view
+ alias Pleroma.ConfigDB
+
def render("index.json", %{configs: configs} = params) do
- map = %{
- configs: render_many(configs, __MODULE__, "show.json", as: :config)
+ %{
+ configs: render_many(configs, __MODULE__, "show.json", as: :config),
+ need_reboot: params[:need_reboot]
}
-
- if params[:need_reboot] do
- Map.put(map, :need_reboot, true)
- else
- map
- end
end
def render("show.json", %{config: config}) do
map = %{
- key: config.key,
- group: config.group,
- value: Pleroma.ConfigDB.from_binary_with_convert(config.value)
+ key: ConfigDB.to_json_types(config.key),
+ group: ConfigDB.to_json_types(config.group),
+ value: ConfigDB.to_json_types(config.value)
}
if config.db != [] do
diff --git a/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex b/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex
new file mode 100644
index 000000000..c97400beb
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/media_proxy_cache_view.ex
@@ -0,0 +1,11 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.MediaProxyCacheView do
+ use Pleroma.Web, :view
+
+ def render("index.json", %{urls: urls}) do
+ %{urls: urls}
+ end
+end
diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex
index a9cfe0fed..a258e8421 100644
--- a/lib/pleroma/web/api_spec/helpers.ex
+++ b/lib/pleroma/web/api_spec/helpers.ex
@@ -40,6 +40,12 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
"Return the newest items newer than this ID"
),
Operation.parameter(
+ :offset,
+ :query,
+ %Schema{type: :integer, default: 0},
+ "Return items past this number of items"
+ ),
+ Operation.parameter(
:limit,
:query,
%Schema{type: :integer, default: 20},
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index 20572f8ea..9bde8fc0d 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -102,6 +102,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Account", "application/json", Account),
+ 401 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
@@ -142,6 +143,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
] ++ pagination_params(),
responses: %{
200 => Operation.response("Statuses", "application/json", array_of_statuses()),
+ 401 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
diff --git a/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex b/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex
new file mode 100644
index 000000000..0358cfbad
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex
@@ -0,0 +1,109 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.MediaProxyCacheOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ 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: ["Admin", "MediaProxyCache"],
+ summary: "Fetch a paginated list of all banned MediaProxy URLs in Cachex",
+ operationId: "AdminAPI.MediaProxyCacheController.index",
+ security: [%{"oAuth" => ["read:media_proxy_caches"]}],
+ parameters: [
+ Operation.parameter(
+ :page,
+ :query,
+ %Schema{type: :integer, default: 1},
+ "Page"
+ ),
+ Operation.parameter(
+ :page_size,
+ :query,
+ %Schema{type: :integer, default: 50},
+ "Number of statuses to return"
+ )
+ ],
+ responses: %{
+ 200 => success_response()
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Admin", "MediaProxyCache"],
+ summary: "Remove a banned MediaProxy URL from Cachex",
+ operationId: "AdminAPI.MediaProxyCacheController.delete",
+ security: [%{"oAuth" => ["write:media_proxy_caches"]}],
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ required: [:urls],
+ properties: %{
+ urls: %Schema{type: :array, items: %Schema{type: :string, format: :uri}}
+ }
+ },
+ required: true
+ ),
+ responses: %{
+ 200 => success_response(),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def purge_operation do
+ %Operation{
+ tags: ["Admin", "MediaProxyCache"],
+ summary: "Purge and optionally ban a MediaProxy URL",
+ operationId: "AdminAPI.MediaProxyCacheController.purge",
+ security: [%{"oAuth" => ["write:media_proxy_caches"]}],
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ required: [:urls],
+ properties: %{
+ urls: %Schema{type: :array, items: %Schema{type: :string, format: :uri}},
+ ban: %Schema{type: :boolean, default: true}
+ }
+ },
+ required: true
+ ),
+ responses: %{
+ 200 => success_response(),
+ 400 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp success_response do
+ Operation.response("Array of banned MediaProxy URLs in Cachex", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ urls: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :string,
+ format: :uri,
+ description: "MediaProxy URLs"
+ }
+ }
+ }
+ })
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex
new file mode 100644
index 000000000..cf299bfc2
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex
@@ -0,0 +1,355 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.ChatOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.Chat
+ alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ @spec open_api_operation(atom) :: Operation.t()
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def mark_as_read_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Mark all messages in the chat as read",
+ operationId: "ChatController.mark_as_read",
+ parameters: [Operation.parameter(:id, :path, :string, "The ID of the Chat")],
+ requestBody: request_body("Parameters", mark_as_read()),
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The updated chat",
+ "application/json",
+ Chat
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def mark_message_as_read_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Mark one message in the chat as read",
+ operationId: "ChatController.mark_message_as_read",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "The ID of the Chat"),
+ Operation.parameter(:message_id, :path, :string, "The ID of the message")
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The read ChatMessage",
+ "application/json",
+ ChatMessage
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Create a chat",
+ operationId: "ChatController.show",
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "The id of the chat",
+ required: true,
+ example: "1234"
+ )
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The existing chat",
+ "application/json",
+ Chat
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["read"]
+ }
+ ]
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Create a chat",
+ operationId: "ChatController.create",
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "The account id of the recipient of this chat",
+ required: true,
+ example: "someflakeid"
+ )
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The created or existing chat",
+ "application/json",
+ Chat
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Get a list of chats that you participated in",
+ operationId: "ChatController.index",
+ parameters: pagination_params(),
+ responses: %{
+ 200 => Operation.response("The chats of the user", "application/json", chats_response())
+ },
+ security: [
+ %{
+ "oAuth" => ["read:chats"]
+ }
+ ]
+ }
+ end
+
+ def messages_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Get the most recent messages of the chat",
+ operationId: "ChatController.messages",
+ parameters:
+ [Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++
+ pagination_params(),
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The messages in the chat",
+ "application/json",
+ chat_messages_response()
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["read:chats"]
+ }
+ ]
+ }
+ end
+
+ def post_chat_message_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Post a message to the chat",
+ operationId: "ChatController.post_chat_message",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "The ID of the Chat")
+ ],
+ requestBody: request_body("Parameters", chat_message_create()),
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The newly created ChatMessage",
+ "application/json",
+ ChatMessage
+ ),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def delete_message_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "delete_message",
+ operationId: "ChatController.delete_message",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "The ID of the Chat"),
+ Operation.parameter(:message_id, :path, :string, "The ID of the message")
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The deleted ChatMessage",
+ "application/json",
+ ChatMessage
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def chats_response do
+ %Schema{
+ title: "ChatsResponse",
+ description: "Response schema for multiple Chats",
+ type: :array,
+ items: Chat,
+ example: [
+ %{
+ "account" => %{
+ "pleroma" => %{
+ "is_admin" => false,
+ "confirmation_pending" => false,
+ "hide_followers_count" => false,
+ "is_moderator" => false,
+ "hide_favorites" => true,
+ "ap_id" => "https://dontbulling.me/users/lain",
+ "hide_follows_count" => false,
+ "hide_follows" => false,
+ "background_image" => nil,
+ "skip_thread_containment" => false,
+ "hide_followers" => false,
+ "relationship" => %{},
+ "tags" => []
+ },
+ "avatar" =>
+ "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+ "following_count" => 0,
+ "header_static" => "https://originalpatchou.li/images/banner.png",
+ "source" => %{
+ "sensitive" => false,
+ "note" => "lain",
+ "pleroma" => %{
+ "discoverable" => false,
+ "actor_type" => "Person"
+ },
+ "fields" => []
+ },
+ "statuses_count" => 1,
+ "locked" => false,
+ "created_at" => "2020-04-16T13:40:15.000Z",
+ "display_name" => "lain",
+ "fields" => [],
+ "acct" => "lain@dontbulling.me",
+ "id" => "9u6Qw6TAZANpqokMkK",
+ "emojis" => [],
+ "avatar_static" =>
+ "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+ "username" => "lain",
+ "followers_count" => 0,
+ "header" => "https://originalpatchou.li/images/banner.png",
+ "bot" => false,
+ "note" => "lain",
+ "url" => "https://dontbulling.me/users/lain"
+ },
+ "id" => "1",
+ "unread" => 2
+ }
+ ]
+ }
+ end
+
+ def chat_messages_response do
+ %Schema{
+ title: "ChatMessagesResponse",
+ description: "Response schema for multiple ChatMessages",
+ type: :array,
+ items: ChatMessage,
+ example: [
+ %{
+ "emojis" => [
+ %{
+ "static_url" => "https://dontbulling.me/emoji/Firefox.gif",
+ "visible_in_picker" => false,
+ "shortcode" => "firefox",
+ "url" => "https://dontbulling.me/emoji/Firefox.gif"
+ }
+ ],
+ "created_at" => "2020-04-21T15:11:46.000Z",
+ "content" => "Check this out :firefox:",
+ "id" => "13",
+ "chat_id" => "1",
+ "actor_id" => "someflakeid",
+ "unread" => false
+ },
+ %{
+ "actor_id" => "someflakeid",
+ "content" => "Whats' up?",
+ "id" => "12",
+ "chat_id" => "1",
+ "emojis" => [],
+ "created_at" => "2020-04-21T15:06:45.000Z",
+ "unread" => false
+ }
+ ]
+ }
+ end
+
+ def chat_message_create do
+ %Schema{
+ title: "ChatMessageCreateRequest",
+ description: "POST body for creating an chat message",
+ type: :object,
+ properties: %{
+ content: %Schema{
+ type: :string,
+ description: "The content of your message. Optional if media_id is present"
+ },
+ media_id: %Schema{type: :string, description: "The id of an upload"}
+ },
+ example: %{
+ "content" => "Hey wanna buy feet pics?",
+ "media_id" => "134234"
+ }
+ }
+ end
+
+ def mark_as_read do
+ %Schema{
+ title: "MarkAsReadRequest",
+ description: "POST body for marking a number of chat messages as read",
+ type: :object,
+ required: [:last_read_id],
+ properties: %{
+ last_read_id: %Schema{
+ type: :string,
+ description: "The content of your message."
+ }
+ },
+ example: %{
+ "last_read_id" => "abcdef12456"
+ }
+ }
+ 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 46e72f8bf..f09be64cb 100644
--- a/lib/pleroma/web/api_spec/operations/notification_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -163,6 +163,13 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
description:
"Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.",
nullable: true
+ },
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ is_seen: %Schema{type: :boolean},
+ is_muted: %Schema{type: :boolean}
+ }
}
},
example: %{
@@ -170,7 +177,8 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
"type" => "mention",
"created_at" => "2019-11-23T07:49:02.064Z",
"account" => Account.schema().example,
- "status" => Status.schema().example
+ "status" => Status.schema().example,
+ "pleroma" => %{"is_seen" => false, "is_muted" => false}
}
}
end
@@ -183,8 +191,8 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
"favourite",
"reblog",
"mention",
- "poll",
"pleroma:emoji_reaction",
+ "pleroma:chat_mention",
"move",
"follow_request"
],
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
index 567688ff5..b2b4f8713 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
@@ -33,6 +33,20 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
tags: ["Emoji Packs"],
summary: "Lists local custom emoji packs",
operationId: "PleromaAPI.EmojiPackController.index",
+ parameters: [
+ Operation.parameter(
+ :page,
+ :query,
+ %Schema{type: :integer, default: 1},
+ "Page"
+ ),
+ Operation.parameter(
+ :page_size,
+ :query,
+ %Schema{type: :integer, default: 50},
+ "Number of emoji packs to return"
+ )
+ ],
responses: %{
200 => emoji_packs_response()
}
@@ -44,7 +58,21 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
tags: ["Emoji Packs"],
summary: "Show emoji pack",
operationId: "PleromaAPI.EmojiPackController.show",
- parameters: [name_param()],
+ parameters: [
+ name_param(),
+ Operation.parameter(
+ :page,
+ :query,
+ %Schema{type: :integer, default: 1},
+ "Page"
+ ),
+ Operation.parameter(
+ :page_size,
+ :query,
+ %Schema{type: :integer, default: 30},
+ "Number of emoji to return"
+ )
+ ],
responses: %{
200 => Operation.response("Emoji Pack", "application/json", emoji_pack()),
400 => Operation.response("Bad Request", "application/json", ApiError),
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
index ca9db01e5..0b7fad793 100644
--- a/lib/pleroma/web/api_spec/operations/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -333,7 +333,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
%Operation{
tags: ["Statuses"],
summary: "Favourited statuses",
- description: "Statuses the user has favourited",
+ description:
+ "Statuses the user has favourited. Please note that you have to use the link headers to paginate this. You can not build the query parameters yourself.",
operationId: "StatusController.favourites",
parameters: pagination_params(),
security: [%{"oAuth" => ["read:favourites"]}],
diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
index c575a87e6..775dd795d 100644
--- a/lib/pleroma/web/api_spec/operations/subscription_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
@@ -141,6 +141,11 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
allOf: [BooleanLike],
nullable: true,
description: "Receive poll notifications?"
+ },
+ "pleroma:chat_mention": %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive chat notifications?"
}
}
}
diff --git a/lib/pleroma/web/api_spec/schemas/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex
new file mode 100644
index 000000000..b4986b734
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/chat.ex
@@ -0,0 +1,75 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Chat",
+ description: "Response schema for a Chat",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ account: %Schema{type: :object},
+ unread: %Schema{type: :integer},
+ last_message: ChatMessage,
+ updated_at: %Schema{type: :string, format: :"date-time"}
+ },
+ example: %{
+ "account" => %{
+ "pleroma" => %{
+ "is_admin" => false,
+ "confirmation_pending" => false,
+ "hide_followers_count" => false,
+ "is_moderator" => false,
+ "hide_favorites" => true,
+ "ap_id" => "https://dontbulling.me/users/lain",
+ "hide_follows_count" => false,
+ "hide_follows" => false,
+ "background_image" => nil,
+ "skip_thread_containment" => false,
+ "hide_followers" => false,
+ "relationship" => %{},
+ "tags" => []
+ },
+ "avatar" =>
+ "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+ "following_count" => 0,
+ "header_static" => "https://originalpatchou.li/images/banner.png",
+ "source" => %{
+ "sensitive" => false,
+ "note" => "lain",
+ "pleroma" => %{
+ "discoverable" => false,
+ "actor_type" => "Person"
+ },
+ "fields" => []
+ },
+ "statuses_count" => 1,
+ "locked" => false,
+ "created_at" => "2020-04-16T13:40:15.000Z",
+ "display_name" => "lain",
+ "fields" => [],
+ "acct" => "lain@dontbulling.me",
+ "id" => "9u6Qw6TAZANpqokMkK",
+ "emojis" => [],
+ "avatar_static" =>
+ "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+ "username" => "lain",
+ "followers_count" => 0,
+ "header" => "https://originalpatchou.li/images/banner.png",
+ "bot" => false,
+ "note" => "lain",
+ "url" => "https://dontbulling.me/users/lain"
+ },
+ "id" => "1",
+ "unread" => 2,
+ "last_message" => ChatMessage.schema().example(),
+ "updated_at" => "2020-04-21T15:06:45.000Z"
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/chat_message.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex
new file mode 100644
index 000000000..3ee85aa76
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "ChatMessage",
+ description: "Response schema for a ChatMessage",
+ nullable: true,
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ account_id: %Schema{type: :string, description: "The Mastodon API id of the actor"},
+ chat_id: %Schema{type: :string},
+ content: %Schema{type: :string, nullable: true},
+ created_at: %Schema{type: :string, format: :"date-time"},
+ emojis: %Schema{type: :array},
+ attachment: %Schema{type: :object, nullable: true}
+ },
+ example: %{
+ "account_id" => "someflakeid",
+ "chat_id" => "1",
+ "content" => "hey you again",
+ "created_at" => "2020-04-21T15:06:45.000Z",
+ "emojis" => [
+ %{
+ "static_url" => "https://dontbulling.me/emoji/Firefox.gif",
+ "visible_in_picker" => false,
+ "shortcode" => "firefox",
+ "url" => "https://dontbulling.me/emoji/Firefox.gif"
+ }
+ ],
+ "id" => "14",
+ "attachment" => nil
+ }
+ })
+end
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 3f1a50b96..9bcb9f587 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -197,6 +197,13 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
defp changes(draft) do
direct? = draft.visibility == "direct"
+ additional = %{"cc" => draft.cc, "directMessage" => direct?}
+
+ additional =
+ case draft.expires_at do
+ %NaiveDateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at)
+ _ -> additional
+ end
changes =
%{
@@ -204,7 +211,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
actor: draft.user,
context: draft.context,
object: draft.object,
- additional: %{"cc" => draft.cc, "directMessage" => direct?}
+ additional: additional
}
|> Utils.maybe_add_list_data(draft.user, draft.visibility)
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index dbb3d7ade..04e081a8e 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
alias Pleroma.FollowingRelationship
+ alias Pleroma.Formatter
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.ThreadMute
@@ -24,6 +25,53 @@ defmodule Pleroma.Web.CommonAPI do
require Pleroma.Constants
require Logger
+ def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
+ with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
+ :ok <- validate_chat_content_length(content, !!maybe_attachment),
+ {_, {:ok, chat_message_data, _meta}} <-
+ {:build_object,
+ Builder.chat_message(
+ user,
+ recipient.ap_id,
+ content |> format_chat_content,
+ attachment: maybe_attachment
+ )},
+ {_, {:ok, create_activity_data, _meta}} <-
+ {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
+ {_, {:ok, %Activity{} = activity, _meta}} <-
+ {:common_pipeline,
+ Pipeline.common_pipeline(create_activity_data,
+ local: true
+ )} do
+ {:ok, activity}
+ end
+ end
+
+ defp format_chat_content(nil), do: nil
+
+ defp format_chat_content(content) do
+ {text, _, _} =
+ content
+ |> Formatter.html_escape("text/plain")
+ |> Formatter.linkify()
+ |> (fn {text, mentions, tags} ->
+ {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
+ end).()
+
+ text
+ end
+
+ defp validate_chat_content_length(_, true), do: :ok
+ defp validate_chat_content_length(nil, false), do: {:error, :no_content}
+
+ defp validate_chat_content_length(content, _) do
+ if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
+ :ok
+ else
+ {:error, :content_too_long}
+ end
+ end
+
def unblock(blocker, blocked) do
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
{:ok, unblock_data, _} <- Builder.undo(blocker, block),
@@ -73,6 +121,7 @@ defmodule Pleroma.Web.CommonAPI do
object: follow_activity.data["id"],
type: "Accept"
}) do
+ Notification.update_notification_type(followed, follow_activity)
{:ok, follower}
end
end
@@ -374,20 +423,10 @@ defmodule Pleroma.Web.CommonAPI do
def post(user, %{status: _} = data) do
with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
- draft.changes
- |> ActivityPub.create(draft.preview?)
- |> maybe_create_activity_expiration(draft.expires_at)
+ ActivityPub.create(draft.changes, draft.preview?)
end
end
- defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
- with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
- {:ok, activity}
- end
- end
-
- defp maybe_create_activity_expiration(result, _), do: result
-
def pin(id, %{ap_id: user_ap_id} = user) do
with %Activity{
actor: ^user_ap_id,
@@ -427,12 +466,13 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, activity}
end
- def thread_muted?(%{id: nil} = _user, _activity), do: false
-
- def thread_muted?(user, activity) do
- ThreadMute.exists?(user.id, activity.data["context"])
+ def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
+ when is_binary("context") do
+ ThreadMute.exists?(user_id, context)
end
+ def thread_muted?(_, _), do: false
+
def report(user, data) do
with {:ok, account} <- get_reported_account(data.account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 6ec489f9a..15594125f 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -429,7 +429,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
)
when type == "Create" do
- object = Object.normalize(activity)
+ object = Object.normalize(activity, false)
object_data =
cond do
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 5d67d75b5..69946fb81 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -57,35 +57,36 @@ defmodule Pleroma.Web.ControllerHelper do
end
end
+ @id_keys Pagination.page_keys() -- ["limit", "order"]
+ defp build_pagination_fields(conn, min_id, max_id, extra_params) do
+ params =
+ conn.params
+ |> Map.drop(Map.keys(conn.path_params))
+ |> Map.merge(extra_params)
+ |> Map.drop(@id_keys)
+
+ %{
+ "next" => current_url(conn, Map.put(params, :max_id, max_id)),
+ "prev" => current_url(conn, Map.put(params, :min_id, min_id)),
+ "id" => current_url(conn)
+ }
+ end
+
def get_pagination_fields(conn, activities, extra_params \\ %{}) do
case List.last(activities) do
- %{id: max_id} ->
- params =
- conn.params
- |> Map.drop(Map.keys(conn.path_params))
- |> Map.merge(extra_params)
- |> Map.drop(Pagination.page_keys() -- ["limit", "order"])
+ %{pagination_id: max_id} when not is_nil(max_id) ->
+ %{pagination_id: min_id} =
+ activities
+ |> List.first()
+
+ build_pagination_fields(conn, min_id, max_id, extra_params)
- min_id =
+ %{id: max_id} ->
+ %{id: min_id} =
activities
|> List.first()
- |> Map.get(:id)
-
- fields = %{
- "next" => current_url(conn, Map.put(params, :max_id, max_id)),
- "prev" => current_url(conn, Map.put(params, :min_id, min_id))
- }
-
- # Generating an `id` without already present pagination keys would
- # need a query-restriction with an `q.id >= ^id` or `q.id <= ^id`
- # instead of the `q.id > ^min_id` and `q.id < ^max_id`.
- # This is because we only have ids present inside of the page, while
- # `min_id`, `since_id` and `max_id` requires to know one outside of it.
- if Map.take(conn.params, Pagination.page_keys() -- ["limit", "order"]) != [] do
- Map.put(fields, "id", current_url(conn, conn.params))
- else
- fields
- end
+
+ build_pagination_fields(conn, min_id, max_id, extra_params)
_ ->
%{}
diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex
index d0d8bc8eb..43ec70021 100644
--- a/lib/pleroma/web/masto_fe_controller.ex
+++ b/lib/pleroma/web/masto_fe_controller.ex
@@ -49,7 +49,7 @@ defmodule Pleroma.Web.MastoFEController do
|> render("manifest.json")
end
- @doc "PUT /api/web/settings"
+ @doc "PUT /api/web/settings: Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere"
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
with {:ok, _} <- User.mastodon_settings_update(user, settings) do
json(conn, %{})
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 7cdd8f458..d50e7c5dd 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -165,6 +165,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end)
|> Maps.put_if_present(:name, params[:display_name])
|> Maps.put_if_present(:bio, params[:note])
+ |> Maps.put_if_present(:raw_bio, params[:note])
|> Maps.put_if_present(:avatar, params[:avatar])
|> Maps.put_if_present(:banner, params[:header])
|> Maps.put_if_present(:background, params[:pleroma_background_image])
@@ -176,6 +177,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
|> Maps.put_if_present(:default_scope, params[:default_scope])
|> Maps.put_if_present(:default_scope, params["source"]["privacy"])
+ |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
+ if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
+ end)
|> Maps.put_if_present(:actor_type, params[:actor_type])
changeset = User.update_changeset(user, user_params)
@@ -230,17 +234,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "GET /api/v1/accounts/:id"
def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
- true <- User.visible_for?(user, for_user) do
+ :visible <- User.visible_for(user, for_user) do
render(conn, "show.json", user: user, for: for_user)
else
- _e -> render_error(conn, :not_found, "Can't find user")
+ error -> user_visibility_error(conn, error)
end
end
@doc "GET /api/v1/accounts/:id/statuses"
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
- true <- User.visible_for?(user, reading_user) do
+ :visible <- User.visible_for(user, reading_user) do
params =
params
|> Map.delete(:tagged)
@@ -257,7 +261,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
as: :activity
)
else
- _e -> render_error(conn, :not_found, "Can't find user")
+ error -> user_visibility_error(conn, error)
+ end
+ end
+
+ defp user_visibility_error(conn, error) do
+ case error do
+ :restrict_unauthenticated ->
+ render_error(conn, :unauthorized, "This API requires an authenticated user")
+
+ _ ->
+ render_error(conn, :not_found, "Can't find user")
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 bcd12c73f..e25cef30b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -42,8 +42,20 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
end
+ @default_notification_types ~w{
+ mention
+ follow
+ follow_request
+ reblog
+ favourite
+ move
+ pleroma:emoji_reaction
+ }
def index(%{assigns: %{user: user}} = conn, params) do
- params = Map.new(params, fn {k, v} -> {to_string(k), v} end)
+ params =
+ Map.new(params, fn {k, v} -> {to_string(k), v} end)
+ |> Map.put_new("include_types", @default_notification_types)
+
notifications = MastodonAPI.get_notifications(user, params)
conn
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index 8840fc19c..e50980122 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -107,23 +107,24 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
)
end
- defp resource_search(:v2, "hashtags", query, _options) do
+ defp resource_search(:v2, "hashtags", query, options) do
tags_path = Web.base_url() <> "/tag/"
query
- |> prepare_tags()
+ |> prepare_tags(options)
|> Enum.map(fn tag ->
%{name: tag, url: tags_path <> tag}
end)
end
- defp resource_search(:v1, "hashtags", query, _options) do
- prepare_tags(query)
+ defp resource_search(:v1, "hashtags", query, options) do
+ prepare_tags(query, options)
end
- defp prepare_tags(query, add_joined_tag \\ true) do
+ defp prepare_tags(query, options) do
tags =
query
+ |> preprocess_uri_query()
|> String.split(~r/[^#\w]+/u, trim: true)
|> Enum.uniq_by(&String.downcase/1)
@@ -138,12 +139,33 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)
- if Enum.empty?(explicit_tags) && add_joined_tag do
- tags
- |> Kernel.++([joined_tag(tags)])
- |> Enum.uniq_by(&String.downcase/1)
+ tags =
+ if Enum.empty?(explicit_tags) && !options[:skip_joined_tag] do
+ add_joined_tag(tags)
+ else
+ tags
+ end
+
+ Pleroma.Pagination.paginate(tags, options)
+ end
+
+ defp add_joined_tag(tags) do
+ tags
+ |> Kernel.++([joined_tag(tags)])
+ |> Enum.uniq_by(&String.downcase/1)
+ end
+
+ # If `query` is a URI, returns last component of its path, otherwise returns `query`
+ defp preprocess_uri_query(query) do
+ if query =~ ~r/https?:\/\// do
+ query
+ |> String.trim_trailing("/")
+ |> URI.parse()
+ |> Map.get(:path)
+ |> String.split("/")
+ |> Enum.at(-1)
else
- tags
+ query
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index 9270ca267..4bdd46d7e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -48,6 +48,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:blocking_user, user)
|> Map.put(:muting_user, user)
|> Map.put(:reply_filtering_user, user)
+ |> Map.put(:announce_filtering_user, user)
|> Map.put(:user, user)
activities =
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
index 70da64a7a..694bf5ca8 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
import Ecto.Query
import Ecto.Changeset
- alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Pagination
alias Pleroma.ScheduledActivity
@@ -82,15 +81,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
end
defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do
- ap_types = convert_and_filter_mastodon_types(mastodon_types)
-
- where(query, [q, a], fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
+ where(query, [n], n.type in ^mastodon_types)
end
defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
- ap_types = convert_and_filter_mastodon_types(mastodon_types)
-
- where(query, [q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
+ where(query, [n], n.type not in ^mastodon_types)
end
defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do
@@ -98,10 +93,4 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
end
defp restrict(query, _, _), do: query
-
- defp convert_and_filter_mastodon_types(types) do
- types
- |> Enum.map(&Activity.from_mastodon_notification_type/1)
- |> Enum.filter(& &1)
- end
end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 04c419d2f..a6e64b4ab 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -35,7 +35,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end
def render("show.json", %{user: user} = opts) do
- if User.visible_for?(user, opts[:for]) do
+ if User.visible_for(user, opts[:for]) == :visible do
do_render("show.json", opts)
else
%{}
@@ -179,7 +179,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
0
end
- bot = user.actor_type in ["Application", "Service"]
+ bot = user.actor_type == "Service"
emojis =
Enum.map(user.emoji, fn {shortcode, raw_url} ->
@@ -224,7 +224,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
fields: user.fields,
bot: bot,
source: %{
- note: prepare_user_bio(user),
+ note: user.raw_bio || "",
sensitive: false,
fields: user.raw_fields,
pleroma: %{
@@ -235,6 +235,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
# Pleroma extension
pleroma: %{
+ ap_id: user.ap_id,
confirmation_pending: user.confirmation_pending,
tags: user.tags,
hide_followers_count: user.hide_followers_count,
@@ -259,17 +260,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_unread_notification_count(user, opts[:for])
end
- defp prepare_user_bio(%User{bio: ""}), do: ""
-
- defp prepare_user_bio(%User{bio: bio}) when is_binary(bio) do
- bio
- |> String.replace(~r(<br */?>), "\n")
- |> Pleroma.HTML.strip_tags()
- |> HtmlEntities.decode()
- end
-
- defp prepare_user_bio(_), do: ""
-
defp username_from_nickname(string) when is_binary(string) do
hd(String.split(string, "@"))
end
diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
index fbe618377..06f0c1728 100644
--- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
@@ -23,10 +23,13 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
last_activity_id =
with nil <- participation.last_activity_id do
- ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
- user: user,
- blocking_user: user
- })
+ ActivityPub.fetch_latest_direct_activity_id_for_context(
+ participation.conversation.ap_id,
+ %{
+ user: user,
+ blocking_user: user
+ }
+ )
end
activity = Activity.get_by_id_with_object(last_activity_id)
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 6a630eafa..35c2fc25c 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
streaming_api: Pleroma.Web.Endpoint.websocket_url()
},
stats: Pleroma.Stats.get_stats(),
- thumbnail: instance_thumbnail(),
+ thumbnail: Keyword.get(instance, :instance_thumbnail),
languages: ["en"],
registrations: Keyword.get(instance, :registrations_open),
# Extra (not present in Mastodon):
@@ -69,7 +69,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
if Config.get([:instance, :safe_dm_mentions]) do
"safe_dm_mentions"
end,
- "pleroma_emoji_reactions"
+ "pleroma_emoji_reactions",
+ "pleroma_chat_messages"
]
|> Enum.filter(& &1)
end
@@ -77,7 +78,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
def federation do
quarantined = Config.get([:instance, :quarantined_instances], [])
- if Config.get([:instance, :mrf_transparency]) do
+ if Config.get([:mrf, :transparency]) do
{:ok, data} = MRF.describe()
data
@@ -87,9 +88,4 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
end
|> Map.put(:enabled, Config.get([:instance, :federating]))
end
-
- defp instance_thumbnail do
- Pleroma.Config.get([:instance, :instance_thumbnail]) ||
- "#{Pleroma.Web.base_url()}/instance/thumbnail.jpeg"
- end
end
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index c46ddcf55..c97e6d32f 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -6,26 +6,28 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
use Pleroma.Web, :view
alias Pleroma.Activity
+ alias Pleroma.Chat.MessageReference
alias Pleroma.Notification
+ alias Pleroma.Object
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+
+ @parent_types ~w{Like Announce EmojiReact}
def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
activities = Enum.map(notifications, & &1.activity)
parent_activities =
activities
- |> Enum.filter(
- &(Activity.mastodon_notification_type(&1) in [
- "favourite",
- "reblog",
- "pleroma:emoji_reaction"
- ])
- )
+ |> Enum.filter(fn
+ %{data: %{"type" => type}} ->
+ type in @parent_types
+ end)
|> Enum.map(& &1.data["object"])
|> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left)
@@ -42,8 +44,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
true ->
move_activities_targets =
activities
- |> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move"))
+ |> Enum.filter(&(&1.data["type"] == "Move"))
|> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
+ |> Enum.filter(& &1)
actors =
activities
@@ -79,52 +82,44 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
end
end
- mastodon_type = Activity.mastodon_notification_type(activity)
-
# Note: :relationships contain user mutes (needed for :muted flag in :status)
status_render_opts = %{relationships: opts[:relationships]}
-
- with %{id: _} = account <-
- AccountView.render(
- "show.json",
- %{user: actor, for: reading_user}
- ) do
- response = %{
- id: to_string(notification.id),
- type: mastodon_type,
- created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
- account: account,
- pleroma: %{
- is_seen: notification.seen
- }
+ account = AccountView.render("show.json", %{user: actor, for: reading_user})
+
+ response = %{
+ id: to_string(notification.id),
+ type: notification.type,
+ created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
+ account: account,
+ pleroma: %{
+ is_muted: User.mutes?(reading_user, actor),
+ is_seen: notification.seen
}
+ }
- case mastodon_type do
- "mention" ->
- put_status(response, activity, reading_user, status_render_opts)
+ case notification.type do
+ "mention" ->
+ put_status(response, activity, reading_user, status_render_opts)
- "favourite" ->
- put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
+ "favourite" ->
+ put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
- "reblog" ->
- put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
+ "reblog" ->
+ put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
- "move" ->
- put_target(response, activity, reading_user, %{})
+ "move" ->
+ put_target(response, activity, reading_user, %{})
- "pleroma:emoji_reaction" ->
- response
- |> put_status(parent_activity_fn.(), reading_user, status_render_opts)
- |> put_emoji(activity)
+ "pleroma:emoji_reaction" ->
+ response
+ |> put_status(parent_activity_fn.(), reading_user, status_render_opts)
+ |> put_emoji(activity)
- type when type in ["follow", "follow_request"] ->
- response
+ "pleroma:chat_mention" ->
+ put_chat_message(response, activity, reading_user, status_render_opts)
- _ ->
- nil
- end
- else
- _ -> nil
+ type when type in ["follow", "follow_request"] ->
+ response
end
end
@@ -132,6 +127,17 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
Map.put(response, :emoji, activity.data["content"])
end
+ defp put_chat_message(response, activity, reading_user, opts) do
+ object = Object.normalize(activity)
+ author = User.get_cached_by_ap_id(object.data["actor"])
+ chat = Pleroma.Chat.get(reading_user.id, author.ap_id)
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+ render_opts = Map.merge(opts, %{for: reading_user, chat_message_reference: cm_ref})
+ chat_message_render = MessageReferenceView.render("show.json", render_opts)
+
+ Map.put(response, :chat_message, chat_message_render)
+ end
+
defp put_status(response, activity, reading_user, opts) do
status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user})
status_render = StatusView.render("show.json", status_render_opts)
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 8e3715093..2c49bedb3 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -377,8 +377,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
page_url_data = URI.parse(page_url)
page_url_data =
- if rich_media[:url] != nil do
- URI.merge(page_url_data, URI.parse(rich_media[:url]))
+ if is_binary(rich_media["url"]) do
+ URI.merge(page_url_data, URI.parse(rich_media["url"]))
else
page_url_data
end
@@ -386,11 +386,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
page_url = page_url_data |> to_string
image_url =
- if rich_media[:image] != nil do
- URI.merge(page_url_data, URI.parse(rich_media[:image]))
+ if is_binary(rich_media["image"]) do
+ URI.merge(page_url_data, URI.parse(rich_media["image"]))
|> to_string
- else
- nil
end
%{
@@ -399,8 +397,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
url: page_url,
image: image_url |> MediaProxy.url(),
- title: rich_media[:title] || "",
- description: rich_media[:description] || "",
+ title: rich_media["title"] || "",
+ description: rich_media["description"] || "",
pleroma: %{
opengraph: rich_media
}
diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex
index c037ff13e..5808861e6 100644
--- a/lib/pleroma/web/media_proxy/invalidation.ex
+++ b/lib/pleroma/web/media_proxy/invalidation.ex
@@ -5,22 +5,34 @@
defmodule Pleroma.Web.MediaProxy.Invalidation do
@moduledoc false
- @callback purge(list(String.t()), map()) :: {:ok, String.t()} | {:error, String.t()}
+ @callback purge(list(String.t()), Keyword.t()) :: {:ok, list(String.t())} | {:error, String.t()}
alias Pleroma.Config
+ alias Pleroma.Web.MediaProxy
- @spec purge(list(String.t())) :: {:ok, String.t()} | {:error, String.t()}
+ @spec enabled?() :: boolean()
+ def enabled?, do: Config.get([:media_proxy, :invalidation, :enabled])
+
+ @spec purge(list(String.t()) | String.t()) :: {:ok, list(String.t())} | {:error, String.t()}
def purge(urls) do
- [:media_proxy, :invalidation, :enabled]
- |> Config.get()
- |> do_purge(urls)
+ prepared_urls = prepare_urls(urls)
+
+ if enabled?() do
+ do_purge(prepared_urls)
+ else
+ {:ok, prepared_urls}
+ end
end
- defp do_purge(true, urls) do
+ defp do_purge(urls) do
provider = Config.get([:media_proxy, :invalidation, :provider])
options = Config.get(provider)
provider.purge(urls, options)
end
- defp do_purge(_, _), do: :ok
+ def prepare_urls(urls) do
+ urls
+ |> List.wrap()
+ |> Enum.map(&MediaProxy.url/1)
+ end
end
diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex
index 07248df6e..bb81d8888 100644
--- a/lib/pleroma/web/media_proxy/invalidations/http.ex
+++ b/lib/pleroma/web/media_proxy/invalidations/http.ex
@@ -9,10 +9,10 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Http do
require Logger
@impl Pleroma.Web.MediaProxy.Invalidation
- def purge(urls, opts) do
- method = Map.get(opts, :method, :purge)
- headers = Map.get(opts, :headers, [])
- options = Map.get(opts, :options, [])
+ def purge(urls, opts \\ []) do
+ method = Keyword.get(opts, :method, :purge)
+ headers = Keyword.get(opts, :headers, [])
+ options = Keyword.get(opts, :options, [])
Logger.debug("Running cache purge: #{inspect(urls)}")
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Http do
end
end)
- {:ok, "success"}
+ {:ok, urls}
end
defp do_purge(method, url, headers, options) do
diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex
index 6be782132..d32ffc50b 100644
--- a/lib/pleroma/web/media_proxy/invalidations/script.ex
+++ b/lib/pleroma/web/media_proxy/invalidations/script.ex
@@ -10,32 +10,34 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Script do
require Logger
@impl Pleroma.Web.MediaProxy.Invalidation
- def purge(urls, %{script_path: script_path} = _options) do
+ def purge(urls, opts \\ []) do
args =
urls
|> List.wrap()
|> Enum.uniq()
|> Enum.join(" ")
- path = Path.expand(script_path)
-
- Logger.debug("Running cache purge: #{inspect(urls)}, #{path}")
-
- case do_purge(path, [args]) do
- {result, exit_status} when exit_status > 0 ->
- Logger.error("Error while cache purge: #{inspect(result)}")
- {:error, inspect(result)}
-
- _ ->
- {:ok, "success"}
- end
+ opts
+ |> Keyword.get(:script_path)
+ |> do_purge([args])
+ |> handle_result(urls)
end
- def purge(_, _), do: {:error, "not found script path"}
-
- defp do_purge(path, args) do
+ defp do_purge(script_path, args) when is_binary(script_path) do
+ path = Path.expand(script_path)
+ Logger.debug("Running cache purge: #{inspect(args)}, #{inspect(path)}")
System.cmd(path, args)
rescue
- error -> {inspect(error), 1}
+ error -> error
+ end
+
+ defp do_purge(_, _), do: {:error, "not found script path"}
+
+ defp handle_result({_result, 0}, urls), do: {:ok, urls}
+ defp handle_result({:error, error}, urls), do: handle_result(error, urls)
+
+ defp handle_result(error, _) do
+ Logger.error("Error while cache purge: #{inspect(error)}")
+ {:error, inspect(error)}
end
end
diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex
index b2b524524..077fabe47 100644
--- a/lib/pleroma/web/media_proxy/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy.ex
@@ -6,20 +6,53 @@ defmodule Pleroma.Web.MediaProxy do
alias Pleroma.Config
alias Pleroma.Upload
alias Pleroma.Web
+ alias Pleroma.Web.MediaProxy.Invalidation
@base64_opts [padding: false]
+ @spec in_banned_urls(String.t()) :: boolean()
+ def in_banned_urls(url), do: elem(Cachex.exists?(:banned_urls_cache, url(url)), 1)
+
+ def remove_from_banned_urls(urls) when is_list(urls) do
+ Cachex.execute!(:banned_urls_cache, fn cache ->
+ Enum.each(Invalidation.prepare_urls(urls), &Cachex.del(cache, &1))
+ end)
+ end
+
+ def remove_from_banned_urls(url) when is_binary(url) do
+ Cachex.del(:banned_urls_cache, url(url))
+ end
+
+ def put_in_banned_urls(urls) when is_list(urls) do
+ Cachex.execute!(:banned_urls_cache, fn cache ->
+ Enum.each(Invalidation.prepare_urls(urls), &Cachex.put(cache, &1, true))
+ end)
+ end
+
+ def put_in_banned_urls(url) when is_binary(url) do
+ Cachex.put(:banned_urls_cache, url(url), true)
+ end
+
def url(url) when is_nil(url) or url == "", do: nil
def url("/" <> _ = url), do: url
def url(url) do
- if disabled?() or local?(url) or whitelisted?(url) do
+ if disabled?() or not url_proxiable?(url) do
url
else
encode_url(url)
end
end
+ @spec url_proxiable?(String.t()) :: boolean()
+ def url_proxiable?(url) do
+ if local?(url) or whitelisted?(url) do
+ false
+ else
+ true
+ end
+ end
+
defp disabled?, do: !Config.get([:media_proxy, :enabled], false)
defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index 4657a4383..9a64b0ef3 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -14,10 +14,11 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
with config <- Pleroma.Config.get([:media_proxy], []),
true <- Keyword.get(config, :enabled, false),
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
+ {_, false} <- {:in_banned_urls, MediaProxy.in_banned_urls(url)},
:ok <- filename_matches(params, conn.request_path, url) do
ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
else
- false ->
+ error when error in [false, {:in_banned_urls, true}] ->
send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
{:error, :invalid_signature} ->
diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
new file mode 100644
index 000000000..c8ef3d915
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -0,0 +1,174 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+defmodule Pleroma.Web.PleromaAPI.ChatController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.Object
+ alias Pleroma.Pagination
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+ alias Pleroma.Web.PleromaAPI.ChatView
+
+ import Ecto.Query
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:chats"]}
+ when action in [
+ :post_chat_message,
+ :create,
+ :mark_as_read,
+ :mark_message_as_read,
+ :delete_message
+ ]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:chats"]} when action in [:messages, :index, :show]
+ )
+
+ plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation
+
+ def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
+ message_id: message_id,
+ id: chat_id
+ }) do
+ with %MessageReference{} = cm_ref <-
+ MessageReference.get_by_id(message_id),
+ ^chat_id <- cm_ref.chat_id |> to_string(),
+ %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
+ {:ok, _} <- remove_or_delete(cm_ref, user) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", chat_message_reference: cm_ref)
+ else
+ _e ->
+ {:error, :could_not_delete}
+ end
+ end
+
+ defp remove_or_delete(
+ %{object: %{data: %{"actor" => actor, "id" => id}}},
+ %{ap_id: actor} = user
+ ) do
+ with %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ CommonAPI.delete(activity.id, user)
+ end
+ end
+
+ defp remove_or_delete(cm_ref, _) do
+ cm_ref
+ |> MessageReference.delete()
+ end
+
+ def post_chat_message(
+ %{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn,
+ %{
+ id: id
+ }
+ ) do
+ with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
+ %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient),
+ {:ok, activity} <-
+ CommonAPI.post_chat_message(user, recipient, params[:content],
+ media_id: params[:media_id]
+ ),
+ message <- Object.normalize(activity, false),
+ cm_ref <- MessageReference.for_chat_and_object(chat, message) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", for: user, chat_message_reference: cm_ref)
+ end
+ end
+
+ def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
+ id: chat_id,
+ message_id: message_id
+ }) do
+ with %MessageReference{} = cm_ref <-
+ MessageReference.get_by_id(message_id),
+ ^chat_id <- cm_ref.chat_id |> to_string(),
+ %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
+ {:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", for: user, chat_message_reference: cm_ref)
+ end
+ end
+
+ def mark_as_read(
+ %{body_params: %{last_read_id: last_read_id}, assigns: %{user: %{id: user_id}}} = conn,
+ %{id: id}
+ ) do
+ with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
+ {_n, _} <-
+ MessageReference.set_all_seen_for_chat(chat, last_read_id) do
+ conn
+ |> put_view(ChatView)
+ |> render("show.json", chat: chat)
+ end
+ end
+
+ def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do
+ with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do
+ cm_refs =
+ chat
+ |> MessageReference.for_chat_query()
+ |> Pagination.fetch_paginated(params)
+
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("index.json", for: user, chat_message_references: cm_refs)
+ else
+ _ ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "not found"})
+ end
+ end
+
+ def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do
+ blocked_ap_ids = User.blocked_users_ap_ids(user)
+
+ chats =
+ from(c in Chat,
+ where: c.user_id == ^user_id,
+ where: c.recipient not in ^blocked_ap_ids,
+ order_by: [desc: c.updated_at]
+ )
+ |> Repo.all()
+
+ conn
+ |> put_view(ChatView)
+ |> render("index.json", chats: chats)
+ end
+
+ def create(%{assigns: %{user: user}} = conn, params) do
+ with %User{ap_id: recipient} <- User.get_by_id(params[:id]),
+ {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
+ conn
+ |> put_view(ChatView)
+ |> render("show.json", chat: chat)
+ end
+ end
+
+ def show(%{assigns: %{user: user}} = conn, params) do
+ with %Chat{} = chat <- Repo.get_by(Chat, user_id: user.id, id: params[:id]) do
+ conn
+ |> put_view(ChatView)
+ |> render("show.json", chat: chat)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
index d1efdeb5d..33ecd1f70 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
@@ -37,14 +37,14 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def index(conn, _params) do
+ def index(conn, params) do
emoji_path =
[:instance, :static_dir]
|> Pleroma.Config.get!()
|> Path.join("emoji")
- with {:ok, packs} <- Pack.list_local() do
- json(conn, packs)
+ with {:ok, packs, count} <- Pack.list_local(page: params.page, page_size: params.page_size) do
+ json(conn, %{packs: packs, count: count})
else
{:error, :create_dir, e} ->
conn
@@ -60,10 +60,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def show(conn, %{name: name}) do
+ def show(conn, %{name: name, page: page, page_size: page_size}) do
name = String.trim(name)
- with {:ok, pack} <- Pack.show(name) do
+ with {:ok, pack} <- Pack.show(name: name, page: page, page_size: page_size) do
json(conn, pack)
else
{:error, :not_found} ->
diff --git a/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
new file mode 100644
index 000000000..f2112a86e
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def render(
+ "show.json",
+ %{
+ chat_message_reference: %{
+ id: id,
+ object: %{data: chat_message},
+ chat_id: chat_id,
+ unread: unread
+ }
+ }
+ ) do
+ %{
+ id: id |> to_string(),
+ content: chat_message["content"],
+ chat_id: chat_id |> to_string(),
+ account_id: User.get_cached_by_ap_id(chat_message["actor"]).id,
+ created_at: Utils.to_masto_date(chat_message["published"]),
+ emojis: StatusView.build_emojis(chat_message["emoji"]),
+ attachment:
+ chat_message["attachment"] &&
+ StatusView.render("attachment.json", attachment: chat_message["attachment"]),
+ unread: unread
+ }
+ end
+
+ def render("index.json", opts) do
+ render_many(
+ opts[:chat_message_references],
+ __MODULE__,
+ "show.json",
+ Map.put(opts, :as, :chat_message_reference)
+ )
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex
new file mode 100644
index 000000000..1c996da11
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ChatView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+
+ def render("show.json", %{chat: %Chat{} = chat} = opts) do
+ recipient = User.get_cached_by_ap_id(chat.recipient)
+ last_message = opts[:last_message] || MessageReference.last_message_for_chat(chat)
+
+ %{
+ id: chat.id |> to_string(),
+ account: AccountView.render("show.json", Map.put(opts, :user, recipient)),
+ unread: MessageReference.unread_count_for_chat(chat),
+ last_message:
+ last_message &&
+ MessageReferenceView.render("show.json", chat_message_reference: last_message),
+ updated_at: Utils.to_masto_date(chat.updated_at)
+ }
+ end
+
+ def render("index.json", %{chats: chats}) do
+ render_many(chats, __MODULE__, "show.json")
+ end
+end
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index 691725702..cdb827e76 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -16,8 +16,6 @@ defmodule Pleroma.Web.Push.Impl do
require Logger
import Ecto.Query
- defdelegate mastodon_notification_type(activity), to: Activity
-
@types ["Create", "Follow", "Announce", "Like", "Move"]
@doc "Performs sending notifications for user subscriptions"
@@ -31,10 +29,10 @@ defmodule Pleroma.Web.Push.Impl do
when activity_type in @types do
actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
- mastodon_type = mastodon_notification_type(notification.activity)
+ mastodon_type = notification.type
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
- object = Object.normalize(activity)
+ object = Object.normalize(activity, false)
user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user)
@@ -116,7 +114,7 @@ defmodule Pleroma.Web.Push.Impl do
end
def build_content(notification, actor, object, mastodon_type) do
- mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+ mastodon_type = mastodon_type || notification.type
%{
title: format_title(notification, mastodon_type),
@@ -126,6 +124,13 @@ defmodule Pleroma.Web.Push.Impl do
def format_body(activity, actor, object, mastodon_type \\ nil)
+ def format_body(_activity, actor, %{data: %{"type" => "ChatMessage", "content" => content}}, _) do
+ case content do
+ nil -> "@#{actor.nickname}: (Attachment)"
+ content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
+ end
+ end
+
def format_body(
%{activity: %{data: %{"type" => "Create"}}},
actor,
@@ -151,7 +156,7 @@ defmodule Pleroma.Web.Push.Impl do
mastodon_type
)
when type in ["Follow", "Like"] do
- mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+ mastodon_type = mastodon_type || notification.type
case mastodon_type do
"follow" -> "@#{actor.nickname} has followed you"
@@ -166,15 +171,14 @@ defmodule Pleroma.Web.Push.Impl do
"New Direct Message"
end
- def format_title(%{activity: activity}, mastodon_type) do
- mastodon_type = mastodon_type || mastodon_notification_type(activity)
-
- case mastodon_type do
+ def format_title(%{type: type}, mastodon_type) do
+ case mastodon_type || type do
"mention" -> "New Mention"
"follow" -> "New Follower"
"follow_request" -> "New Follow Request"
"reblog" -> "New Repeat"
"favourite" -> "New Favorite"
+ "pleroma:chat_mention" -> "New Chat Message"
type -> "New #{String.capitalize(type || "event")}"
end
end
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
index 3e401a490..5b5aa0d59 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -25,7 +25,7 @@ defmodule Pleroma.Web.Push.Subscription do
timestamps()
end
- @supported_alert_types ~w[follow favourite mention reblog]a
+ @supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention]a
defp alerts(%{data: %{alerts: alerts}}) do
alerts = Map.take(alerts, @supported_alert_types)
diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex
index 9d3d7f978..1729141e9 100644
--- a/lib/pleroma/web/rich_media/helpers.ex
+++ b/lib/pleroma/web/rich_media/helpers.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do
alias Pleroma.Object
alias Pleroma.Web.RichMedia.Parser
- @spec validate_page_url(any()) :: :ok | :error
+ @spec validate_page_url(URI.t() | binary()) :: :ok | :error
defp validate_page_url(page_url) when is_binary(page_url) do
validate_tld = Application.get_env(:auto_linker, :opts)[:validate_tld]
@@ -18,8 +18,8 @@ defmodule Pleroma.Web.RichMedia.Helpers do
|> parse_uri(page_url)
end
- defp validate_page_url(%URI{host: host, scheme: scheme, authority: authority})
- when scheme == "https" and not is_nil(authority) do
+ defp validate_page_url(%URI{host: host, scheme: "https", authority: authority})
+ when is_binary(authority) do
cond do
host in Config.get([:rich_media, :ignore_hosts], []) ->
:error
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
index 40980def8..ef5ead2da 100644
--- a/lib/pleroma/web/rich_media/parser.ex
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -91,7 +91,7 @@ defmodule Pleroma.Web.RichMedia.Parser do
html
|> parse_html()
|> maybe_parse()
- |> Map.put(:url, url)
+ |> Map.put("url", url)
|> clean_parsed_data()
|> check_parsed_data()
rescue
@@ -105,14 +105,14 @@ defmodule Pleroma.Web.RichMedia.Parser do
defp maybe_parse(html) do
Enum.reduce_while(parsers(), %{}, fn parser, acc ->
case parser.parse(html, acc) do
- {:ok, data} -> {:halt, data}
- {:error, _msg} -> {:cont, acc}
+ data when data != %{} -> {:halt, data}
+ _ -> {:cont, acc}
end
end)
end
- defp check_parsed_data(%{title: title} = data)
- when is_binary(title) and byte_size(title) > 0 do
+ defp check_parsed_data(%{"title" => title} = data)
+ when is_binary(title) and title != "" do
{:ok, data}
end
@@ -123,11 +123,7 @@ defmodule Pleroma.Web.RichMedia.Parser do
defp clean_parsed_data(data) do
data
|> Enum.reject(fn {key, val} ->
- with {:ok, _} <- Jason.encode(%{key => val}) do
- false
- else
- _ -> true
- end
+ not match?({:ok, _}, Jason.encode(%{key => val}))
end)
|> Map.new()
end
diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
index ae0f36702..3d577e254 100644
--- a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
+++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
@@ -3,22 +3,15 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do
- def parse(html, data, prefix, error_message, key_name, value_name \\ "content") do
- meta_data =
- html
- |> get_elements(key_name, prefix)
- |> Enum.reduce(data, fn el, acc ->
- attributes = normalize_attributes(el, prefix, key_name, value_name)
-
- Map.merge(acc, attributes)
- end)
- |> maybe_put_title(html)
-
- if Enum.empty?(meta_data) do
- {:error, error_message}
- else
- {:ok, meta_data}
- end
+ def parse(data, html, prefix, key_name, value_name \\ "content") do
+ html
+ |> get_elements(key_name, prefix)
+ |> Enum.reduce(data, fn el, acc ->
+ attributes = normalize_attributes(el, prefix, key_name, value_name)
+
+ Map.merge(acc, attributes)
+ end)
+ |> maybe_put_title(html)
end
defp get_elements(html, key_name, prefix) do
@@ -29,19 +22,19 @@ defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do
{_tag, attributes, _children} = html_node
data =
- Enum.into(attributes, %{}, fn {name, value} ->
+ Map.new(attributes, fn {name, value} ->
{name, String.trim_leading(value, "#{prefix}:")}
end)
- %{String.to_atom(data[key_name]) => data[value_name]}
+ %{data[key_name] => data[value_name]}
end
- defp maybe_put_title(%{title: _} = meta, _), do: meta
+ defp maybe_put_title(%{"title" => _} = meta, _), do: meta
defp maybe_put_title(meta, html) when meta != %{} do
case get_page_title(html) do
"" -> meta
- title -> Map.put_new(meta, :title, title)
+ title -> Map.put_new(meta, "title", title)
end
end
diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex
index 8f32bf91b..6bdeac89c 100644
--- a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex
+++ b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex
@@ -5,11 +5,11 @@
defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do
def parse(html, _data) do
with elements = [_ | _] <- get_discovery_data(html),
- {:ok, oembed_url} <- get_oembed_url(elements),
+ oembed_url when is_binary(oembed_url) <- get_oembed_url(elements),
{:ok, oembed_data} <- get_oembed_data(oembed_url) do
- {:ok, oembed_data}
+ oembed_data
else
- _e -> {:error, "No OEmbed data found"}
+ _e -> %{}
end
end
@@ -17,19 +17,13 @@ defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do
html |> Floki.find("link[type='application/json+oembed']")
end
- defp get_oembed_url(nodes) do
- {"link", attributes, _children} = nodes |> hd()
-
- {:ok, Enum.into(attributes, %{})["href"]}
+ defp get_oembed_url([{"link", attributes, _children} | _]) do
+ Enum.find_value(attributes, fn {k, v} -> if k == "href", do: v end)
end
defp get_oembed_data(url) do
- {:ok, %Tesla.Env{body: json}} = Pleroma.HTTP.get(url, [], adapter: [pool: :media])
-
- {:ok, data} = Jason.decode(json)
-
- data = data |> Map.new(fn {k, v} -> {String.to_atom(k), v} end)
-
- {:ok, data}
+ with {:ok, %Tesla.Env{body: json}} <- Pleroma.HTTP.get(url, [], adapter: [pool: :media]) do
+ Jason.decode(json)
+ end
end
end
diff --git a/lib/pleroma/web/rich_media/parsers/ogp.ex b/lib/pleroma/web/rich_media/parsers/ogp.ex
index 3e9012588..b3b3b059c 100644
--- a/lib/pleroma/web/rich_media/parsers/ogp.ex
+++ b/lib/pleroma/web/rich_media/parsers/ogp.ex
@@ -3,13 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.OGP do
- def parse(html, data) do
- Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse(
- html,
- data,
- "og",
- "No OGP metadata found",
- "property"
- )
+ @deprecated "OGP parser is deprecated. Use TwitterCard instead."
+ def parse(_html, _data) do
+ %{}
end
end
diff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex
index 09d4b526e..4a04865d2 100644
--- a/lib/pleroma/web/rich_media/parsers/twitter_card.ex
+++ b/lib/pleroma/web/rich_media/parsers/twitter_card.ex
@@ -5,18 +5,11 @@
defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do
alias Pleroma.Web.RichMedia.Parsers.MetaTagsParser
- @spec parse(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
+ @spec parse(list(), map()) :: map()
def parse(html, data) do
data
- |> parse_name_attrs(html)
- |> parse_property_attrs(html)
- end
-
- defp parse_name_attrs(data, html) do
- MetaTagsParser.parse(html, data, "twitter", %{}, "name")
- end
-
- defp parse_property_attrs({_, data}, html) do
- MetaTagsParser.parse(html, data, "twitter", "No twitter card metadata found", "property")
+ |> MetaTagsParser.parse(html, "og", "property")
+ |> MetaTagsParser.parse(html, "twitter", "name")
+ |> MetaTagsParser.parse(html, "twitter", "property")
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index aa272540d..419aa55e4 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -209,6 +209,10 @@ defmodule Pleroma.Web.Router do
post("/oauth_app", OAuthAppController, :create)
patch("/oauth_app/:id", OAuthAppController, :update)
delete("/oauth_app/:id", OAuthAppController, :delete)
+
+ get("/media_proxy_caches", MediaProxyCacheController, :index)
+ post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
+ post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
@@ -306,6 +310,15 @@ defmodule Pleroma.Web.Router do
scope [] do
pipe_through(:authenticated_api)
+ post("/chats/by-account-id/:id", ChatController, :create)
+ get("/chats", ChatController, :index)
+ get("/chats/:id", ChatController, :show)
+ get("/chats/:id/messages", ChatController, :messages)
+ post("/chats/:id/messages", ChatController, :post_chat_message)
+ delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
+ post("/chats/:id/read", ChatController, :mark_as_read)
+ post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read)
+
get("/conversations/:id/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show)
post("/conversations/read", ConversationController, :mark_as_read)
@@ -454,6 +467,7 @@ defmodule Pleroma.Web.Router do
scope "/api/web", Pleroma.Web do
pipe_through(:authenticated_api)
+ # Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere
put("/settings", MastoFEController, :put_settings)
end
diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex
index 0cf41189b..d1d2c9b9c 100644
--- a/lib/pleroma/web/streamer/streamer.ex
+++ b/lib/pleroma/web/streamer/streamer.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do
require Logger
alias Pleroma.Activity
+ alias Pleroma.Chat.MessageReference
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
@@ -22,7 +23,7 @@ defmodule Pleroma.Web.Streamer do
def registry, do: @registry
@public_streams ["public", "public:local", "public:media", "public:local:media"]
- @user_streams ["user", "user:notification", "direct"]
+ @user_streams ["user", "user:notification", "direct", "user:pleroma_chat"]
@doc "Expands and authorizes a stream, and registers the process for streaming."
@spec get_topic_and_add_socket(stream :: String.t(), User.t() | nil, Map.t() | nil) ::
@@ -89,34 +90,20 @@ defmodule Pleroma.Web.Streamer do
if should_env_send?(), do: Registry.unregister(@registry, topic)
end
- def stream(topics, item) when is_list(topics) do
+ def stream(topics, items) do
if should_env_send?() do
- Enum.each(topics, fn t ->
- spawn(fn -> do_stream(t, item) end)
+ List.wrap(topics)
+ |> Enum.each(fn topic ->
+ List.wrap(items)
+ |> Enum.each(fn item ->
+ spawn(fn -> do_stream(topic, item) end)
+ end)
end)
end
:ok
end
- def stream(topic, items) when is_list(items) do
- if should_env_send?() do
- Enum.each(items, fn i ->
- spawn(fn -> do_stream(topic, i) end)
- end)
-
- :ok
- end
- end
-
- def stream(topic, item) do
- if should_env_send?() do
- spawn(fn -> do_stream(topic, item) end)
- end
-
- :ok
- end
-
def filtered_by_user?(%User{} = user, %Activity{} = item) do
%{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
@@ -200,6 +187,19 @@ defmodule Pleroma.Web.Streamer do
end)
end
+ defp do_stream(topic, {user, %MessageReference{} = cm_ref})
+ when topic in ["user", "user:pleroma_chat"] do
+ topic = "#{topic}:#{user.id}"
+
+ text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
+
+ Registry.dispatch(@registry, topic, fn list ->
+ Enum.each(list, fn {pid, _auth} ->
+ send(pid, {:text, text})
+ end)
+ end)
+ end
+
defp do_stream("user", item) do
Logger.debug("Trying to push to users")
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index 237b29ded..476a33245 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -51,6 +51,29 @@ defmodule Pleroma.Web.StreamerView do
|> 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
+ # streaming it out
+ #
+ # It also contains the chat with a cache of the correct unread count
+ Logger.debug("Trying to stream out #{inspect(cm_ref)}")
+
+ representation =
+ Pleroma.Web.PleromaAPI.ChatView.render(
+ "show.json",
+ %{last_message: cm_ref, chat: cm_ref.chat}
+ )
+
+ %{
+ event: "pleroma:chat_update",
+ payload:
+ representation
+ |> Jason.encode!()
+ }
+ |> Jason.encode!()
+ end
+
def render("conversation.json", %Participation{} = participation) do
%{
event: "conversation",
diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex
index 49352db2a..8deeabda0 100644
--- a/lib/pleroma/workers/attachments_cleanup_worker.ex
+++ b/lib/pleroma/workers/attachments_cleanup_worker.ex
@@ -18,13 +18,19 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do
},
_job
) do
- hrefs =
- Enum.flat_map(attachments, fn attachment ->
- Enum.map(attachment["url"], & &1["href"])
- end)
+ attachments
+ |> Enum.flat_map(fn item -> Enum.map(item["url"], & &1["href"]) end)
+ |> fetch_objects
+ |> prepare_objects(actor, Enum.map(attachments, & &1["name"]))
+ |> filter_objects
+ |> do_clean
- names = Enum.map(attachments, & &1["name"])
+ {:ok, :success}
+ end
+
+ def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: {:ok, :skip}
+ defp do_clean({object_ids, attachment_urls}) do
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
prefix =
@@ -39,68 +45,70 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do
"/"
)
- # find all objects for copies of the attachments, name and actor doesn't matter here
- object_ids_and_hrefs =
- from(o in Object,
- where:
- fragment(
- "to_jsonb(array(select jsonb_array_elements((?)#>'{url}') ->> 'href' where jsonb_typeof((?)#>'{url}') = 'array'))::jsonb \\?| (?)",
- o.data,
- o.data,
- ^hrefs
- )
- )
- # The query above can be time consumptive on large instances until we
- # refactor how uploads are stored
- |> Repo.all(timeout: :infinity)
- # we should delete 1 object for any given attachment, but don't delete
- # files if there are more than 1 object for it
- |> Enum.reduce(%{}, fn %{
- id: id,
- data: %{
- "url" => [%{"href" => href}],
- "actor" => obj_actor,
- "name" => name
- }
- },
- acc ->
- Map.update(acc, href, %{id: id, count: 1}, fn val ->
- case obj_actor == actor and name in names do
- true ->
- # set id of the actor's object that will be deleted
- %{val | id: id, count: val.count + 1}
-
- false ->
- # another actor's object, just increase count to not delete file
- %{val | count: val.count + 1}
- end
- end)
- end)
- |> Enum.map(fn {href, %{id: id, count: count}} ->
- # only delete files that have single instance
- with 1 <- count do
- href
- |> String.trim_leading("#{base_url}/#{prefix}")
- |> uploader.delete_file()
-
- {id, href}
- else
- _ -> {id, nil}
- end
- end)
+ Enum.each(attachment_urls, fn href ->
+ href
+ |> String.trim_leading("#{base_url}/#{prefix}")
+ |> uploader.delete_file()
+ end)
- object_ids = Enum.map(object_ids_and_hrefs, fn {id, _} -> id end)
+ delete_objects(object_ids)
+ end
- from(o in Object, where: o.id in ^object_ids)
- |> Repo.delete_all()
+ defp delete_objects([_ | _] = object_ids) do
+ Repo.delete_all(from(o in Object, where: o.id in ^object_ids))
+ end
- object_ids_and_hrefs
- |> Enum.filter(fn {_, href} -> not is_nil(href) end)
- |> Enum.map(&elem(&1, 1))
- |> Pleroma.Web.MediaProxy.Invalidation.purge()
+ defp delete_objects(_), do: :ok
- {:ok, :success}
+ # we should delete 1 object for any given attachment, but don't delete
+ # files if there are more than 1 object for it
+ defp filter_objects(objects) do
+ Enum.reduce(objects, {[], []}, fn {href, %{id: id, count: count}}, {ids, hrefs} ->
+ with 1 <- count do
+ {ids ++ [id], hrefs ++ [href]}
+ else
+ _ -> {ids ++ [id], hrefs}
+ end
+ end)
end
- def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: {:ok, :skip}
+ defp prepare_objects(objects, actor, names) do
+ objects
+ |> Enum.reduce(%{}, fn %{
+ id: id,
+ data: %{
+ "url" => [%{"href" => href}],
+ "actor" => obj_actor,
+ "name" => name
+ }
+ },
+ acc ->
+ Map.update(acc, href, %{id: id, count: 1}, fn val ->
+ case obj_actor == actor and name in names do
+ true ->
+ # set id of the actor's object that will be deleted
+ %{val | id: id, count: val.count + 1}
+
+ false ->
+ # another actor's object, just increase count to not delete file
+ %{val | count: val.count + 1}
+ end
+ end)
+ end)
+ end
+
+ defp fetch_objects(hrefs) do
+ from(o in Object,
+ where:
+ fragment(
+ "to_jsonb(array(select jsonb_array_elements((?)#>'{url}') ->> 'href' where jsonb_typeof((?)#>'{url}') = 'array'))::jsonb \\?| (?)",
+ o.data,
+ o.data,
+ ^hrefs
+ )
+ )
+ # The query above can be time consumptive on large instances until we
+ # refactor how uploads are stored
+ |> Repo.all(timeout: :infinity)
+ end
end