Merge branch 'features/users-raw_bio' into 'develop'
authorrinpatch <rinpatch@sdf.org>
Wed, 17 Jun 2020 10:34:23 +0000 (10:34 +0000)
committerrinpatch <rinpatch@sdf.org>
Wed, 17 Jun 2020 10:34:23 +0000 (10:34 +0000)
User: Add raw_bio, storing unformatted bio

See merge request pleroma/pleroma!2326

162 files changed:
.gitlab-ci.yml
CHANGELOG.md
benchmarks/load_testing/fetcher.ex
benchmarks/mix/tasks/pleroma/benchmarks/tags.ex
config/config.exs
config/description.exs
docs/API/chats.md [new file with mode: 0644]
docs/API/differences_in_mastoapi_responses.md
docs/administration/CLI_tasks/emoji.md
docs/administration/CLI_tasks/user.md
docs/ap_extensions.md [new file with mode: 0644]
docs/configuration/cheatsheet.md
elixir_buildpack.config
installation/pleroma.nginx
lib/mix/tasks/pleroma/config.ex
lib/mix/tasks/pleroma/emoji.ex
lib/mix/tasks/pleroma/user.ex
lib/pleroma/activity.ex
lib/pleroma/bbs/handler.ex
lib/pleroma/chat.ex [new file with mode: 0644]
lib/pleroma/chat/message_reference.ex [new file with mode: 0644]
lib/pleroma/config/config_db.ex
lib/pleroma/config/deprecation_warnings.ex
lib/pleroma/config/transfer_task.ex
lib/pleroma/config/type/atom.ex [new file with mode: 0644]
lib/pleroma/config/type/binary_value.ex [new file with mode: 0644]
lib/pleroma/conversation/participation.ex
lib/pleroma/following_relationship.ex
lib/pleroma/migration_helper/notification_backfill.ex [new file with mode: 0644]
lib/pleroma/notification.ex
lib/pleroma/pagination.ex
lib/pleroma/plugs/http_security_plug.ex
lib/pleroma/repo.ex
lib/pleroma/upload.ex
lib/pleroma/user.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/activity_pub_controller.ex
lib/pleroma/web/activity_pub/builder.ex
lib/pleroma/web/activity_pub/mrf.ex
lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex
lib/pleroma/web/activity_pub/object_validator.ex
lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex [moved from lib/pleroma/web/activity_pub/object_validators/create_validator.ex with 100% similarity]
lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/pipeline.ex
lib/pleroma/web/activity_pub/side_effects.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/activity_pub/utils.ex
lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
lib/pleroma/web/admin_api/controllers/config_controller.ex
lib/pleroma/web/admin_api/controllers/relay_controller.ex [new file with mode: 0644]
lib/pleroma/web/admin_api/controllers/status_controller.ex
lib/pleroma/web/admin_api/views/account_view.ex
lib/pleroma/web/admin_api/views/config_view.ex
lib/pleroma/web/admin_api/views/report_view.ex
lib/pleroma/web/api_spec/operations/admin/relay_operation.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/operations/chat_operation.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/operations/notification_operation.ex
lib/pleroma/web/api_spec/operations/status_operation.ex
lib/pleroma/web/api_spec/operations/subscription_operation.ex
lib/pleroma/web/api_spec/schemas/chat.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/chat_message.ex [new file with mode: 0644]
lib/pleroma/web/common_api/activity_draft.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/common_api/utils.ex
lib/pleroma/web/controller_helper.ex
lib/pleroma/web/feed/tag_controller.ex
lib/pleroma/web/feed/user_controller.ex
lib/pleroma/web/mastodon_api/controllers/account_controller.ex
lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
lib/pleroma/web/mastodon_api/controllers/search_controller.ex
lib/pleroma/web/mastodon_api/controllers/status_controller.ex
lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
lib/pleroma/web/mastodon_api/mastodon_api.ex
lib/pleroma/web/mastodon_api/views/account_view.ex
lib/pleroma/web/mastodon_api/views/conversation_view.ex
lib/pleroma/web/mastodon_api/views/instance_view.ex
lib/pleroma/web/mastodon_api/views/notification_view.ex
lib/pleroma/web/mastodon_api/views/status_view.ex
lib/pleroma/web/pleroma_api/controllers/account_controller.ex
lib/pleroma/web/pleroma_api/controllers/chat_controller.ex [new file with mode: 0644]
lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex [new file with mode: 0644]
lib/pleroma/web/pleroma_api/views/chat_view.ex [new file with mode: 0644]
lib/pleroma/web/push/impl.ex
lib/pleroma/web/push/subscription.ex
lib/pleroma/web/rich_media/helpers.ex
lib/pleroma/web/rich_media/parser.ex
lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
lib/pleroma/web/rich_media/parsers/oembed_parser.ex
lib/pleroma/web/rich_media/parsers/ogp.ex
lib/pleroma/web/rich_media/parsers/twitter_card.ex
lib/pleroma/web/router.ex
lib/pleroma/web/static_fe/static_fe_controller.ex
lib/pleroma/web/streamer/streamer.ex
lib/pleroma/web/views/streamer_view.ex
mix.exs
priv/repo/migrations/20200309123730_create_chats.exs [new file with mode: 0644]
priv/repo/migrations/20200527163635_delete_notifications_from_invisible_users.exs [new file with mode: 0644]
priv/repo/migrations/20200602094828_add_type_to_notifications.exs [new file with mode: 0644]
priv/repo/migrations/20200602125218_backfill_notification_types.exs [new file with mode: 0644]
priv/repo/migrations/20200602150528_create_chat_message_reference.exs [new file with mode: 0644]
priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs [new file with mode: 0644]
priv/repo/migrations/20200603120448_remove_unread_from_chats.exs [new file with mode: 0644]
priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs [new file with mode: 0644]
priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs [new file with mode: 0644]
priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs [new file with mode: 0644]
priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs [new file with mode: 0644]
priv/static/schemas/litepub-0.1.jsonld
test/chat/message_reference_test.exs [new file with mode: 0644]
test/chat_test.exs [new file with mode: 0644]
test/config/config_db_test.exs
test/config/transfer_task_test.exs
test/fixtures/create-chat-message.json [new file with mode: 0644]
test/migration_helper/notification_backfill_test.exs [new file with mode: 0644]
test/notification_test.exs
test/pagination_test.exs
test/support/factory.ex
test/tasks/config_test.exs
test/tasks/relay_test.exs
test/tasks/user_test.exs
test/upload/filter/mogrify_test.exs
test/upload_test.exs
test/user_test.exs
test/web/activity_pub/activity_pub_test.exs
test/web/activity_pub/mrf/activity_expiration_policy_test.exs [new file with mode: 0644]
test/web/activity_pub/mrf/user_allowlist_policy_test.exs
test/web/activity_pub/object_validator_test.exs
test/web/activity_pub/object_validators/types/object_id_test.exs
test/web/activity_pub/object_validators/types/safe_text_test.exs [new file with mode: 0644]
test/web/activity_pub/pipeline_test.exs
test/web/activity_pub/side_effects_test.exs
test/web/activity_pub/transmogrifier/chat_message_test.exs [new file with mode: 0644]
test/web/activity_pub/transmogrifier/follow_handling_test.exs
test/web/admin_api/controllers/admin_api_controller_test.exs
test/web/admin_api/controllers/config_controller_test.exs
test/web/admin_api/controllers/relay_controller_test.exs [new file with mode: 0644]
test/web/common_api/common_api_test.exs
test/web/mastodon_api/controllers/notification_controller_test.exs
test/web/mastodon_api/controllers/search_controller_test.exs
test/web/mastodon_api/controllers/status_controller_test.exs
test/web/mastodon_api/controllers/subscription_controller_test.exs
test/web/mastodon_api/views/account_view_test.exs
test/web/mastodon_api/views/conversation_view_test.exs
test/web/mastodon_api/views/notification_view_test.exs
test/web/node_info_test.exs
test/web/pleroma_api/controllers/chat_controller_test.exs [new file with mode: 0644]
test/web/pleroma_api/views/chat/message_reference_view_test.exs [new file with mode: 0644]
test/web/pleroma_api/views/chat_view_test.exs [new file with mode: 0644]
test/web/push/impl_test.exs
test/web/rich_media/parser_test.exs
test/web/rich_media/parsers/twitter_card_test.exs
test/web/streamer/streamer_test.exs
test/workers/cron/purge_expired_activities_worker_test.exs

index aad28a2d879459c3c7ad3ea9a645239ec3bb49e5..b4bd59b43a32ddad040b3df83d746902e2c6060c 100644 (file)
@@ -1,4 +1,4 @@
-image: elixir:1.8.1
+image: elixir:1.9.4
 
 variables: &global_variables
   POSTGRES_DB: pleroma_test
@@ -170,8 +170,7 @@ stop_review_app:
 
 amd64:
   stage: release
-  # TODO: Replace with upstream image when 1.9.0 comes out
-  image: rinpatch/elixir:1.9.0-rc.0
+  image: elixir:1.10.3
   only: &release-only
   - stable@pleroma/pleroma
   - develop@pleroma/pleroma
@@ -208,8 +207,7 @@ amd64-musl:
   stage: release
   artifacts: *release-artifacts
   only: *release-only
-  # TODO: Replace with upstream image when 1.9.0 comes out
-  image: rinpatch/elixir:1.9.0-rc.0-alpine
+  image: elixir:1.10.3-alpine 
   cache: *release-cache
   variables: *release-variables
   before_script: &before-release-musl
@@ -225,8 +223,7 @@ arm:
   only: *release-only
   tags:
     - arm32
-  # TODO: Replace with upstream image when 1.9.0 comes out
-  image: rinpatch/elixir:1.9.0-rc.0-arm
+  image: elixir:1.10.3
   cache: *release-cache
   variables: *release-variables
   before_script: *before-release
@@ -238,8 +235,7 @@ arm-musl:
   only: *release-only
   tags:
     - arm32
-  # TODO: Replace with upstream image when 1.9.0 comes out
-  image: rinpatch/elixir:1.9.0-rc.0-arm-alpine
+  image: elixir:1.10.3-alpine
   cache: *release-cache
   variables: *release-variables
   before_script: *before-release-musl
@@ -251,8 +247,7 @@ arm64:
   only: *release-only
   tags:
     - arm
-  # TODO: Replace with upstream image when 1.9.0 comes out
-  image: rinpatch/elixir:1.9.0-rc.0-arm64
+  image: elixir:1.10.3
   cache: *release-cache
   variables: *release-variables
   before_script: *before-release
@@ -265,7 +260,7 @@ arm64-musl:
   tags:
     - arm
   # TODO: Replace with upstream image when 1.9.0 comes out
-  image: rinpatch/elixir:1.9.0-rc.0-arm64-alpine
+  image: elixir:1.10.3-alpine
   cache: *release-cache
   variables: *release-variables
   before_script: *before-release-musl
index 839bf90ab984a3a09c8e54021dfd42a135228894..3ee13904f352e6eb3ead49cd67e3dd24ae6f106b 100644 (file)
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ## [unreleased]
 
 ### Changed
+- **Breaking:** Elixir >=1.9 is now required (was >= 1.8)
+- In Conversations, return only direct messages as `last_status`
+- MFR policy to set global expiration for all local Create activities
+- OGP rich media parser merged with TwitterCard
 <details>
   <summary>API Changes</summary>
 - **Breaking:** Emoji API: changed methods and renamed routes.
@@ -15,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - **Breaking:** removed `with_move` parameter from notifications timeline.
 
 ### Added
+- Chats: Added support for federated chats. For details, see the docs.
 - ActivityPub: Added support for existing AP ids for instances migrated from Mastodon.
 - Instance: Add `background_image` to configuration and `/api/v1/instance`
 - Instance: Extend `/api/v1/instance` with Pleroma-specific information.
@@ -25,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Configuration: `filename_display_max_length` option to set filename truncate limit, if filename display enabled (0 = no limit).
 - New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
 - Mix task to create trusted OAuth App.
+- Mix task to reset MFA for user accounts
 - Notifications: Added `follow_request` notification type.
 - Added `:reject_deletes` group to SimplePolicy
 - MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
@@ -36,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mastodon API: Add support for filtering replies in public and home timelines
 - Admin API: endpoints for create/update/delete OAuth Apps.
 - Admin API: endpoint for status view.
+- OTP: Add command to reload emoji packs
 </details>
 
 ### Fixed
@@ -45,6 +52,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Filtering of push notifications on activities from blocked domains
 - Resolving Peertube accounts with Webfinger
 - `blob:` urls not being allowed by connect-src CSP
+- Mastodon API: fix `GET /api/v1/notifications` not returning the full result set
 
 ## [Unreleased (patch)]
 
index 22a06e472c9a185f17bb3e9f88ce09cbf0d0a2c5..15fd06c3d262a0189f7ed473025989799a817dd0 100644 (file)
@@ -52,12 +52,12 @@ defmodule Pleroma.LoadTesting.Fetcher do
 
   defp opts_for_home_timeline(user) do
     %{
-      "blocking_user" => user,
-      "count" => "20",
-      "muting_user" => user,
-      "type" => ["Create", "Announce"],
-      "user" => user,
-      "with_muted" => "true"
+      blocking_user: user,
+      count: "20",
+      muting_user: user,
+      type: ["Create", "Announce"],
+      user: user,
+      with_muted: true
     }
   end
 
@@ -70,17 +70,17 @@ defmodule Pleroma.LoadTesting.Fetcher do
       ActivityPub.fetch_activities(recipients, opts) |> Enum.reverse() |> List.last()
 
     second_page_last =
-      ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", first_page_last.id))
+      ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, first_page_last.id))
       |> Enum.reverse()
       |> List.last()
 
     third_page_last =
-      ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", second_page_last.id))
+      ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, second_page_last.id))
       |> Enum.reverse()
       |> List.last()
 
     forth_page_last =
-      ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", third_page_last.id))
+      ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, third_page_last.id))
       |> Enum.reverse()
       |> List.last()
 
@@ -90,19 +90,19 @@ defmodule Pleroma.LoadTesting.Fetcher do
       },
       inputs: %{
         "1 page" => opts,
-        "2 page" => Map.put(opts, "max_id", first_page_last.id),
-        "3 page" => Map.put(opts, "max_id", second_page_last.id),
-        "4 page" => Map.put(opts, "max_id", third_page_last.id),
-        "5 page" => Map.put(opts, "max_id", forth_page_last.id),
-        "1 page only media" => Map.put(opts, "only_media", "true"),
+        "2 page" => Map.put(opts, :max_id, first_page_last.id),
+        "3 page" => Map.put(opts, :max_id, second_page_last.id),
+        "4 page" => Map.put(opts, :max_id, third_page_last.id),
+        "5 page" => Map.put(opts, :max_id, forth_page_last.id),
+        "1 page only media" => Map.put(opts, :only_media, true),
         "2 page only media" =>
-          Map.put(opts, "max_id", first_page_last.id) |> Map.put("only_media", "true"),
+          Map.put(opts, :max_id, first_page_last.id) |> Map.put(:only_media, true),
         "3 page only media" =>
-          Map.put(opts, "max_id", second_page_last.id) |> Map.put("only_media", "true"),
+          Map.put(opts, :max_id, second_page_last.id) |> Map.put(:only_media, true),
         "4 page only media" =>
-          Map.put(opts, "max_id", third_page_last.id) |> Map.put("only_media", "true"),
+          Map.put(opts, :max_id, third_page_last.id) |> Map.put(:only_media, true),
         "5 page only media" =>
-          Map.put(opts, "max_id", forth_page_last.id) |> Map.put("only_media", "true")
+          Map.put(opts, :max_id, forth_page_last.id) |> Map.put(:only_media, true)
       },
       formatters: formatters()
     )
@@ -110,12 +110,12 @@ defmodule Pleroma.LoadTesting.Fetcher do
 
   defp opts_for_direct_timeline(user) do
     %{
-      :visibility => "direct",
-      "blocking_user" => user,
-      "count" => "20",
-      "type" => "Create",
-      "user" => user,
-      "with_muted" => "true"
+      visibility: "direct",
+      blocking_user: user,
+      count: "20",
+      type: "Create",
+      user: user,
+      with_muted: true
     }
   end
 
@@ -130,7 +130,7 @@ defmodule Pleroma.LoadTesting.Fetcher do
       |> Pagination.fetch_paginated(opts)
       |> List.last()
 
-    opts2 = Map.put(opts, "max_id", first_page_last.id)
+    opts2 = Map.put(opts, :max_id, first_page_last.id)
 
     second_page_last =
       recipients
@@ -138,7 +138,7 @@ defmodule Pleroma.LoadTesting.Fetcher do
       |> Pagination.fetch_paginated(opts2)
       |> List.last()
 
-    opts3 = Map.put(opts, "max_id", second_page_last.id)
+    opts3 = Map.put(opts, :max_id, second_page_last.id)
 
     third_page_last =
       recipients
@@ -146,7 +146,7 @@ defmodule Pleroma.LoadTesting.Fetcher do
       |> Pagination.fetch_paginated(opts3)
       |> List.last()
 
-    opts4 = Map.put(opts, "max_id", third_page_last.id)
+    opts4 = Map.put(opts, :max_id, third_page_last.id)
 
     forth_page_last =
       recipients
@@ -165,7 +165,7 @@ defmodule Pleroma.LoadTesting.Fetcher do
         "2 page" => opts2,
         "3 page" => opts3,
         "4 page" => opts4,
-        "5 page" => Map.put(opts4, "max_id", forth_page_last.id)
+        "5 page" => Map.put(opts4, :max_id, forth_page_last.id)
       },
       formatters: formatters()
     )
@@ -173,34 +173,34 @@ defmodule Pleroma.LoadTesting.Fetcher do
 
   defp opts_for_public_timeline(user) do
     %{
-      "type" => ["Create", "Announce"],
-      "local_only" => false,
-      "blocking_user" => user,
-      "muting_user" => user
+      type: ["Create", "Announce"],
+      local_only: false,
+      blocking_user: user,
+      muting_user: user
     }
   end
 
   defp opts_for_public_timeline(user, :local) do
     %{
-      "type" => ["Create", "Announce"],
-      "local_only" => true,
-      "blocking_user" => user,
-      "muting_user" => user
+      type: ["Create", "Announce"],
+      local_only: true,
+      blocking_user: user,
+      muting_user: user
     }
   end
 
   defp opts_for_public_timeline(user, :tag) do
     %{
-      "blocking_user" => user,
-      "count" => "20",
-      "local_only" => nil,
-      "muting_user" => user,
-      "tag" => ["tag"],
-      "tag_all" => [],
-      "tag_reject" => [],
-      "type" => "Create",
-      "user" => user,
-      "with_muted" => "true"
+      blocking_user: user,
+      count: "20",
+      local_only: nil,
+      muting_user: user,
+      tag: ["tag"],
+      tag_all: [],
+      tag_reject: [],
+      type: "Create",
+      user: user,
+      with_muted: true
     }
   end
 
@@ -223,7 +223,7 @@ defmodule Pleroma.LoadTesting.Fetcher do
   end
 
   defp fetch_public_timeline(user, :only_media) do
-    opts = opts_for_public_timeline(user) |> Map.put("only_media", "true")
+    opts = opts_for_public_timeline(user) |> Map.put(:only_media, true)
 
     fetch_public_timeline(opts, "public timeline only media")
   end
@@ -245,15 +245,13 @@ defmodule Pleroma.LoadTesting.Fetcher do
 
     user = User.get_by_id(user.id)
 
-    opts = Map.put(opts, "blocking_user", user)
+    opts = Map.put(opts, :blocking_user, user)
 
-    Benchee.run(
-      %{
-        "public timeline with user block" => fn ->
-          ActivityPub.fetch_public_activities(opts)
-        end
-      },
-    )
+    Benchee.run(%{
+      "public timeline with user block" => fn ->
+        ActivityPub.fetch_public_activities(opts)
+      end
+    })
 
     domains =
       Enum.reduce(remote_non_friends, [], fn non_friend, domains ->
@@ -269,30 +267,28 @@ defmodule Pleroma.LoadTesting.Fetcher do
     end)
 
     user = User.get_by_id(user.id)
-    opts = Map.put(opts, "blocking_user", user)
+    opts = Map.put(opts, :blocking_user, user)
 
-    Benchee.run(
-      %{
-        "public timeline with domain block" => fn opts ->
-          ActivityPub.fetch_public_activities(opts)
-        end
-      }
-    )
+    Benchee.run(%{
+      "public timeline with domain block" => fn ->
+        ActivityPub.fetch_public_activities(opts)
+      end
+    })
   end
 
   defp fetch_public_timeline(opts, title) when is_binary(title) do
     first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last()
 
     second_page_last =
-      ActivityPub.fetch_public_activities(Map.put(opts, "max_id", first_page_last.id))
+      ActivityPub.fetch_public_activities(Map.put(opts, :max_id, first_page_last.id))
       |> List.last()
 
     third_page_last =
-      ActivityPub.fetch_public_activities(Map.put(opts, "max_id", second_page_last.id))
+      ActivityPub.fetch_public_activities(Map.put(opts, :max_id, second_page_last.id))
       |> List.last()
 
     forth_page_last =
-      ActivityPub.fetch_public_activities(Map.put(opts, "max_id", third_page_last.id))
+      ActivityPub.fetch_public_activities(Map.put(opts, :max_id, third_page_last.id))
       |> List.last()
 
     Benchee.run(
@@ -303,17 +299,17 @@ defmodule Pleroma.LoadTesting.Fetcher do
       },
       inputs: %{
         "1 page" => opts,
-        "2 page" => Map.put(opts, "max_id", first_page_last.id),
-        "3 page" => Map.put(opts, "max_id", second_page_last.id),
-        "4 page" => Map.put(opts, "max_id", third_page_last.id),
-        "5 page" => Map.put(opts, "max_id", forth_page_last.id)
+        "2 page" => Map.put(opts, :max_id, first_page_last.id),
+        "3 page" => Map.put(opts, :max_id, second_page_last.id),
+        "4 page" => Map.put(opts, :max_id, third_page_last.id),
+        "5 page" => Map.put(opts, :max_id, forth_page_last.id)
       },
       formatters: formatters()
     )
   end
 
   defp opts_for_notifications do
-    %{"count" => "20", "with_muted" => "true"}
+    %{count: "20", with_muted: true}
   end
 
   defp fetch_notifications(user) do
@@ -322,15 +318,15 @@ defmodule Pleroma.LoadTesting.Fetcher do
     first_page_last = MastodonAPI.get_notifications(user, opts) |> List.last()
 
     second_page_last =
-      MastodonAPI.get_notifications(user, Map.put(opts, "max_id", first_page_last.id))
+      MastodonAPI.get_notifications(user, Map.put(opts, :max_id, first_page_last.id))
       |> List.last()
 
     third_page_last =
-      MastodonAPI.get_notifications(user, Map.put(opts, "max_id", second_page_last.id))
+      MastodonAPI.get_notifications(user, Map.put(opts, :max_id, second_page_last.id))
       |> List.last()
 
     forth_page_last =
-      MastodonAPI.get_notifications(user, Map.put(opts, "max_id", third_page_last.id))
+      MastodonAPI.get_notifications(user, Map.put(opts, :max_id, third_page_last.id))
       |> List.last()
 
     Benchee.run(
@@ -341,10 +337,10 @@ defmodule Pleroma.LoadTesting.Fetcher do
       },
       inputs: %{
         "1 page" => opts,
-        "2 page" => Map.put(opts, "max_id", first_page_last.id),
-        "3 page" => Map.put(opts, "max_id", second_page_last.id),
-        "4 page" => Map.put(opts, "max_id", third_page_last.id),
-        "5 page" => Map.put(opts, "max_id", forth_page_last.id)
+        "2 page" => Map.put(opts, :max_id, first_page_last.id),
+        "3 page" => Map.put(opts, :max_id, second_page_last.id),
+        "4 page" => Map.put(opts, :max_id, third_page_last.id),
+        "5 page" => Map.put(opts, :max_id, forth_page_last.id)
       },
       formatters: formatters()
     )
@@ -354,13 +350,13 @@ defmodule Pleroma.LoadTesting.Fetcher do
     first_page_last = ActivityPub.fetch_favourites(user) |> List.last()
 
     second_page_last =
-      ActivityPub.fetch_favourites(user, %{"max_id" => first_page_last.id}) |> List.last()
+      ActivityPub.fetch_favourites(user, %{:max_id => first_page_last.id}) |> List.last()
 
     third_page_last =
-      ActivityPub.fetch_favourites(user, %{"max_id" => second_page_last.id}) |> List.last()
+      ActivityPub.fetch_favourites(user, %{:max_id => second_page_last.id}) |> List.last()
 
     forth_page_last =
-      ActivityPub.fetch_favourites(user, %{"max_id" => third_page_last.id}) |> List.last()
+      ActivityPub.fetch_favourites(user, %{:max_id => third_page_last.id}) |> List.last()
 
     Benchee.run(
       %{
@@ -370,10 +366,10 @@ defmodule Pleroma.LoadTesting.Fetcher do
       },
       inputs: %{
         "1 page" => %{},
-        "2 page" => %{"max_id" => first_page_last.id},
-        "3 page" => %{"max_id" => second_page_last.id},
-        "4 page" => %{"max_id" => third_page_last.id},
-        "5 page" => %{"max_id" => forth_page_last.id}
+        "2 page" => %{:max_id => first_page_last.id},
+        "3 page" => %{:max_id => second_page_last.id},
+        "4 page" => %{:max_id => third_page_last.id},
+        "5 page" => %{:max_id => forth_page_last.id}
       },
       formatters: formatters()
     )
@@ -381,8 +377,8 @@ defmodule Pleroma.LoadTesting.Fetcher do
 
   defp opts_for_long_thread(user) do
     %{
-      "blocking_user" => user,
-      "user" => user
+      blocking_user: user,
+      user: user
     }
   end
 
@@ -392,9 +388,9 @@ defmodule Pleroma.LoadTesting.Fetcher do
 
     opts = opts_for_long_thread(user)
 
-    private_input = {private.data["context"], Map.put(opts, "exclude_id", private.id)}
+    private_input = {private.data["context"], Map.put(opts, :exclude_id, private.id)}
 
-    public_input = {public.data["context"], Map.put(opts, "exclude_id", public.id)}
+    public_input = {public.data["context"], Map.put(opts, :exclude_id, public.id)}
 
     Benchee.run(
       %{
@@ -514,13 +510,13 @@ defmodule Pleroma.LoadTesting.Fetcher do
     public_context =
       ActivityPub.fetch_activities_for_context(
         public.data["context"],
-        Map.put(fetch_opts, "exclude_id", public.id)
+        Map.put(fetch_opts, :exclude_id, public.id)
       )
 
     private_context =
       ActivityPub.fetch_activities_for_context(
         private.data["context"],
-        Map.put(fetch_opts, "exclude_id", private.id)
+        Map.put(fetch_opts, :exclude_id, private.id)
       )
 
     Benchee.run(
@@ -551,14 +547,14 @@ defmodule Pleroma.LoadTesting.Fetcher do
         end,
         "Public timeline with reply filtering - following" => fn ->
           public_params
-          |> Map.put("reply_visibility", "following")
-          |> Map.put("reply_filtering_user", user)
+          |> Map.put(:reply_visibility, "following")
+          |> Map.put(:reply_filtering_user, user)
           |> ActivityPub.fetch_public_activities()
         end,
         "Public timeline with reply filtering - self" => fn ->
           public_params
-          |> Map.put("reply_visibility", "self")
-          |> Map.put("reply_filtering_user", user)
+          |> Map.put(:reply_visibility, "self")
+          |> Map.put(:reply_filtering_user, user)
           |> ActivityPub.fetch_public_activities()
         end
       },
@@ -577,16 +573,16 @@ defmodule Pleroma.LoadTesting.Fetcher do
         "Home timeline with reply filtering - following" => fn ->
           private_params =
             private_params
-            |> Map.put("reply_filtering_user", user)
-            |> Map.put("reply_visibility", "following")
+            |> Map.put(:reply_filtering_user, user)
+            |> Map.put(:reply_visibility, "following")
 
           ActivityPub.fetch_activities(recipients, private_params)
         end,
         "Home timeline with reply filtering - self" => fn ->
           private_params =
             private_params
-            |> Map.put("reply_filtering_user", user)
-            |> Map.put("reply_visibility", "self")
+            |> Map.put(:reply_filtering_user, user)
+            |> Map.put(:reply_visibility, "self")
 
           ActivityPub.fetch_activities(recipients, private_params)
         end
index 1162b2e062ddacae1bb9a763148d0f6738787820..c051335a5ab644ee738673a347877223d668a128 100644 (file)
@@ -100,14 +100,14 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do
 
     _activities =
       params
-      |> Map.put("type", "Create")
-      |> Map.put("local_only", local_only)
-      |> Map.put("blocking_user", user)
-      |> Map.put("muting_user", user)
-      |> Map.put("user", user)
-      |> Map.put("tag", tags)
-      |> Map.put("tag_all", tag_all)
-      |> Map.put("tag_reject", tag_reject)
+      |> Map.put(:type, "Create")
+      |> Map.put(:local_only, local_only)
+      |> Map.put(:blocking_user, user)
+      |> Map.put(:muting_user, user)
+      |> Map.put(:user, user)
+      |> Map.put(:tag, tags)
+      |> Map.put(:tag_all, tag_all)
+      |> Map.put(:tag_reject, tag_reject)
       |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
   end
 end
index 9508ae07718a24ddfb906506b150154edaa5527b..6a7bb9e063e18242ab50ac4aeb791f7bae63aead 100644 (file)
@@ -371,6 +371,8 @@ config :pleroma, :mrf_keyword,
 
 config :pleroma, :mrf_subchain, match_actor: %{}
 
+config :pleroma, :mrf_activity_expiration, days: 365
+
 config :pleroma, :mrf_vocabulary,
   accept: [],
   reject: []
@@ -385,7 +387,6 @@ config :pleroma, :rich_media,
   ignore_tld: ["local", "localdomain", "lan"],
   parsers: [
     Pleroma.Web.RichMedia.Parsers.TwitterCard,
-    Pleroma.Web.RichMedia.Parsers.OGP,
     Pleroma.Web.RichMedia.Parsers.OEmbed
   ],
   ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl]
index 807c945e00ca990b3f44bdecf0e77b6f2f40f154..b21d7840cb2c63dbc2ff2b5781410b434aa9dfa7 100644 (file)
@@ -1471,6 +1471,21 @@ config :pleroma, :config_description, [
       }
     ]
   },
+  %{
+    group: :pleroma,
+    key: :mrf_activity_expiration,
+    label: "MRF Activity Expiration Policy",
+    type: :group,
+    description: "Adds expiration to all local Create Note activities",
+    children: [
+      %{
+        key: :days,
+        type: :integer,
+        description: "Default global expiration time for all local Create activities (in days)",
+        suggestions: [90, 365]
+      }
+    ]
+  },
   %{
     group: :pleroma,
     key: :mrf_subchain,
@@ -1608,14 +1623,12 @@ config :pleroma, :config_description, [
   # %{
   #   group: :pleroma,
   #   key: :mrf_user_allowlist,
-  #   type: :group,
+  #   type: :map,
   #   description:
   #     "The keys in this section are the domain names that the policy should apply to." <>
   #       " Each key should be assigned a list of users that should be allowed through by their ActivityPub ID",
-  #   children: [
-  #     ["example.org": ["https://example.org/users/admin"]],
   #     suggestions: [
-  #       ["example.org": ["https://example.org/users/admin"]]
+  #       %{"example.org" => ["https://example.org/users/admin"]}
   #     ]
   #   ]
   # },
@@ -2091,9 +2104,7 @@ config :pleroma, :config_description, [
         description:
           "List of Rich Media parsers. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parsers.` part), but on adding custom module you need to use full name.",
         suggestions: [
-          Pleroma.Web.RichMedia.Parsers.MetaTagsParser,
           Pleroma.Web.RichMedia.Parsers.OEmbed,
-          Pleroma.Web.RichMedia.Parsers.OGP,
           Pleroma.Web.RichMedia.Parsers.TwitterCard
         ]
       },
diff --git a/docs/API/chats.md b/docs/API/chats.md
new file mode 100644 (file)
index 0000000..aa61196
--- /dev/null
@@ -0,0 +1,248 @@
+# Chats
+
+Chats are a way to represent an IM-style conversation between two actors. They are not the same as direct messages and they are not `Status`es, even though they have a lot in common.
+
+## Why Chats?
+
+There are no 'visibility levels' in ActivityPub, their definition is purely a Mastodon convention. Direct Messaging between users on the fediverse has mostly been modeled by using ActivityPub addressing following Mastodon conventions on normal `Note` objects. In this case, a 'direct message' would be a message that has no followers addressed and also does not address the special public actor, but just the recipients in the `to` field. It would still be a `Note` and is presented with other `Note`s as a `Status` in the API.
+
+This is an awkward setup for a few reasons:
+
+- As DMs generally still follow the usual `Status` conventions, it is easy to accidentally pull somebody into a DM thread by mentioning them. (e.g. "I hate @badguy so much")
+- It is possible to go from a publicly addressed `Status` to a DM reply, back to public, then to a 'followers only' reply, and so on. This can be become very confusing, as it is unclear which user can see which part of the conversation.
+- The standard `Status` format of implicit addressing also leads to rather ugly results if you try to display the messages as a chat, because all the recipients are always mentioned by name in the message.
+- As direct messages are posted with the same api call (and usually same frontend component) as public messages, accidentally making a public message private or vice versa can happen easily. Client bugs can also lead to this, accidentally making private messages public.
+
+As a measure to improve this situation, the `Conversation` concept and related Pleroma extensions were introduced. While it made it possible to work around a few of the issues, many of the problems remained and it didn't see much adoption because it was too complicated to use correctly. 
+
+## Chats explained
+For this reasons, Chats are a new and different entity, both in the API as well as in ActivityPub. A quick overview:
+
+- Chats are meant to represent an instant message conversation between two actors. For now these are only 1-on-1 conversations, but the other actor can be a group in the future.
+- Chat messages have the ActivityPub type `ChatMessage`. They are not `Note`s. Servers that don't understand them will just drop them.
+- The only addressing allowed in `ChatMessage`s is one single ActivityPub actor in the `to` field.
+- There's always only one Chat between two actors. If you start chatting with someone and later start a 'new' Chat, the old Chat will be continued.
+- `ChatMessage`s are posted with a different api, making it very hard to accidentally send a message to the wrong person.
+- `ChatMessage`s don't show up in the existing timelines.
+- Chats can never go from private to public. They are always private between the two actors.
+
+## Caveats
+
+- Chats are NOT E2E encrypted (yet). Security is still the same as email.
+
+## API
+
+In general, the way to send a `ChatMessage` is to first create a `Chat`, then post a message to that `Chat`. `Group`s will later be supported by making them a sub-type of `Account`.
+
+This is the overview of using the API. The API is also documented via OpenAPI, so you can view it and play with it by pointing SwaggerUI or a similar OpenAPI tool to `https://yourinstance.tld/api/openapi`.
+
+### Creating or getting a chat.
+
+To create or get an existing Chat for a certain recipient (identified by Account ID)
+you can call:
+
+`POST /api/v1/pleroma/chats/by-account-id/:account_id`
+
+The account id is the normal FlakeId of the user
+```
+POST /api/v1/pleroma/chats/by-account-id/someflakeid
+```
+
+If you already have the id of a chat, you can also use
+
+```
+GET /api/v1/pleroma/chats/:id
+```
+
+There will only ever be ONE Chat for you and a given recipient, so this call
+will return the same Chat if you already have one with that user.
+
+Returned data:
+
+```json
+{
+  "account": {
+    "id": "someflakeid",
+    "username": "somenick",
+    ...
+  },
+  "id" : "1",
+  "unread" : 2,
+  "last_message" : {...}, // The last message in that chat
+  "updated_at": "2020-04-21T15:11:46.000Z"
+}
+```
+
+### Marking a chat as read
+
+To mark a number of messages in a chat up to a certain message as read, you can use
+
+`POST /api/v1/pleroma/chats/:id/read`
+
+
+Parameters:
+- last_read_id: Given this id, all chat messages until this one will be marked as read. Required.
+
+
+Returned data:
+
+```json
+{
+  "account": {
+    "id": "someflakeid",
+    "username": "somenick",
+    ...
+  },
+  "id" : "1",
+  "unread" : 0,
+  "updated_at": "2020-04-21T15:11:46.000Z"
+}
+```
+
+### Marking a single chat message as read
+
+To set the `unread` property of a message to `false`
+
+`POST /api/v1/pleroma/chats/:id/messages/:message_id/read`
+
+Returned data:
+
+The modified chat message
+
+### Getting a list of Chats
+
+`GET /api/v1/pleroma/chats`
+
+This will return a list of chats that you have been involved in, sorted by their
+last update (so new chats will be at the top).
+
+Returned data:
+
+```json
+[
+   {
+      "account": {
+        "id": "someflakeid",
+        "username": "somenick",
+        ...
+      },
+      "id" : "1",
+      "unread" : 2,
+      "last_message" : {...}, // The last message in that chat
+      "updated_at": "2020-04-21T15:11:46.000Z"
+   }
+]
+```
+
+The recipient of messages that are sent to this chat is given by their AP ID.
+No pagination is implemented for now.
+
+### Getting the messages for a Chat
+
+For a given Chat id, you can get the associated messages with
+
+`GET /api/v1/pleroma/chats/:id/messages`
+
+This will return all messages, sorted by most recent to least recent. The usual
+pagination options are implemented.
+
+Returned data:
+
+```json
+[
+  {
+    "account_id": "someflakeid",
+    "chat_id": "1",
+    "content": "Check this out :firefox:",
+    "created_at": "2020-04-21T15:11:46.000Z",
+    "emojis": [
+      {
+        "shortcode": "firefox",
+        "static_url": "https://dontbulling.me/emoji/Firefox.gif",
+        "url": "https://dontbulling.me/emoji/Firefox.gif",
+        "visible_in_picker": false
+      }
+    ],
+    "id": "13",
+    "unread": true
+  },
+  {
+    "account_id": "someflakeid",
+    "chat_id": "1",
+    "content": "Whats' up?",
+    "created_at": "2020-04-21T15:06:45.000Z",
+    "emojis": [],
+    "id": "12",
+    "unread": false
+  }
+]
+```
+
+### Posting a chat message
+
+Posting a chat message for given Chat id works like this:
+
+`POST /api/v1/pleroma/chats/:id/messages`
+
+Parameters:
+- content: The text content of the message. Optional if media is attached.
+- media_id: The id of an upload that will be attached to the message.
+
+Currently, no formatting beyond basic escaping and emoji is implemented.
+
+Returned data:
+
+```json
+{
+  "account_id": "someflakeid",
+  "chat_id": "1",
+  "content": "Check this out :firefox:",
+  "created_at": "2020-04-21T15:11:46.000Z",
+  "emojis": [
+    {
+      "shortcode": "firefox",
+      "static_url": "https://dontbulling.me/emoji/Firefox.gif",
+      "url": "https://dontbulling.me/emoji/Firefox.gif",
+      "visible_in_picker": false
+    }
+  ],
+  "id": "13",
+  "unread": false
+}
+```
+
+### Deleting a chat message
+
+Deleting a chat message for given Chat id works like this:
+
+`DELETE /api/v1/pleroma/chats/:chat_id/messages/:message_id`
+
+Returned data is the deleted message.
+
+### Notifications
+
+There's a new `pleroma:chat_mention` notification, which has this form. It is not given out in the notifications endpoint by default, you need to explicitly request it with `include_types[]=pleroma:chat_mention`:
+
+```json
+{
+  "id": "someid",
+  "type": "pleroma:chat_mention",
+  "account": { ... } // User account of the sender,
+  "chat_message": {
+    "chat_id": "1",
+    "id": "10",
+    "content": "Hello",
+    "account_id": "someflakeid",
+    "unread": false
+  },
+  "created_at": "somedate"
+}
+```
+
+### Streaming
+
+There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
+
+### Web Push
+
+If you want to receive push messages for this type, you'll need to add the `pleroma:chat_mention` type to your alerts in the push subscription.
index 434ade9a42f0d8125c1b99232fc9a3a596242d2c..be3c802af23d675d9abb5ec81c55da2d41a1b20f 100644 (file)
@@ -230,3 +230,7 @@ Has theses additional parameters (which are the same as in Pleroma-API):
 Has these additional fields under the `pleroma` object:
 
 - `unread_count`: contains number unread notifications
+
+## Streaming
+
+There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
index 3d524a52b190b16e1f26ec8f4c139a9b506b462b..ddcb7e62c4c56fde1abd126bac25f676a709d28a 100644 (file)
@@ -44,3 +44,11 @@ Currently, only .zip archives are recognized as remote pack files and packs are
   The manifest entry will either be written to a newly created `pack_name.json` file (pack name is asked in questions) or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously.
 
   The file list will be written to the file specified previously, *replacing* that file. You _should_ check that the file list doesn't contain anything you don't need in the pack, that is, anything that is not an emoji (the whole pack is downloaded, but only emoji files are extracted).
+
+## Reload emoji packs
+
+```sh tab="OTP"
+./bin/pleroma_ctl emoji reload
+```
+
+This command only works with OTP releases.
index afeb8d52f72512d2b8cc87a48e07395c3d0ebba0..1e6f4a8b43ddcb88256bb3a0e30a469998025000 100644 (file)
@@ -135,6 +135,16 @@ mix pleroma.user reset_password <nickname>
 ```
 
 
+## Disable Multi Factor Authentication (MFA/2FA) for a user
+```sh tab="OTP"
+ ./bin/pleroma_ctl user reset_mfa <nickname>
+```
+
+```sh tab="From Source"
+mix pleroma.user reset_mfa <nickname>
+```
+
+
 ## Set the value of the given user's settings
 ```sh tab="OTP"
  ./bin/pleroma_ctl user set <nickname> [option ...]
diff --git a/docs/ap_extensions.md b/docs/ap_extensions.md
new file mode 100644 (file)
index 0000000..c4550a1
--- /dev/null
@@ -0,0 +1,35 @@
+# ChatMessages
+
+ChatMessages are the messages sent in 1-on-1 chats. They are similar to
+`Note`s, but the addresing is done by having a single AP actor in the `to`
+field. Addressing multiple actors is not allowed. These messages are always
+private, there is no public version of them. They are created with a `Create`
+activity.
+
+Example:
+
+```json
+{
+  "actor": "http://2hu.gensokyo/users/raymoo",
+  "id": "http://2hu.gensokyo/objects/1",
+  "object": {
+    "attributedTo": "http://2hu.gensokyo/users/raymoo",
+    "content": "You expected a cute girl? Too bad.",
+    "id": "http://2hu.gensokyo/objects/2",
+    "published": "2020-02-12T14:08:20Z",
+    "to": [
+      "http://2hu.gensokyo/users/marisa"
+    ],
+    "type": "ChatMessage"
+  },
+  "published": "2018-02-12T14:08:20Z",
+  "to": [
+    "http://2hu.gensokyo/users/marisa"
+  ],
+  "type": "Create"
+}
+```
+
+This setup does not prevent multi-user chats, but these will have to go through
+a `Group`, which will be the recipient of the messages and then `Announce` them
+to the users in the `Group`.
index 505acb293fcb5f02d13a61e16949e0106dc94a6d..fad67fc4da1365765cba078caf1ad00a5512ff08 100644 (file)
@@ -39,7 +39,7 @@ To add configuration to your config file, you can copy it from the base config.
 * `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
     * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default).
     * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production.
-    * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)).
+    * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certain instances (See [`:mrf_simple`](#mrf_simple)).
     * `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive).
     * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)).
     * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)).
@@ -49,7 +49,8 @@ To add configuration to your config file, you can copy it from the base config.
     * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)).
     * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)).
     * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
-* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
+    * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Adds expiration to all local Create activities (see [`:mrf_activity_expiration`](#mrf_activity_expiration)).
+* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
 * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
 * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``.
 * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
@@ -137,8 +138,9 @@ their ActivityPub ID.
 An example:
 
 ```elixir
-config :pleroma, :mrf_user_allowlist,
-  "example.org": ["https://example.org/users/admin"]
+config :pleroma, :mrf_user_allowlist, %{
+  "example.org" => ["https://example.org/users/admin"]
+}
 ```
 
 #### :mrf_object_age
@@ -154,6 +156,10 @@ config :pleroma, :mrf_user_allowlist,
 * `rejected_shortcodes`: Regex-list of shortcodes to reject
 * `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
 
+#### :mrf_activity_expiration
+
+* `days`: Default global expiration time for all local Create activities (in days)
+
 ### :activitypub
 * `unfollow_blocked`: Whether blocks result in people getting unfollowed
 * `outgoing_blocks`: Whether to federate blocks to other instances
index c23b08fb852ebc7147dcefff9596300909564c85..946408c12fd6e9bf254895a7a24de88a4785ecd5 100644 (file)
@@ -1,2 +1,2 @@
-elixir_version=1.8.2
-erlang_version=21.3.7
+elixir_version=1.9.4
+erlang_version=22.3.4.1
index 688be3e712a4371fd2e37dbdf45548d3e9ac1d21..d301ca615e99bc7ba3917a62d7b89b9e56ef9ee3 100644 (file)
@@ -37,18 +37,17 @@ server {
 
     listen 443 ssl http2;
     listen [::]:443 ssl http2;
-    ssl_session_timeout 5m;
+    ssl_session_timeout 1d;
+    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
+    ssl_session_tickets off;
 
     ssl_trusted_certificate   /etc/letsencrypt/live/example.tld/chain.pem;
     ssl_certificate           /etc/letsencrypt/live/example.tld/fullchain.pem;
     ssl_certificate_key       /etc/letsencrypt/live/example.tld/privkey.pem;
 
-    # Add TLSv1.0 to support older devices
-    ssl_protocols TLSv1.2;
-    # Uncomment line below if you want to support older devices (Before Android 4.4.2, IE 8, etc.)
-    # ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
+    ssl_protocols TLSv1.2 TLSv1.3;
     ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
-    ssl_prefer_server_ciphers on;
+    ssl_prefer_server_ciphers off;
     # In case of an old server with an OpenSSL version of 1.0.2 or below,
     # leave only prime256v1 or comment out the following line.
     ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;
index 5c9ef6904ac325c53887062f38182586285c60ba..f1b3a8766f12b720387537f2617acb5eeb073d04 100644 (file)
@@ -72,8 +72,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 +130,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
index 29a5fa99cacf8a1045c9ee34266668c96dae1c74..f4eaeac98c2c5244679183af0b7ff1b2d227c392 100644 (file)
@@ -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)
index 3635c02bc4d21aaf0b29c5f25434e3d83e624ebf..bca7e87bf533c01391f681a79dfaa7dfe2b7ff8e 100644 (file)
@@ -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()
 
index 6213d0eb7cb732142325b11ec235140ca6677c4f..c3cea8d2a2a24284a03e9d8dd15aaec7a245e11a 100644 (file)
@@ -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: []
 
index 12d64c2fe2c982f2c09800bb9d5cb5871b5661bb..cd523cf7d9855476ec578679d4f1102f578b618d 100644 (file)
@@ -92,10 +92,10 @@ defmodule Pleroma.BBS.Handler do
 
     params =
       %{}
-      |> Map.put("type", ["Create"])
-      |> Map.put("blocking_user", user)
-      |> Map.put("muting_user", user)
-      |> Map.put("user", user)
+      |> Map.put(:type, ["Create"])
+      |> Map.put(:blocking_user, user)
+      |> Map.put(:muting_user, user)
+      |> Map.put(:user, user)
 
     activities =
       [user.ap_id | Pleroma.User.following(user)]
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
new file mode 100644 (file)
index 0000000..24a8637
--- /dev/null
@@ -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 (file)
index 0000000..131ae01
--- /dev/null
@@ -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
index 2b43d4c365f91586b5e0a9b569958fce83048a74..30bd51b05a2ab68ec70e2db8ee35b8bb15026215 100644 (file)
@@ -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.Config.Type.Atom)
+    field(:group, Pleroma.Config.Type.Atom)
+    field(:value, Pleroma.Config.Type.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,43 +130,40 @@ 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
 
@@ -205,11 +173,10 @@ defmodule Pleroma.ConfigDB do
 
     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 +192,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 +226,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 +292,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
+
+  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
 
-  defp do_transform(entity), do: entity
+  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 +373,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
index c39a8984bf0d308d23ab2d99df11ad092c025e1e..b68ded01f6bd486f575485515cd588c2e0b312e5 100644 (file)
@@ -4,9 +4,10 @@
 
 defmodule Pleroma.Config.DeprecationWarnings do
   require Logger
+  alias Pleroma.Config
 
   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 +15,29 @@ 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()
   end
 end
index c02b70e96691b18647ba7ceef83419dfd19b125f..eb86b8ff490d8126a520ab9396bcb5f999493be8 100644 (file)
@@ -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/config/type/atom.ex b/lib/pleroma/config/type/atom.ex
new file mode 100644 (file)
index 0000000..3878692
--- /dev/null
@@ -0,0 +1,22 @@
+defmodule Pleroma.Config.Type.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/config/type/binary_value.ex b/lib/pleroma/config/type/binary_value.ex
new file mode 100644 (file)
index 0000000..17c5524
--- /dev/null
@@ -0,0 +1,23 @@
+defmodule Pleroma.Config.Type.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
index 51bb1bda94d43e85043d3c930896f4462565a2ca..8bc3e85d6e1f50f65c199c52999e05e903508fcf 100644 (file)
@@ -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
index 3a3082e728037e265bc56a55883f8df7f5e36fe9..093b1f4050d62a94f70c865bc27262f2cb70b6ca 100644 (file)
@@ -141,6 +141,12 @@ defmodule Pleroma.FollowingRelationship do
     |> where([r], r.state == ^:follow_accept)
   end
 
+  def outgoing_pending_follow_requests_query(%User{} = follower) do
+    __MODULE__
+    |> where([r], r.follower_id == ^follower.id)
+    |> where([r], r.state == ^:follow_pending)
+  end
+
   def following(%User{} = user) do
     following =
       following_query(user)
diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex
new file mode 100644 (file)
index 0000000..b377030
--- /dev/null
@@ -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
index 7eca55ac9fd76dd80b4cc4f6b2737d4444c4a890..9ee9606becb9fd9bf779d81b6b72a01c5b4595ac 100644 (file)
@@ -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
index d43a96cd2ee4bcfef30d9570722c59e7fd730b89..1b99e44f9304210b811ecd9668097847be2c395e 100644 (file)
@@ -23,12 +23,12 @@ defmodule Pleroma.Pagination do
   @spec fetch_paginated(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
   def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil)
 
-  def fetch_paginated(query, %{"total" => true} = params, :keyset, table_binding) do
+  def fetch_paginated(query, %{total: true} = params, :keyset, table_binding) do
     total = Repo.aggregate(query, :count, :id)
 
     %{
       total: total,
-      items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset, table_binding)
+      items: fetch_paginated(query, Map.drop(params, [:total]), :keyset, table_binding)
     }
   end
 
@@ -41,7 +41,7 @@ defmodule Pleroma.Pagination do
     |> enforce_order(options)
   end
 
-  def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) do
+  def fetch_paginated(query, %{total: true} = params, :offset, table_binding) do
     total =
       query
       |> Ecto.Query.exclude(:left_join)
@@ -49,7 +49,7 @@ defmodule Pleroma.Pagination do
 
     %{
       total: total,
-      items: fetch_paginated(query, Map.drop(params, ["total"]), :offset, table_binding)
+      items: fetch_paginated(query, Map.drop(params, [:total]), :offset, table_binding)
     }
   end
 
@@ -90,12 +90,6 @@ defmodule Pleroma.Pagination do
       skip_order: :boolean
     }
 
-    params =
-      Enum.reduce(params, %{}, fn
-        {key, _value}, acc when is_atom(key) -> Map.drop(acc, [key])
-        {key, value}, acc -> Map.put(acc, key, value)
-      end)
-
     changeset = cast({%{}, param_types}, params, Map.keys(param_types))
     changeset.changes
   end
index 6a339b32ca2e2b5491f992aca5dff5317eb1fc00..1420a96113e92a777df09ef5263c317c032a7b69 100644 (file)
@@ -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)
index f621384663266e7122484971af82d3da5ba90b79..6d85d70bc66c9ac6de1d0266c4937572402c2e95 100644 (file)
@@ -8,6 +8,7 @@ defmodule Pleroma.Repo do
     adapter: Ecto.Adapters.Postgres,
     migration_timestamps: [type: :naive_datetime_usec]
 
+  import Ecto.Query
   require Logger
 
   defmodule Instrumenter do
@@ -78,6 +79,33 @@ defmodule Pleroma.Repo do
       :ok
     end
   end
+
+  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}
+
+            records ->
+              last_id = List.last(records).id
+              {records, last_id}
+          end
+      end,
+      fn _ -> :ok end
+    )
+  end
 end
 
 defmodule Pleroma.Repo.UnappliedMigrationsError do
index 1be1a3a5b3e03c371b89549374b8cf9579187e0c..797555bffa324643d7105450f21cbc1090304c39 100644 (file)
@@ -67,6 +67,7 @@ defmodule Pleroma.Upload do
       {:ok,
        %{
          "type" => opts.activity_type,
+         "mediaType" => upload.content_type,
          "url" => [
            %{
              "type" => "Link",
index 23ca8c9f3e2b2eb0e3c4f8edf4d28872b7ad53a3..ff3ec1197e16956be0a37bc4fcdfcfdbc07aacfc 100644 (file)
@@ -1499,6 +1499,9 @@ defmodule Pleroma.User do
     end)
 
     delete_user_activities(user)
+    delete_notifications_from_user_activities(user)
+
+    delete_outgoing_pending_follow_requests(user)
 
     delete_or_deactivate(user)
   end
@@ -1585,6 +1588,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()
@@ -1622,6 +1632,12 @@ defmodule Pleroma.User do
 
   defp delete_activity(_activity, _user), do: "Doing nothing"
 
+  defp delete_outgoing_pending_follow_requests(user) do
+    user
+    |> FollowingRelationship.outgoing_pending_follow_requests_query()
+    |> Repo.delete_all()
+  end
+
   def html_filter_policy(%User{no_rich_text: true}) do
     Pleroma.HTML.Scrubber.TwitterText
   end
index 75468f415ced7755a50ce130021f954b1dab293a..3e4f3ad30804290f7967fbab281598bf6e06d6f5 100644 (file)
@@ -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", [])
@@ -67,16 +49,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     {recipients, to, cc}
   end
 
-  defp check_actor_is_active(actor) do
-    if not is_nil(actor) do
-      with user <- User.get_cached_by_ap_id(actor),
-           false <- user.deactivated do
-        true
-      else
-        _e -> false
-      end
-    else
-      true
+  defp check_actor_is_active(nil), do: true
+
+  defp check_actor_is_active(actor) when is_binary(actor) do
+    case User.get_cached_by_ap_id(actor) do
+      %User{deactivated: deactivated} -> not deactivated
+      _ -> false
     end
   end
 
@@ -87,7 +65,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp check_remote_limit(_), do: true
 
-  def increase_note_count_if_public(actor, object) do
+  defp increase_note_count_if_public(actor, object) do
     if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
   end
 
@@ -95,38 +73,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
   end
 
-  def increase_replies_count_if_reply(%{
-        "object" => %{"inReplyTo" => reply_ap_id} = object,
-        "type" => "Create"
-      }) do
+  defp increase_replies_count_if_reply(%{
+         "object" => %{"inReplyTo" => reply_ap_id} = object,
+         "type" => "Create"
+       }) do
     if is_public?(object) do
       Object.increase_replies_count(reply_ap_id)
     end
   end
 
-  def increase_replies_count_if_reply(_create_data), do: :noop
+  defp increase_replies_count_if_reply(_create_data), do: :noop
 
-  def decrease_replies_count_if_reply(%Object{
-        data: %{"inReplyTo" => reply_ap_id} = object
-      }) do
-    if is_public?(object) do
-      Object.decrease_replies_count(reply_ap_id)
-    end
-  end
-
-  def decrease_replies_count_if_reply(_object), do: :noop
-
-  def increase_poll_votes_if_vote(%{
-        "object" => %{"inReplyTo" => reply_ap_id, "name" => name},
-        "type" => "Create",
-        "actor" => actor
-      }) do
+  defp increase_poll_votes_if_vote(%{
+         "object" => %{"inReplyTo" => reply_ap_id, "name" => name},
+         "type" => "Create",
+         "actor" => actor
+       }) do
     Object.increase_vote_count(reply_ap_id, name, actor)
   end
 
-  def increase_poll_votes_if_vote(_create_data), do: :noop
+  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),
@@ -153,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)
@@ -196,10 +173,18 @@ 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),
-         Participation.mark_as_read(user, conversation) do
+         %User{} = user <- User.get_cached_by_ap_id(actor) do
+      Participation.mark_as_read(user, conversation)
       {:ok, conversation}
     end
   end
@@ -221,13 +206,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   end
 
   def stream_out_participations(%Object{data: %{"context" => context}}, user) do
-    with %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
-         conversation = Repo.preload(conversation, :participations),
-         last_activity_id =
-           fetch_latest_activity_id_for_context(conversation.ap_id, %{
-             "user" => user,
-             "blocking_user" => user
-           }) do
+    with %Conversation{} = conversation <- Conversation.get_for_ap_id(context) do
+      conversation = Repo.preload(conversation, :participations)
+
+      last_activity_id =
+        fetch_latest_direct_activity_id_for_context(conversation.ap_id, %{
+          user: user,
+          blocking_user: user
+        })
+
       if last_activity_id do
         stream_out_participations(conversation.participations)
       end
@@ -261,12 +248,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     published = params[:published]
     quick_insert? = Config.get([:env]) == :benchmark
 
-    with create_data <-
-           make_create_data(
-             %{to: to, actor: actor, published: published, context: context, object: object},
-             additional
-           ),
-         {:ok, activity} <- insert(create_data, local, fake),
+    create_data =
+      make_create_data(
+        %{to: to, actor: actor, published: published, context: context, object: object},
+        additional
+      )
+
+    with {:ok, activity} <- insert(create_data, local, fake),
          {:fake, false, activity} <- {:fake, fake, activity},
          _ <- increase_replies_count_if_reply(create_data),
          _ <- increase_poll_votes_if_vote(create_data),
@@ -294,12 +282,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     local = !(params[:local] == false)
     published = params[:published]
 
-    with listen_data <-
-           make_listen_data(
-             %{to: to, actor: actor, published: published, context: context, object: object},
-             additional
-           ),
-         {:ok, activity} <- insert(listen_data, local),
+    listen_data =
+      make_listen_data(
+        %{to: to, actor: actor, published: published, context: context, object: object},
+        additional
+      )
+
+    with {:ok, activity} <- insert(listen_data, local),
          _ <- notify_and_stream(activity),
          :ok <- maybe_federate(activity) do
       {:ok, activity}
@@ -317,14 +306,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   end
 
   @spec accept_or_reject(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
-  def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
+  defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
     local = Map.get(params, :local, true)
     activity_id = Map.get(params, :activity_id, nil)
 
-    with data <-
-           %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
-           |> Maps.put_if_present("id", activity_id),
-         {:ok, activity} <- insert(data, local),
+    data =
+      %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
+      |> Maps.put_if_present("id", activity_id)
+
+    with {:ok, activity} <- insert(data, local),
          _ <- notify_and_stream(activity),
          :ok <- maybe_federate(activity) do
       {:ok, activity}
@@ -336,34 +326,38 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     local = !(params[:local] == false)
     activity_id = params[:activity_id]
 
-    with data <- %{
-           "to" => to,
-           "cc" => cc,
-           "type" => "Update",
-           "actor" => actor,
-           "object" => object
-         },
-         data <- Maps.put_if_present(data, "id", activity_id),
-         {:ok, activity} <- insert(data, local),
+    data =
+      %{
+        "to" => to,
+        "cc" => cc,
+        "type" => "Update",
+        "actor" => actor,
+        "object" => object
+      }
+      |> Maps.put_if_present("id", activity_id)
+
+    with {:ok, activity} <- insert(data, local),
          _ <- notify_and_stream(activity),
          :ok <- maybe_federate(activity) do
       {:ok, activity}
     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
-    with data <- make_follow_data(follower, followed, activity_id),
-         {:ok, activity} <- insert(data, local),
-         _ <- notify_and_stream(activity),
+  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),
+         _ <- skip_notify_and_stream || notify_and_stream(activity),
          :ok <- maybe_federate(activity) do
       {:ok, activity}
     else
@@ -406,13 +400,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   defp do_block(blocker, blocked, activity_id, local) do
     unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
 
-    if unfollow_blocked do
-      follow_activity = fetch_latest_follow(blocker, blocked)
-      if follow_activity, do: unfollow(blocker, blocked, nil, local)
+    if unfollow_blocked and fetch_latest_follow(blocker, blocked) do
+      unfollow(blocker, blocked, nil, local)
     end
 
-    with block_data <- make_block_data(blocker, blocked, activity_id),
-         {:ok, activity} <- insert(block_data, local),
+    block_data = make_block_data(blocker, blocked, activity_id)
+
+    with {:ok, activity} <- insert(block_data, local),
          _ <- notify_and_stream(activity),
          :ok <- maybe_federate(activity) do
       {:ok, activity}
@@ -491,8 +485,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     public = [Constants.as_public()]
 
     recipients =
-      if opts["user"],
-        do: [opts["user"].ap_id | User.following(opts["user"])] ++ public,
+      if opts[:user],
+        do: [opts[:user].ap_id | User.following(opts[:user])] ++ public,
         else: public
 
     from(activity in Activity)
@@ -500,7 +494,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> maybe_preload_bookmarks(opts)
     |> maybe_set_thread_muted_field(opts)
     |> restrict_blocked(opts)
-    |> restrict_recipients(recipients, opts["user"])
+    |> restrict_recipients(recipients, opts[:user])
     |> where(
       [activity],
       fragment(
@@ -523,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))
+    |> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts))
+    |> restrict_visibility(%{visibility: "direct"})
     |> limit(1)
     |> select([a], a.id)
     |> Repo.one()
@@ -535,24 +530,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   @spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
   def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
-    opts = Map.drop(opts, ["user"])
-
-    query = fetch_activities_query([Constants.as_public()], opts)
+    opts = Map.delete(opts, :user)
 
-    query =
-      if opts["restrict_unlisted"] do
-        restrict_unlisted(query)
-      else
-        query
-      end
-
-    Pagination.fetch_paginated(query, opts, pagination)
+    [Constants.as_public()]
+    |> fetch_activities_query(opts)
+    |> restrict_unlisted(opts)
+    |> Pagination.fetch_paginated(opts, pagination)
   end
 
   @spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
   def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
     opts
-    |> Map.put("restrict_unlisted", true)
+    |> Map.put(:restrict_unlisted, true)
     |> fetch_public_or_unlisted_activities(pagination)
   end
 
@@ -561,20 +550,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   defp restrict_visibility(query, %{visibility: visibility})
        when is_list(visibility) do
     if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
-      query =
-        from(
-          a in query,
-          where:
-            fragment(
-              "activity_visibility(?, ?, ?) = ANY (?)",
-              a.actor,
-              a.recipients,
-              a.data,
-              ^visibility
-            )
-        )
-
-      query
+      from(
+        a in query,
+        where:
+          fragment(
+            "activity_visibility(?, ?, ?) = ANY (?)",
+            a.actor,
+            a.recipients,
+            a.data,
+            ^visibility
+          )
+      )
     else
       Logger.error("Could not restrict visibility to #{visibility}")
     end
@@ -596,7 +582,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_visibility(query, _visibility), do: query
 
-  defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
+  defp exclude_visibility(query, %{exclude_visibilities: visibility})
        when is_list(visibility) do
     if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
       from(
@@ -616,7 +602,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     end
   end
 
-  defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
+  defp exclude_visibility(query, %{exclude_visibilities: visibility})
        when visibility in @valid_visibilities do
     from(
       a in query,
@@ -631,7 +617,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     )
   end
 
-  defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
+  defp exclude_visibility(query, %{exclude_visibilities: visibility})
        when visibility not in [nil | @valid_visibilities] do
     Logger.error("Could not exclude visibility to #{visibility}")
     query
@@ -642,14 +628,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
     do: query
 
-  defp restrict_thread_visibility(
-         query,
-         %{"user" => %User{skip_thread_containment: true}},
-         _
-       ),
-       do: query
+  defp restrict_thread_visibility(query, %{user: %User{skip_thread_containment: true}}, _),
+    do: query
 
-  defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do
+  defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do
     from(
       a in query,
       where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
@@ -661,87 +643,99 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   def fetch_user_abstract_activities(user, reading_user, params \\ %{}) do
     params =
       params
-      |> Map.put("user", reading_user)
-      |> Map.put("actor_id", user.ap_id)
+      |> Map.put(:user, reading_user)
+      |> Map.put(:actor_id, user.ap_id)
 
-    recipients =
-      user_activities_recipients(%{
-        "godmode" => params["godmode"],
-        "reading_user" => reading_user
-      })
-
-    fetch_activities(recipients, params)
+    %{
+      godmode: params[:godmode],
+      reading_user: reading_user
+    }
+    |> user_activities_recipients()
+    |> fetch_activities(params)
     |> Enum.reverse()
   end
 
   def fetch_user_activities(user, reading_user, params \\ %{}) do
     params =
       params
-      |> Map.put("type", ["Create", "Announce"])
-      |> Map.put("user", reading_user)
-      |> Map.put("actor_id", user.ap_id)
-      |> Map.put("pinned_activity_ids", user.pinned_activities)
+      |> Map.put(:type, ["Create", "Announce"])
+      |> Map.put(:user, reading_user)
+      |> Map.put(:actor_id, user.ap_id)
+      |> Map.put(:pinned_activity_ids, user.pinned_activities)
 
     params =
       if User.blocks?(reading_user, user) do
         params
       else
         params
-        |> Map.put("blocking_user", reading_user)
-        |> Map.put("muting_user", reading_user)
+        |> Map.put(:blocking_user, reading_user)
+        |> Map.put(:muting_user, reading_user)
       end
 
-    recipients =
-      user_activities_recipients(%{
-        "godmode" => params["godmode"],
-        "reading_user" => reading_user
-      })
-
-    fetch_activities(recipients, params)
+    %{
+      godmode: params[:godmode],
+      reading_user: reading_user
+    }
+    |> user_activities_recipients()
+    |> fetch_activities(params)
     |> Enum.reverse()
   end
 
   def fetch_statuses(reading_user, params) do
-    params =
-      params
-      |> Map.put("type", ["Create", "Announce"])
+    params = Map.put(params, :type, ["Create", "Announce"])
 
-    recipients =
-      user_activities_recipients(%{
-        "godmode" => params["godmode"],
-        "reading_user" => reading_user
-      })
-
-    fetch_activities(recipients, params, :offset)
+    %{
+      godmode: params[:godmode],
+      reading_user: reading_user
+    }
+    |> user_activities_recipients()
+    |> fetch_activities(params, :offset)
     |> Enum.reverse()
   end
 
-  defp user_activities_recipients(%{"godmode" => true}) do
-    []
-  end
+  defp user_activities_recipients(%{godmode: true}), do: []
 
-  defp user_activities_recipients(%{"reading_user" => reading_user}) do
+  defp user_activities_recipients(%{reading_user: reading_user}) do
     if reading_user do
-      [Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)]
+      [Constants.as_public()reading_user.ap_id | User.following(reading_user)]
     else
       [Constants.as_public()]
     end
   end
 
-  defp restrict_since(query, %{"since_id" => ""}), do: query
+  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" => since_id}) do
+  defp restrict_since(query, %{since_id: ""}), do: query
+
+  defp restrict_since(query, %{since_id: since_id}) do
     from(activity in query, where: activity.id > ^since_id)
   end
 
   defp restrict_since(query, _), do: query
 
-  defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do
+  defp restrict_tag_reject(_query, %{tag_reject: _tag_reject, skip_preload: true}) do
     raise "Can't use the child object without preloading!"
   end
 
-  defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
-       when is_list(tag_reject) and tag_reject != [] do
+  defp restrict_tag_reject(query, %{tag_reject: [_ | _] = tag_reject}) do
     from(
       [_activity, object] in query,
       where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
@@ -750,12 +744,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_tag_reject(query, _), do: query
 
-  defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do
+  defp restrict_tag_all(_query, %{tag_all: _tag_all, skip_preload: true}) do
     raise "Can't use the child object without preloading!"
   end
 
-  defp restrict_tag_all(query, %{"tag_all" => tag_all})
-       when is_list(tag_all) and tag_all != [] do
+  defp restrict_tag_all(query, %{tag_all: [_ | _] = tag_all}) do
     from(
       [_activity, object] in query,
       where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
@@ -764,18 +757,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_tag_all(query, _), do: query
 
-  defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do
+  defp restrict_tag(_query, %{tag: _tag, skip_preload: true}) do
     raise "Can't use the child object without preloading!"
   end
 
-  defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
+  defp restrict_tag(query, %{tag: tag}) when is_list(tag) do
     from(
       [_activity, object] in query,
       where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
     )
   end
 
-  defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
+  defp restrict_tag(query, %{tag: tag}) when is_binary(tag) do
     from(
       [_activity, object] in query,
       where: fragment("(?)->'tag' \\? (?)", object.data, ^tag)
@@ -798,35 +791,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     )
   end
 
-  defp restrict_local(query, %{"local_only" => true}) do
+  defp restrict_local(query, %{local_only: true}) do
     from(activity in query, where: activity.local == true)
   end
 
   defp restrict_local(query, _), do: query
 
-  defp restrict_actor(query, %{"actor_id" => actor_id}) do
+  defp restrict_actor(query, %{actor_id: actor_id}) do
     from(activity in query, where: activity.actor == ^actor_id)
   end
 
   defp restrict_actor(query, _), do: query
 
-  defp restrict_type(query, %{"type" => type}) when is_binary(type) do
+  defp restrict_type(query, %{type: type}) when is_binary(type) do
     from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))
   end
 
-  defp restrict_type(query, %{"type" => type}) do
+  defp restrict_type(query, %{type: type}) do
     from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type))
   end
 
   defp restrict_type(query, _), do: query
 
-  defp restrict_state(query, %{"state" => state}) do
+  defp restrict_state(query, %{state: state}) do
     from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state))
   end
 
   defp restrict_state(query, _), do: query
 
-  defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
+  defp restrict_favorited_by(query, %{favorited_by: ap_id}) do
     from(
       [_activity, object] in query,
       where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id)
@@ -835,11 +828,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_favorited_by(query, _), do: query
 
-  defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do
+  defp restrict_media(_query, %{only_media: _val, skip_preload: true}) do
     raise "Can't use the child object without preloading!"
   end
 
-  defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1"] do
+  defp restrict_media(query, %{only_media: true}) do
     from(
       [_activity, object] in query,
       where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
@@ -848,7 +841,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_media(query, _), do: query
 
-  defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "true", "1"] do
+  defp restrict_replies(query, %{exclude_replies: true}) do
     from(
       [_activity, object] in query,
       where: fragment("?->>'inReplyTo' is null", object.data)
@@ -856,8 +849,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   end
 
   defp restrict_replies(query, %{
-         "reply_filtering_user" => user,
-         "reply_visibility" => "self"
+         reply_filtering_user: user,
+         reply_visibility: "self"
        }) do
     from(
       [activity, object] in query,
@@ -872,8 +865,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   end
 
   defp restrict_replies(query, %{
-         "reply_filtering_user" => user,
-         "reply_visibility" => "following"
+         reply_filtering_user: user,
+         reply_visibility: "following"
        }) do
     from(
       [activity, object] in query,
@@ -892,16 +885,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_replies(query, _), do: query
 
-  defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val in [true, "true", "1"] do
+  defp restrict_reblogs(query, %{exclude_reblogs: true}) do
     from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
   end
 
   defp restrict_reblogs(query, _), do: query
 
-  defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
+  defp restrict_muted(query, %{with_muted: true}), do: query
 
-  defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do
-    mutes = opts["muted_users_ap_ids"] || User.muted_users_ap_ids(user)
+  defp restrict_muted(query, %{muting_user: %User{} = user} = opts) do
+    mutes = opts[:muted_users_ap_ids] || User.muted_users_ap_ids(user)
 
     query =
       from([activity] in query,
@@ -909,7 +902,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
         where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
       )
 
-    unless opts["skip_preload"] do
+    unless opts[:skip_preload] do
       from([thread_mute: tm] in query, where: is_nil(tm.user_id))
     else
       query
@@ -918,8 +911,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_muted(query, _), do: query
 
-  defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do
-    blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user)
+  defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do
+    blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
     domain_blocks = user.domain_blocks || []
 
     following_ap_ids = User.get_friends_ap_ids(user)
@@ -965,7 +958,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_blocked(query, _), do: query
 
-  defp restrict_unlisted(query) do
+  defp restrict_unlisted(query, %{restrict_unlisted: true}) do
     from(
       activity in query,
       where:
@@ -977,19 +970,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     )
   end
 
-  # TODO: when all endpoints migrated to OpenAPI compare `pinned` with `true` (boolean) only,
-  # the same for `restrict_media/2`, `restrict_replies/2`, 'restrict_reblogs/2'
-  # and `restrict_muted/2`
+  defp restrict_unlisted(query, _), do: query
 
-  defp restrict_pinned(query, %{"pinned" => pinned, "pinned_activity_ids" => ids})
-       when pinned in [true, "true", "1"] do
+  defp restrict_pinned(query, %{pinned: true, pinned_activity_ids: ids}) do
     from(activity in query, where: activity.id in ^ids)
   end
 
   defp restrict_pinned(query, _), do: query
 
-  defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do
-    muted_reblogs = opts["reblog_muted_users_ap_ids"] || User.reblog_muted_users_ap_ids(user)
+  defp restrict_muted_reblogs(query, %{muting_user: %User{} = user} = opts) do
+    muted_reblogs = opts[:reblog_muted_users_ap_ids] || User.reblog_muted_users_ap_ids(user)
 
     from(
       activity in query,
@@ -1005,7 +995,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_muted_reblogs(query, _), do: query
 
-  defp restrict_instance(query, %{"instance" => instance}) do
+  defp restrict_instance(query, %{instance: instance}) do
     users =
       from(
         u in User,
@@ -1019,7 +1009,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_instance(query, _), do: query
 
-  defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query
+  defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
 
   defp exclude_poll_votes(query, _) do
     if has_named_binding?(query, :object) do
@@ -1031,7 +1021,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     end
   end
 
-  defp exclude_invisible_actors(query, %{"invisible_actors" => true}), do: query
+  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
     invisible_ap_ids =
@@ -1042,38 +1044,38 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     from([activity] in query, where: activity.actor not in ^invisible_ap_ids)
   end
 
-  defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do
+  defp exclude_id(query, %{exclude_id: id}) when is_binary(id) do
     from(activity in query, where: activity.id != ^id)
   end
 
   defp exclude_id(query, _), do: query
 
-  defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
+  defp maybe_preload_objects(query, %{skip_preload: true}), do: query
 
   defp maybe_preload_objects(query, _) do
     query
     |> Activity.with_preloaded_object()
   end
 
-  defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query
+  defp maybe_preload_bookmarks(query, %{skip_preload: true}), do: query
 
   defp maybe_preload_bookmarks(query, opts) do
     query
-    |> Activity.with_preloaded_bookmark(opts["user"])
+    |> Activity.with_preloaded_bookmark(opts[:user])
   end
 
-  defp maybe_preload_report_notes(query, %{"preload_report_notes" => true}) do
+  defp maybe_preload_report_notes(query, %{preload_report_notes: true}) do
     query
     |> Activity.with_preloaded_report_notes()
   end
 
   defp maybe_preload_report_notes(query, _), do: query
 
-  defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query
+  defp maybe_set_thread_muted_field(query, %{skip_preload: true}), do: query
 
   defp maybe_set_thread_muted_field(query, opts) do
     query
-    |> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"])
+    |> Activity.with_set_thread_muted_field(opts[:muting_user] || opts[:user])
   end
 
   defp maybe_order(query, %{order: :desc}) do
@@ -1089,24 +1091,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   defp maybe_order(query, _), do: query
 
   defp fetch_activities_query_ap_ids_ops(opts) do
-    source_user = opts["muting_user"]
+    source_user = opts[:muting_user]
     ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: []
 
     ap_id_relationships =
-      ap_id_relationships ++
-        if opts["blocking_user"] && opts["blocking_user"] == source_user do
-          [:block]
-        else
-          []
-        end
+      if opts[:blocking_user] && opts[:blocking_user] == source_user do
+        [:block | ap_id_relationships]
+      else
+        ap_id_relationships
+      end
 
     preloaded_ap_ids = User.outgoing_relationships_ap_ids(source_user, ap_id_relationships)
 
-    restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts)
-    restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts)
+    restrict_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
+    restrict_muted_opts = Map.merge(%{muted_users_ap_ids: preloaded_ap_ids[:mute]}, opts)
 
     restrict_muted_reblogs_opts =
-      Map.merge(%{"reblog_muted_users_ap_ids" => preloaded_ap_ids[:reblog_mute]}, opts)
+      Map.merge(%{reblog_muted_users_ap_ids: preloaded_ap_ids[:reblog_mute]}, opts)
 
     {restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts}
   end
@@ -1125,7 +1126,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> maybe_preload_report_notes(opts)
     |> maybe_set_thread_muted_field(opts)
     |> maybe_order(opts)
-    |> restrict_recipients(recipients, opts["user"])
+    |> restrict_recipients(recipients, opts[:user])
     |> restrict_replies(opts)
     |> restrict_tag(opts)
     |> restrict_tag_reject(opts)
@@ -1145,19 +1146,21 @@ 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
 
   def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
-    list_memberships = Pleroma.List.memberships(opts["user"])
+    list_memberships = Pleroma.List.memberships(opts[:user])
 
     fetch_activities_query(recipients ++ list_memberships, opts)
     |> Pagination.fetch_paginated(opts, pagination)
     |> Enum.reverse()
-    |> maybe_update_cc(list_memberships, opts["user"])
+    |> maybe_update_cc(list_memberships, opts[:user])
   end
 
   @doc """
@@ -1170,19 +1173,17 @@ 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
+      Map.merge(params, %{skip_order: true}),
+      pagination
     )
   end
 
-  defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id})
-       when is_list(list_memberships) and length(list_memberships) > 0 do
+  defp maybe_update_cc(activities, [_ | _] = list_memberships, %User{ap_id: user_ap_id}) do
     Enum.map(activities, fn
-      %{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 ->
+      %{data: %{"bcc" => [_ | _] = bcc}} = activity ->
         if Enum.any?(bcc, &(&1 in list_memberships)) do
           update_in(activity.data["cc"], &[user_ap_id | &1])
         else
@@ -1196,7 +1197,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp maybe_update_cc(activities, _, _), do: activities
 
-  def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
+  defp fetch_activities_bounded_query(query, recipients, recipients_with_public) do
     from(activity in query,
       where:
         fragment("? && ?", activity.recipients, ^recipients) or
@@ -1266,8 +1267,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
         %{"type" => "Emoji"} -> true
         _ -> false
       end)
-      |> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc ->
-        Map.put(acc, String.trim(name, ":"), url)
+      |> Map.new(fn %{"icon" => %{"url" => url}, "name" => name} ->
+        {String.trim(name, ":"), url}
       end)
 
     locked = data["manuallyApprovesFollowers"] || false
@@ -1313,18 +1314,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     }
 
     # nickname can be nil because of virtual actors
-    user_data =
-      if data["preferredUsername"] do
-        Map.put(
-          user_data,
-          :nickname,
-          "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}"
-        )
-      else
-        Map.put(user_data, :nickname, nil)
-      end
-
-    {:ok, user_data}
+    if data["preferredUsername"] do
+      Map.put(
+        user_data,
+        :nickname,
+        "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}"
+      )
+    else
+      Map.put(user_data, :nickname, nil)
+    end
   end
 
   def fetch_follow_information_for_user(user) do
@@ -1399,9 +1397,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   defp collection_private(_data), do: {:ok, true}
 
   def user_data_from_user_object(data) do
-    with {:ok, data} <- MRF.filter(data),
-         {:ok, data} <- object_to_user_data(data) do
-      {:ok, data}
+    with {:ok, data} <- MRF.filter(data) do
+      {:ok, object_to_user_data(data)}
     else
       e -> {:error, e}
     end
@@ -1409,15 +1406,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   def fetch_and_prepare_user_from_ap_id(ap_id) do
     with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
-         {:ok, data} <- user_data_from_user_object(data),
-         data <- maybe_update_follow_information(data) do
-      {:ok, data}
+         {:ok, data} <- user_data_from_user_object(data) do
+      {:ok, maybe_update_follow_information(data)}
     else
-      {:error, "Object has been deleted"} = e ->
+      {:error, "Object has been deleted" = e} ->
         Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
         {:error, e}
 
-      e ->
+      {:error, e} ->
         Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
         {:error, e}
     end
@@ -1440,8 +1436,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
           |> Repo.insert()
           |> User.set_cache()
         end
-      else
-        e -> {:error, e}
       end
     end
   end
@@ -1455,7 +1449,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   end
 
   # filter out broken threads
-  def contain_broken_threads(%Activity{} = activity, %User{} = user) do
+  defp contain_broken_threads(%Activity{} = activity, %User{} = user) do
     entire_thread_visible_for_user?(activity, user)
   end
 
@@ -1466,7 +1460,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   def fetch_direct_messages_query do
     Activity
-    |> restrict_type(%{"type" => "Create"})
+    |> restrict_type(%{type: "Create"})
     |> restrict_visibility(%{visibility: "direct"})
     |> order_by([activity], asc: activity.id)
   end
index 5b84413848ce5c39746bf153a7481d9473efcc4e..f0b5c6e935eca254b6238f3d8fb44be2bd303ac2 100644 (file)
@@ -238,6 +238,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
         params
         |> Map.drop(["nickname", "page"])
         |> Map.put("include_poll_votes", true)
+        |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
 
       activities = ActivityPub.fetch_user_activities(user, for_user, params)
 
@@ -354,6 +355,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
       |> Map.drop(["nickname", "page"])
       |> Map.put("blocking_user", user)
       |> Map.put("user", user)
+      |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
 
     activities =
       [user.ap_id | User.following(user)]
index 51b74414ac6b8404426d2a459932f94dc249fa19..1aac62c69e446d3162020c20c50e6d237210dbe9 100644 (file)
@@ -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,
index a0b3af432b18ecc5cde7a84765a3f07924d7ad1a..5a4a76085dd41e95016120e4c82fc21f607fc5a7 100644 (file)
@@ -8,11 +8,8 @@ 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
 
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 (file)
index 0000000..8e47f1e
--- /dev/null
@@ -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
index a927a4ed8ec32459935351d6b3ace045bf3b5470..651aed70f00b5ae3bb5c33daa4e7230b01d9165d 100644 (file)
@@ -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],
         []
       )
 
index 2599067a87089141db941ff3cdb94a94e117ece8..c01c5f78067dc57395e0ce90ce098d86e74c112d 100644 (file)
@@ -12,6 +12,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
   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
@@ -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,17 +95,30 @@ 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
       User.get_or_fetch_by_ap_id(actor)
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 (file)
index 0000000..f53bb02
--- /dev/null
@@ -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 (file)
index 0000000..138736f
--- /dev/null
@@ -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.User
+  alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+  import Ecto.Changeset
+  import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1]
+
+  @primary_key false
+  @derive Jason.Encoder
+
+  embedded_schema do
+    field(:id, Types.ObjectID, primary_key: true)
+    field(:to, Types.Recipients, default: [])
+    field(:type, :string)
+    field(:content, Types.SafeText)
+    field(:actor, Types.ObjectID)
+    field(:published, Types.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 (file)
index 0000000..fc58240
--- /dev/null
@@ -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.Object
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+  import Ecto.Changeset
+  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+  @primary_key false
+
+  embedded_schema do
+    field(:id, Types.ObjectID, primary_key: true)
+    field(:actor, Types.ObjectID)
+    field(:type, :string)
+    field(:to, Types.Recipients, default: [])
+    field(:object, Types.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
index f42c035105444a8b48eefa53389bdff4c3ab7d0a..e5d08eb5c7f0b380fa8733b990db348ed4e27cbc 100644 (file)
@@ -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
index 48fe61e1a9793e289fb98343c32f48c042e503fb..408e0f6ee6e114521a85aabf0f8dabed9cdbafcb 100644 (file)
@@ -11,11 +11,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do
 
   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]}
+    |> Enum.reduce_while({:ok, []}, fn element, {:ok, list} ->
+      case ObjectID.cast(element) do
+        {:ok, id} ->
+          {:cont, {:ok, [id | list]}}
+
+        _ ->
+          {:halt, :error}
       end
     end)
   end
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex b/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex
new file mode 100644 (file)
index 0000000..95c9481
--- /dev/null
@@ -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.Web.ActivityPub.ObjectValidators.Types.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/url_object_validator.ex b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex
new file mode 100644 (file)
index 0000000..47e2311
--- /dev/null
@@ -0,0 +1,20 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do
+  use Ecto.Schema
+
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+  import Ecto.Changeset
+  @primary_key false
+
+  embedded_schema do
+    field(:type, :string)
+    field(:href, Types.Uri)
+    field(:mediaType, :string)
+  end
+
+  def changeset(struct, data) do
+    struct
+    |> cast(data, __schema__(:fields))
+    |> validate_required([:type, :href, :mediaType])
+  end
+end
index 0c54c4b234d75c15e24c3bb59ffe620dd9747634..6875c47f67e230e412b1f1f050bbc1e170326256 100644 (file)
@@ -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
 
index fb627545041c26f6520958dcd81c401c63e1e714..1a1cc675cc3e5b0909c44e95157702c2666e4041 100644 (file)
@@ -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
index fda1c71df1e13244e329466903db3fb506b22781..985921aa0dd451be0d8edfe6df3014a9815e4a4c 100644 (file)
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   alias Pleroma.EarmarkRenderer
   alias Pleroma.FollowingRelationship
   alias Pleroma.Maps
+  alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Object.Containment
   alias Pleroma.Repo
@@ -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
 
@@ -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),
@@ -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
index 5fce0ba639616b0fb37f9c0bc2290daed8a2c659..dfae602dfea61a6c01496f54c51fe289cb40ce05 100644 (file)
@@ -245,7 +245,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
   Inserts a full object if it is contained in an activity.
   """
   def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
-      when is_map(object_data) and type in @supported_object_types do
+      when type in @supported_object_types do
     with {:ok, object} <- Object.create(object_data) do
       map = Map.put(map, "object", object.data["id"])
 
@@ -741,13 +741,12 @@ defmodule Pleroma.Web.ActivityPub.Utils do
   def get_reports(params, page, page_size) do
     params =
       params
-      |> Map.new(fn {key, value} -> {to_string(key), value} end)
-      |> Map.put("type", "Flag")
-      |> Map.put("skip_preload", true)
-      |> Map.put("preload_report_notes", true)
-      |> Map.put("total", true)
-      |> Map.put("limit", page_size)
-      |> Map.put("offset", (page - 1) * page_size)
+      |> Map.put(:type, "Flag")
+      |> Map.put(:skip_preload, true)
+      |> Map.put(:preload_report_notes, true)
+      |> Map.put(:total, true)
+      |> Map.put(:limit, page_size)
+      |> Map.put(:offset, (page - 1) * page_size)
 
     ActivityPub.fetch_activities([], params, :offset)
   end
index bf24581ccc8c0966e64b1da1319e554f96516dcf..5cbf0dd4fceaa74e2f2843ca3cee6eb0e5ca2855 100644 (file)
@@ -16,7 +16,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Builder
   alias Pleroma.Web.ActivityPub.Pipeline
-  alias Pleroma.Web.ActivityPub.Relay
   alias Pleroma.Web.AdminAPI
   alias Pleroma.Web.AdminAPI.AccountView
   alias Pleroma.Web.AdminAPI.ModerationLogView
@@ -59,7 +58,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   plug(
     OAuthScopesPlug,
     %{scopes: ["write:follows"], admin: true}
-    when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
+    when action in [:user_follow, :user_unfollow]
   )
 
   plug(
@@ -74,7 +73,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     when action in [
            :list_log,
            :stats,
-           :relay_list,
            :need_reboot
          ]
   )
@@ -228,10 +226,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
 
     activities =
       ActivityPub.fetch_statuses(nil, %{
-        "instance" => instance,
-        "limit" => page_size,
-        "offset" => (page - 1) * page_size,
-        "exclude_reblogs" => !with_reblogs && "true"
+        instance: instance,
+        limit: page_size,
+        offset: (page - 1) * page_size,
+        exclude_reblogs: not with_reblogs
       })
 
     conn
@@ -248,9 +246,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
 
       activities =
         ActivityPub.fetch_user_activities(user, nil, %{
-          "limit" => page_size,
-          "godmode" => godmode,
-          "exclude_reblogs" => !with_reblogs && "true"
+          limit: page_size,
+          godmode: godmode,
+          exclude_reblogs: not with_reblogs
         })
 
       conn
@@ -491,50 +489,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     render_error(conn, :forbidden, "You can't revoke your own admin status.")
   end
 
-  def relay_list(conn, _params) do
-    with {:ok, list} <- Relay.list() do
-      json(conn, %{relays: list})
-    else
-      _ ->
-        conn
-        |> put_status(500)
-    end
-  end
-
-  def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
-    with {:ok, _message} <- Relay.follow(target) do
-      ModerationLog.insert_log(%{
-        action: "relay_follow",
-        actor: admin,
-        target: target
-      })
-
-      json(conn, target)
-    else
-      _ ->
-        conn
-        |> put_status(500)
-        |> json(target)
-    end
-  end
-
-  def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
-    with {:ok, _message} <- Relay.unfollow(target) do
-      ModerationLog.insert_log(%{
-        action: "relay_unfollow",
-        actor: admin,
-        target: target
-      })
-
-      json(conn, target)
-    else
-      _ ->
-        conn
-        |> put_status(500)
-        |> json(target)
-    end
-  end
-
   @doc "Get a password reset token (base64 string) for given nickname"
   def get_password_reset(conn, %{"nickname" => nickname}) do
     (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
index d6e2019bcfcefad385618d0b9a834aaf6f855af5..7f60470cbb7a4a237131f4688df9e3a1ba0a2650 100644 (file)
@@ -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/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex
new file mode 100644 (file)
index 0000000..cf9f3a1
--- /dev/null
@@ -0,0 +1,67 @@
+# 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.RelayController do
+  use Pleroma.Web, :controller
+
+  alias Pleroma.ModerationLog
+  alias Pleroma.Plugs.OAuthScopesPlug
+  alias Pleroma.Web.ActivityPub.Relay
+
+  require Logger
+
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:follows"], admin: true}
+    when action in [:follow, :unfollow]
+  )
+
+  plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :index)
+
+  action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.RelayOperation
+
+  def index(conn, _params) do
+    with {:ok, list} <- Relay.list() do
+      json(conn, %{relays: list})
+    end
+  end
+
+  def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do
+    with {:ok, _message} <- Relay.follow(target) do
+      ModerationLog.insert_log(%{
+        action: "relay_follow",
+        actor: admin,
+        target: target
+      })
+
+      json(conn, target)
+    else
+      _ ->
+        conn
+        |> put_status(500)
+        |> json(target)
+    end
+  end
+
+  def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do
+    with {:ok, _message} <- Relay.unfollow(target) do
+      ModerationLog.insert_log(%{
+        action: "relay_unfollow",
+        actor: admin,
+        target: target
+      })
+
+      json(conn, target)
+    else
+      _ ->
+        conn
+        |> put_status(500)
+        |> json(target)
+    end
+  end
+end
index 574196be855f06233f10ae2ef753204150965be3..bc48cc5278997ca9add04f1162ffe2a81fa9d2fc 100644 (file)
@@ -29,11 +29,11 @@ defmodule Pleroma.Web.AdminAPI.StatusController do
   def index(%{assigns: %{user: _admin}} = conn, params) do
     activities =
       ActivityPub.fetch_statuses(nil, %{
-        "godmode" => params.godmode,
-        "local_only" => params.local_only,
-        "limit" => params.page_size,
-        "offset" => (params.page - 1) * params.page_size,
-        "exclude_reblogs" => not params.with_reblogs
+        godmode: params.godmode,
+        local_only: params.local_only,
+        limit: params.page_size,
+        offset: (params.page - 1) * params.page_size,
+        exclude_reblogs: not params.with_reblogs
       })
 
     render(conn, "index.json", activities: activities, as: :activity)
index 120159527abefe8259b52ff4624a09ecacd23137..e1e92963251f87f79c4847e3129e8e5eaa715fde 100644 (file)
@@ -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
 
index 587ef760ee0c51ae6059cbeba789186880a249bb..d2d8b59070c0e942ee48ed8700b3abeb5bbbe447 100644 (file)
@@ -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
index f432b8c2c9e20da80512dd5e823ebf6e190232a2..773f798fe4a5010aa12acd35955f559514824a5c 100644 (file)
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
     %{
       reports:
         reports[:items]
-        |> Enum.map(&Report.extract_report_info(&1))
+        |> Enum.map(&Report.extract_report_info/1)
         |> Enum.map(&render(__MODULE__, "show.json", &1))
         |> Enum.reverse(),
       total: reports[:total]
diff --git a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex
new file mode 100644 (file)
index 0000000..7672cb4
--- /dev/null
@@ -0,0 +1,83 @@
+# 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.RelayOperation do
+  alias OpenApiSpex.Operation
+  alias OpenApiSpex.Schema
+
+  import Pleroma.Web.ApiSpec.Helpers
+
+  def open_api_operation(action) do
+    operation = String.to_existing_atom("#{action}_operation")
+    apply(__MODULE__, operation, [])
+  end
+
+  def index_operation do
+    %Operation{
+      tags: ["Admin", "Relays"],
+      summary: "List Relays",
+      operationId: "AdminAPI.RelayController.index",
+      security: [%{"oAuth" => ["read"]}],
+      responses: %{
+        200 =>
+          Operation.response("Response", "application/json", %Schema{
+            type: :object,
+            properties: %{
+              relays: %Schema{
+                type: :array,
+                items: %Schema{type: :string},
+                example: ["lain.com", "mstdn.io"]
+              }
+            }
+          })
+      }
+    }
+  end
+
+  def follow_operation do
+    %Operation{
+      tags: ["Admin", "Relays"],
+      summary: "Follow a Relay",
+      operationId: "AdminAPI.RelayController.follow",
+      security: [%{"oAuth" => ["write:follows"]}],
+      requestBody:
+        request_body("Parameters", %Schema{
+          type: :object,
+          properties: %{
+            relay_url: %Schema{type: :string, format: :uri}
+          }
+        }),
+      responses: %{
+        200 =>
+          Operation.response("Status", "application/json", %Schema{
+            type: :string,
+            example: "http://mastodon.example.org/users/admin"
+          })
+      }
+    }
+  end
+
+  def unfollow_operation do
+    %Operation{
+      tags: ["Admin", "Relays"],
+      summary: "Unfollow a Relay",
+      operationId: "AdminAPI.RelayController.unfollow",
+      security: [%{"oAuth" => ["write:follows"]}],
+      requestBody:
+        request_body("Parameters", %Schema{
+          type: :object,
+          properties: %{
+            relay_url: %Schema{type: :string, format: :uri}
+          }
+        }),
+      responses: %{
+        200 =>
+          Operation.response("Status", "application/json", %Schema{
+            type: :string,
+            example: "http://mastodon.example.org/users/admin"
+          })
+      }
+    }
+  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 (file)
index 0000000..cf299bf
--- /dev/null
@@ -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
index 46e72f8bf0c82bc0065b999f11070ca157863ec1..c966b553ae1e3aba2c2477eb78ea023958c7bf70 100644 (file)
@@ -185,6 +185,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
         "mention",
         "poll",
         "pleroma:emoji_reaction",
+        "pleroma:chat_mention",
         "move",
         "follow_request"
       ],
index ca9db01e550c3baaf2d1bca830b084828d88e2f4..0b7fad79353e7fef220c6c9044ec23de9505c8ca 100644 (file)
@@ -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"]}],
index c575a87e622e78fc0125956d0abbba29cbc22d15..775dd795de3a05cfac95b927232145f4dbe87c71 100644 (file)
@@ -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 (file)
index 0000000..b4986b7
--- /dev/null
@@ -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 (file)
index 0000000..3ee85aa
--- /dev/null
@@ -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
index 3f1a50b9604a426e3ee1b631061545d22491b111..9bcb9f5875d31403898b5d616a24d0b05cd52efa 100644 (file)
@@ -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)
 
index dbb3d7ade5d7bed0f3a855c56d1cc103a54085ec..04e081a8e88def3de6bf6139ff3a08854d612699 100644 (file)
@@ -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]),
index 6ec489f9ad646c0eb8342df933416f122aec464a..15594125fce93fd096dd1b5ffe2fc2495eb46e49 100644 (file)
@@ -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
index 9414cf3f24377414bf55e6cc90e92cbc482c1bca..69946fb81ee6a5571728586e8c8fbde681ccb351 100644 (file)
@@ -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)
 
       _ ->
         %{}
@@ -93,8 +94,7 @@ defmodule Pleroma.Web.ControllerHelper do
   end
 
   def assign_account_by_id(conn, _) do
-    # TODO: use `conn.params[:id]` only after moving to OpenAPI
-    case Pleroma.User.get_cached_by_id(conn.params[:id] || conn.params["id"]) do
+    case Pleroma.User.get_cached_by_id(conn.params.id) do
       %Pleroma.User{} = account -> assign(conn, :account, account)
       nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
     end
index 4e86cfeb58221b0cd2c8085c4393029caa042e70..39b2a766a503cc25ff8715e435d18ec6917f1c82 100644 (file)
@@ -13,8 +13,8 @@ defmodule Pleroma.Web.Feed.TagController do
     {format, tag} = parse_tag(raw_tag)
 
     activities =
-      %{"type" => ["Create"], "tag" => tag}
-      |> Pleroma.Maps.put_if_present("max_id", params["max_id"])
+      %{type: ["Create"], tag: tag}
+      |> Pleroma.Maps.put_if_present(:max_id, params["max_id"])
       |> ActivityPub.fetch_public_activities()
 
     conn
index 7c2e0d5220036f309fc9bdc5f0b02fb9c33620e4..d56f438184fa920457ad4f8d6ceaf5f5b6b932a4 100644 (file)
@@ -50,10 +50,10 @@ defmodule Pleroma.Web.Feed.UserController do
     with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
       activities =
         %{
-          "type" => ["Create"],
-          "actor_id" => user.ap_id
+          type: ["Create"],
+          actor_id: user.ap_id
         }
-        |> Pleroma.Maps.put_if_present("max_id", params["max_id"])
+        |> Pleroma.Maps.put_if_present(:max_id, params["max_id"])
         |> ActivityPub.fetch_public_or_unlisted_activities()
 
       conn
index ebfa533dd58b547f47a42c60f5449baef446c680..c38c2b895b09a8631886273141757521af4710df 100644 (file)
@@ -245,9 +245,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
       params =
         params
         |> Map.delete(:tagged)
-        |> Enum.filter(&(not is_nil(&1)))
-        |> Map.new(fn {key, value} -> {to_string(key), value} end)
-        |> Map.put("tag", params[:tagged])
+        |> Map.put(:tag, params[:tagged])
 
       activities = ActivityPub.fetch_user_activities(user, reading_user, params)
 
index 69f0e384600ae80b9698638da7c4767d141cb6b2..f35ec3596589c902da4e5a79d06ad09291c9dbf6 100644 (file)
@@ -21,7 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
 
   @doc "GET /api/v1/conversations"
   def index(%{assigns: %{user: user}} = conn, params) do
-    params = stringify_pagination_params(params)
     participations = Participation.for_user_with_last_activity_id(user, params)
 
     conn
@@ -37,20 +36,4 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
       render(conn, "participation.json", participation: participation, for: user)
     end
   end
-
-  defp stringify_pagination_params(params) do
-    atom_keys =
-      Pleroma.Pagination.page_keys()
-      |> Enum.map(&String.to_atom(&1))
-
-    str_keys =
-      params
-      |> Map.take(atom_keys)
-      |> Enum.map(fn {key, value} -> {to_string(key), value} end)
-      |> Enum.into(%{})
-
-    params
-    |> Map.delete(atom_keys)
-    |> Map.merge(str_keys)
-  end
 end
index bcd12c73f74678e36897152c6d982691520a30af..e25cef30bbf80f3f9a405ee2f2211b5deb18c51f 100644 (file)
@@ -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
index 8840fc19ce790d7c11695035705f11c037687465..3be0ca095c83f6520559fe945fabd1870782d46e 100644 (file)
@@ -124,6 +124,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
   defp prepare_tags(query, add_joined_tag \\ true) do
     tags =
       query
+      |> preprocess_uri_query()
       |> String.split(~r/[^#\w]+/u, trim: true)
       |> Enum.uniq_by(&String.downcase/1)
 
@@ -147,6 +148,20 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
     end
   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
+      query
+    end
+  end
+
   defp joined_tag(tags) do
     tags
     |> Enum.map(fn tag -> String.capitalize(tag) end)
index f20157a5f36f47067c9f158ef5dd63a6eb3d39d1..468b44b6758ea43f89d67fb7603d6c19a1dabb2b 100644 (file)
@@ -359,9 +359,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
     with %Activity{} = activity <- Activity.get_by_id(id) do
       activities =
         ActivityPub.fetch_activities_for_context(activity.data["context"], %{
-          "blocking_user" => user,
-          "user" => user,
-          "exclude_id" => activity.id
+          blocking_user: user,
+          user: user,
+          exclude_id: activity.id
         })
 
       render(conn, "context.json", activity: activity, activities: activities, user: user)
@@ -370,11 +370,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
 
   @doc "GET /api/v1/favourites"
   def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
-    params =
-      params
-      |> Map.new(fn {key, value} -> {to_string(key), value} end)
-      |> Map.take(Pleroma.Pagination.page_keys())
-
     activities = ActivityPub.fetch_favourites(user, params)
 
     conn
index f3b9285a918f9ddac622c7b5151514e04cca3921..4bdd46d7e97f99a5d0ad6f5c52938ff7a2ca7eca 100644 (file)
@@ -44,12 +44,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
   def home(%{assigns: %{user: user}} = conn, params) do
     params =
       params
-      |> Map.new(fn {key, value} -> {to_string(key), value} end)
-      |> Map.put("type", ["Create", "Announce"])
-      |> Map.put("blocking_user", user)
-      |> Map.put("muting_user", user)
-      |> Map.put("reply_filtering_user", user)
-      |> Map.put("user", user)
+      |> Map.put(:type, ["Create", "Announce"])
+      |> 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 =
       [user.ap_id | User.following(user)]
@@ -69,10 +69,9 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
   def direct(%{assigns: %{user: user}} = conn, params) do
     params =
       params
-      |> Map.new(fn {key, value} -> {to_string(key), value} end)
-      |> Map.put("type", "Create")
-      |> Map.put("blocking_user", user)
-      |> Map.put("user", user)
+      |> Map.put(:type, "Create")
+      |> Map.put(:blocking_user, user)
+      |> Map.put(:user, user)
       |> Map.put(:visibility, "direct")
 
     activities =
@@ -91,9 +90,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
 
   # GET /api/v1/timelines/public
   def public(%{assigns: %{user: user}} = conn, params) do
-    params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
-
-    local_only = params["local"]
+    local_only = params[:local]
 
     cfg_key =
       if local_only do
@@ -109,11 +106,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
     else
       activities =
         params
-        |> Map.put("type", ["Create"])
-        |> Map.put("local_only", local_only)
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("reply_filtering_user", user)
+        |> Map.put(:type, ["Create"])
+        |> Map.put(:local_only, local_only)
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:reply_filtering_user, user)
         |> ActivityPub.fetch_public_activities()
 
       conn
@@ -128,39 +125,38 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
 
   defp hashtag_fetching(params, user, local_only) do
     tags =
-      [params["tag"], params["any"]]
+      [params[:tag], params[:any]]
       |> List.flatten()
       |> Enum.uniq()
-      |> Enum.filter(& &1)
-      |> Enum.map(&String.downcase(&1))
+      |> Enum.reject(&is_nil/1)
+      |> Enum.map(&String.downcase/1)
 
     tag_all =
       params
-      |> Map.get("all", [])
-      |> Enum.map(&String.downcase(&1))
+      |> Map.get(:all, [])
+      |> Enum.map(&String.downcase/1)
 
     tag_reject =
       params
-      |> Map.get("none", [])
-      |> Enum.map(&String.downcase(&1))
+      |> Map.get(:none, [])
+      |> Enum.map(&String.downcase/1)
 
     _activities =
       params
-      |> Map.put("type", "Create")
-      |> Map.put("local_only", local_only)
-      |> Map.put("blocking_user", user)
-      |> Map.put("muting_user", user)
-      |> Map.put("user", user)
-      |> Map.put("tag", tags)
-      |> Map.put("tag_all", tag_all)
-      |> Map.put("tag_reject", tag_reject)
+      |> Map.put(:type, "Create")
+      |> Map.put(:local_only, local_only)
+      |> Map.put(:blocking_user, user)
+      |> Map.put(:muting_user, user)
+      |> Map.put(:user, user)
+      |> Map.put(:tag, tags)
+      |> Map.put(:tag_all, tag_all)
+      |> Map.put(:tag_reject, tag_reject)
       |> ActivityPub.fetch_public_activities()
   end
 
   # GET /api/v1/timelines/tag/:tag
   def hashtag(%{assigns: %{user: user}} = conn, params) do
-    params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
-    local_only = params["local"]
+    local_only = params[:local]
     activities = hashtag_fetching(params, user, local_only)
 
     conn
index 70da64a7a8f0fe8d7e7e36181f263a942109e1e9..694bf5ca8b9216dc593577ee0243b032065ba06b 100644 (file)
@@ -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
index 5326b02c637c312cb8197e3b7ccaf7ccf58633b4..68beb69b8f91bab2ab75f0b78029800ce69a615a 100644 (file)
@@ -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,
index 2b6f84c72bbea703236178cb8c78f2f93eb13c05..06f0c172865406bd10f2a8c70a8583e704dd1fd5 100644 (file)
@@ -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)
index 6a630eafac491d87a524e39090a98be294ccd25d..c498fe6326084e5302502a277fc29c145ba00d73 100644 (file)
@@ -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
index c46ddcf5578f456b30c18cdce99ff3a9b0baccd0..3865be2801a5b4ea36e0bb4262c07cd847842ca0 100644 (file)
@@ -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,48 @@ 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_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 +131,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)
index 8e37150931a9c4c3a0ab7c1ff510e226c5091e49..2c49bedb36760f23698836d988eb952845b9c982 100644 (file)
@@ -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
       }
index 0a3f45620581558c01c2378cad3f125671d8d96e..f3554d919990613cc04a1d77d0310ae588916d9d 100644 (file)
@@ -126,10 +126,9 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
   def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
     params =
       params
-      |> Map.new(fn {key, value} -> {to_string(key), value} end)
-      |> Map.put("type", "Create")
-      |> Map.put("favorited_by", user.ap_id)
-      |> Map.put("blocking_user", for_user)
+      |> Map.put(:type, "Create")
+      |> Map.put(:favorited_by, user.ap_id)
+      |> Map.put(:blocking_user, for_user)
 
     recipients =
       if for_user do
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 (file)
index 0000000..c8ef3d9
--- /dev/null
@@ -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
index 21d5eb8d5cae109309293500918bb6a4424570ec..3d007f3245136e31e604f530c2bbd0e647889774 100644 (file)
@@ -42,15 +42,14 @@ defmodule Pleroma.Web.PleromaAPI.ConversationController do
            Participation.get(participation_id, preload: [:conversation]) do
       params =
         params
-        |> Map.new(fn {key, value} -> {to_string(key), value} end)
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("user", user)
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:user, user)
 
       activities =
         participation.conversation.ap_id
         |> ActivityPub.fetch_activities_for_context_query(params)
-        |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false))
+        |> Pleroma.Pagination.fetch_paginated(Map.put(params, :total, false))
         |> Enum.reverse()
 
       conn
index 8665ca56ca667b5525c6cf288478d65d862d9c97..e9a4fba92df552c55179dd32950d32f73a51fce3 100644 (file)
@@ -36,10 +36,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
 
   def index(%{assigns: %{user: reading_user}} = conn, %{id: id} = params) do
     with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do
-      params =
-        params
-        |> Map.new(fn {key, value} -> {to_string(key), value} end)
-        |> Map.put("type", ["Listen"])
+      params = Map.put(params, :type, ["Listen"])
 
       activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params)
 
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 (file)
index 0000000..f2112a8
--- /dev/null
@@ -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 (file)
index 0000000..1c996da
--- /dev/null
@@ -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
index 6917257022ee2752c4679b79d3e7dcfb372b212b..cdb827e7664820f7d08088ad489a7e7f21a033ef 100644 (file)
@@ -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
index 3e401a49026231941bb6583908ab0ce992ca12da..5b5aa0d597567fb80cf5521f66628d4e09d86fae 100644 (file)
@@ -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)
index 9d3d7f978b10b48c9afc3960a222fb85bcfb4061..1729141e996bba5fc9de39846da505390e311f0a 100644 (file)
@@ -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
index 40980def8198d134496ed429a3efd494352c4c95..ef5ead2daf1fdcad1309591344223c07513f5136 100644 (file)
@@ -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
index ae0f36702ef36492458e5ed1a22183077a4da7c2..3d577e2540ef71c460945d41e39ec9c148158227 100644 (file)
@@ -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
 
index 8f32bf91b3123c16ca683b2902ba5d8c6523d3df..6bdeac89c21158165231569bea381967167cef9f 100644 (file)
@@ -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
index 3e90125882e2f2d58799584bd6be4b726f4b311b..b3b3b059cfcae206ed35367a59f403e49deda922 100644 (file)
@@ -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
index 09d4b526e4b55341b4699634b8984c789b888370..4a04865d2925e5f12b294b58c6f37975eee97901 100644 (file)
@@ -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
index f912cb930147bc1dc0b0444ca58460c27dcb9730..57570b672227ceab3aa7328c01d9f04e75d4bd82 100644 (file)
@@ -160,9 +160,9 @@ defmodule Pleroma.Web.Router do
       :right_delete_multiple
     )
 
-    get("/relay", AdminAPIController, :relay_list)
-    post("/relay", AdminAPIController, :relay_follow)
-    delete("/relay", AdminAPIController, :relay_unfollow)
+    get("/relay", RelayController, :index)
+    post("/relay", RelayController, :follow)
+    delete("/relay", RelayController, :unfollow)
 
     post("/users/invite_token", InviteController, :create)
     get("/users/invites", InviteController, :index)
@@ -306,6 +306,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)
index c3efb66513304ac4b254e6d6cc445419a06929f5..a7a891b133ea6e8fd28b7601cb7d31385803f7f0 100644 (file)
@@ -111,8 +111,14 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
       %User{} = user ->
         meta = Metadata.build_tags(%{user: user})
 
+        params =
+          params
+          |> Map.take(@page_keys)
+          |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
+
         timeline =
-          ActivityPub.fetch_user_activities(user, nil, Map.take(params, @page_keys))
+          user
+          |> ActivityPub.fetch_user_activities(nil, params)
           |> Enum.map(&represent/1)
 
         prev_page_id =
index 0cf41189b2f7b75e06dad3d1135b46f7a38f9562..d1d2c9b9c5f85bfa53bd48168a5796a8523c80b1 100644 (file)
@@ -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")
 
index 237b29ded98ba4082ba6b49e22f7efb267400e42..476a3324513a1f42d2b8f13022d1a740f20ad464 100644 (file)
@@ -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/mix.exs b/mix.exs
index 03b060bc0ff698c1a0237b38bbc350982edf2a26..6040c994ea4612977d45d60e263ecd8e930d2219 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -5,7 +5,7 @@ defmodule Pleroma.Mixfile do
     [
       app: :pleroma,
       version: version("2.0.50"),
-      elixir: "~> 1.8",
+      elixir: "~> 1.9",
       elixirc_paths: elixirc_paths(Mix.env()),
       compilers: [:phoenix, :gettext] ++ Mix.compilers(),
       elixirc_options: [warnings_as_errors: warnings_as_errors(Mix.env())],
diff --git a/priv/repo/migrations/20200309123730_create_chats.exs b/priv/repo/migrations/20200309123730_create_chats.exs
new file mode 100644 (file)
index 0000000..715d798
--- /dev/null
@@ -0,0 +1,16 @@
+defmodule Pleroma.Repo.Migrations.CreateChats do
+  use Ecto.Migration
+
+  def change do
+    create table(:chats) do
+      add(:user_id, references(:users, type: :uuid))
+      # Recipient is an ActivityPub id, to future-proof for group support.
+      add(:recipient, :string)
+      add(:unread, :integer, default: 0)
+      timestamps()
+    end
+
+    # There's only one chat between a user and a recipient.
+    create(index(:chats, [:user_id, :recipient], unique: true))
+  end
+end
diff --git a/priv/repo/migrations/20200527163635_delete_notifications_from_invisible_users.exs b/priv/repo/migrations/20200527163635_delete_notifications_from_invisible_users.exs
new file mode 100644 (file)
index 0000000..9e95a81
--- /dev/null
@@ -0,0 +1,18 @@
+defmodule Pleroma.Repo.Migrations.DeleteNotificationsFromInvisibleUsers do
+  use Ecto.Migration
+
+  import Ecto.Query
+  alias Pleroma.Repo
+
+  def up do
+    Pleroma.Notification
+    |> join(:inner, [n], activity in assoc(n, :activity))
+    |> where(
+      [n, a],
+      fragment("? in (SELECT ap_id FROM users WHERE invisible = true)", a.actor)
+    )
+    |> Repo.delete_all()
+  end
+
+  def down, do: :ok
+end
diff --git a/priv/repo/migrations/20200602094828_add_type_to_notifications.exs b/priv/repo/migrations/20200602094828_add_type_to_notifications.exs
new file mode 100644 (file)
index 0000000..19c7336
--- /dev/null
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.AddTypeToNotifications do
+  use Ecto.Migration
+
+  def change do
+    alter table(:notifications) do
+      add(:type, :string)
+    end
+  end
+end
diff --git a/priv/repo/migrations/20200602125218_backfill_notification_types.exs b/priv/repo/migrations/20200602125218_backfill_notification_types.exs
new file mode 100644 (file)
index 0000000..996d721
--- /dev/null
@@ -0,0 +1,10 @@
+defmodule Pleroma.Repo.Migrations.BackfillNotificationTypes do
+  use Ecto.Migration
+
+  def up do
+    Pleroma.MigrationHelper.NotificationBackfill.fill_in_notification_types()
+  end
+
+  def down do
+  end
+end
diff --git a/priv/repo/migrations/20200602150528_create_chat_message_reference.exs b/priv/repo/migrations/20200602150528_create_chat_message_reference.exs
new file mode 100644 (file)
index 0000000..6f9148b
--- /dev/null
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Repo.Migrations.CreateChatMessageReference do
+  use Ecto.Migration
+
+  def change do
+    create table(:chat_message_references, primary_key: false) do
+      add(:id, :uuid, primary_key: true)
+      add(:chat_id, references(:chats, on_delete: :delete_all), null: false)
+      add(:object_id, references(:objects, on_delete: :delete_all), null: false)
+      add(:seen, :boolean, default: false, null: false)
+
+      timestamps()
+    end
+
+    create(index(:chat_message_references, [:chat_id, "id desc"]))
+  end
+end
diff --git a/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs b/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs
new file mode 100644 (file)
index 0000000..fdf8513
--- /dev/null
@@ -0,0 +1,7 @@
+defmodule Pleroma.Repo.Migrations.AddUniqueIndexToChatMessageReferences do
+  use Ecto.Migration
+
+  def change do
+    create(unique_index(:chat_message_references, [:object_id, :chat_id]))
+  end
+end
diff --git a/priv/repo/migrations/20200603120448_remove_unread_from_chats.exs b/priv/repo/migrations/20200603120448_remove_unread_from_chats.exs
new file mode 100644 (file)
index 0000000..6322137
--- /dev/null
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.RemoveUnreadFromChats do
+  use Ecto.Migration
+
+  def change do
+    alter table(:chats) do
+      remove(:unread, :integer, default: 0)
+    end
+  end
+end
diff --git a/priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs b/priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs
new file mode 100644 (file)
index 0000000..a5065d6
--- /dev/null
@@ -0,0 +1,12 @@
+defmodule Pleroma.Repo.Migrations.AddSeenIndexToChatMessageReferences do
+  use Ecto.Migration
+
+  def change do
+    create(
+      index(:chat_message_references, [:chat_id],
+        where: "seen = false",
+        name: "unseen_messages_count_index"
+      )
+    )
+  end
+end
diff --git a/priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs b/priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs
new file mode 100644 (file)
index 0000000..fd6bc7b
--- /dev/null
@@ -0,0 +1,30 @@
+defmodule Pleroma.Repo.Migrations.MigrateSeenToUnreadInChatMessageReferences do
+  use Ecto.Migration
+
+  def change do
+    drop(
+      index(:chat_message_references, [:chat_id],
+        where: "seen = false",
+        name: "unseen_messages_count_index"
+      )
+    )
+
+    alter table(:chat_message_references) do
+      add(:unread, :boolean, default: true)
+    end
+
+    execute("update chat_message_references set unread = not seen")
+
+    alter table(:chat_message_references) do
+      modify(:unread, :boolean, default: true, null: false)
+      remove(:seen, :boolean, default: false, null: false)
+    end
+
+    create(
+      index(:chat_message_references, [:chat_id],
+        where: "unread = true",
+        name: "unread_messages_count_index"
+      )
+    )
+  end
+end
diff --git a/priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs b/priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs
new file mode 100644 (file)
index 0000000..9ea3443
--- /dev/null
@@ -0,0 +1,36 @@
+defmodule Pleroma.Repo.Migrations.ChangeTypeToEnumForNotifications do
+  use Ecto.Migration
+
+  def up do
+    """
+    create type notification_type as enum (
+      'follow',
+      'follow_request',
+      'mention',
+      'move',
+      'pleroma:emoji_reaction',
+      'pleroma:chat_mention',
+      'reblog',
+      'favourite'
+    )
+    """
+    |> execute()
+
+    """
+    alter table notifications 
+    alter column type type notification_type using (type::notification_type)
+    """
+    |> execute()
+  end
+
+  def down do
+    alter table(:notifications) do
+      modify(:type, :string)
+    end
+
+    """
+    drop type notification_type
+    """
+    |> execute()
+  end
+end
diff --git a/priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs b/priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs
new file mode 100644 (file)
index 0000000..f14e269
--- /dev/null
@@ -0,0 +1,23 @@
+defmodule Pleroma.Repo.Migrations.ChangeChatIdToFlake do
+  use Ecto.Migration
+
+  def up do
+    execute("""
+    alter table chats
+    drop constraint chats_pkey cascade,
+    alter column id drop default,
+    alter column id set data type uuid using cast( lpad( to_hex(id), 32, '0') as uuid),
+    add primary key (id)
+    """)
+
+    execute("""
+    alter table chat_message_references
+    alter column chat_id set data type uuid using cast( lpad( to_hex(chat_id), 32, '0') as uuid),
+    add constraint chat_message_references_chat_id_fkey foreign key (chat_id) references chats(id) on delete cascade
+    """)
+  end
+
+  def down do
+    :ok
+  end
+end
index 278ad2f96f6a9eeb7bf86e005893bab6d2491f4b..7cc3fee40875f7d8be2c92896da7acf7e4d76fcd 100644 (file)
@@ -30,6 +30,7 @@
                 "@type": "@id"
             },
             "EmojiReact": "litepub:EmojiReact",
+            "ChatMessage": "litepub:ChatMessage",
             "alsoKnownAs": {
                 "@id": "as:alsoKnownAs",
                 "@type": "@id"
diff --git a/test/chat/message_reference_test.exs b/test/chat/message_reference_test.exs
new file mode 100644 (file)
index 0000000..aaa7c1a
--- /dev/null
@@ -0,0 +1,29 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Chat.MessageReferenceTest do
+  use Pleroma.DataCase, async: true
+
+  alias Pleroma.Chat
+  alias Pleroma.Chat.MessageReference
+  alias Pleroma.Web.CommonAPI
+
+  import Pleroma.Factory
+
+  describe "messages" do
+    test "it returns the last message in a chat" do
+      user = insert(:user)
+      recipient = insert(:user)
+
+      {:ok, _message_1} = CommonAPI.post_chat_message(user, recipient, "hey")
+      {:ok, _message_2} = CommonAPI.post_chat_message(recipient, user, "ho")
+
+      {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id)
+
+      message = MessageReference.last_message_for_chat(chat)
+
+      assert message.object.data["content"] == "ho"
+    end
+  end
+end
diff --git a/test/chat_test.exs b/test/chat_test.exs
new file mode 100644 (file)
index 0000000..332f218
--- /dev/null
@@ -0,0 +1,61 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ChatTest do
+  use Pleroma.DataCase, async: true
+
+  alias Pleroma.Chat
+
+  import Pleroma.Factory
+
+  describe "creation and getting" do
+    test "it only works if the recipient is a valid user (for now)" do
+      user = insert(:user)
+
+      assert {:error, _chat} = Chat.bump_or_create(user.id, "http://some/nonexisting/account")
+      assert {:error, _chat} = Chat.get_or_create(user.id, "http://some/nonexisting/account")
+    end
+
+    test "it creates a chat for a user and recipient" do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
+
+      assert chat.id
+    end
+
+    test "it returns and bumps a chat for a user and recipient if it already exists" do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
+      {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id)
+
+      assert chat.id == chat_two.id
+    end
+
+    test "it returns a chat for a user and recipient if it already exists" do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+      {:ok, chat_two} = Chat.get_or_create(user.id, other_user.ap_id)
+
+      assert chat.id == chat_two.id
+    end
+
+    test "a returning chat will have an updated `update_at` field" do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
+      :timer.sleep(1500)
+      {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id)
+
+      assert chat.id == chat_two.id
+      assert chat.updated_at != chat_two.updated_at
+    end
+  end
+end
index 336de735900b12806295cfb2810ae855e97af477..3895e2cdac32f7e0132a998625c45d22de744b52 100644 (file)
@@ -7,40 +7,28 @@ defmodule Pleroma.ConfigDBTest do
   import Pleroma.Factory
   alias Pleroma.ConfigDB
 
-  test "get_by_key/1" do
+  test "get_by_params/1" do
     config = insert(:config)
     insert(:config)
 
     assert config == ConfigDB.get_by_params(%{group: config.group, key: config.key})
   end
 
-  test "create/1" do
-    {:ok, config} = ConfigDB.create(%{group: ":pleroma", key: ":some_key", value: "some_value"})
-    assert config == ConfigDB.get_by_params(%{group: ":pleroma", key: ":some_key"})
-  end
-
-  test "update/1" do
-    config = insert(:config)
-    {:ok, updated} = ConfigDB.update(config, %{value: "some_value"})
-    loaded = ConfigDB.get_by_params(%{group: config.group, key: config.key})
-    assert loaded == updated
-  end
-
   test "get_all_as_keyword/0" do
     saved = insert(:config)
-    insert(:config, group: ":quack", key: ":level", value: ConfigDB.to_binary(:info))
-    insert(:config, group: ":quack", key: ":meta", value: ConfigDB.to_binary([:none]))
+    insert(:config, group: ":quack", key: ":level", value: :info)
+    insert(:config, group: ":quack", key: ":meta", value: [:none])
 
     insert(:config,
       group: ":quack",
       key: ":webhook_url",
-      value: ConfigDB.to_binary("https://hooks.slack.com/services/KEY/some_val")
+      value: "https://hooks.slack.com/services/KEY/some_val"
     )
 
     config = ConfigDB.get_all_as_keyword()
 
     assert config[:pleroma] == [
-             {ConfigDB.from_string(saved.key), ConfigDB.from_binary(saved.value)}
+             {saved.key, saved.value}
            ]
 
     assert config[:quack][:level] == :info
@@ -51,11 +39,11 @@ defmodule Pleroma.ConfigDBTest do
   describe "update_or_create/1" do
     test "common" do
       config = insert(:config)
-      key2 = "another_key"
+      key2 = :another_key
 
       params = [
-        %{group: "pleroma", key: key2, value: "another_value"},
-        %{group: config.group, key: config.key, value: "new_value"}
+        %{group: :pleroma, key: key2, value: "another_value"},
+        %{group: :pleroma, key: config.key, value: [a: 1, b: 2, c: "new_value"]}
       ]
 
       assert Repo.all(ConfigDB) |> length() == 1
@@ -65,16 +53,16 @@ defmodule Pleroma.ConfigDBTest do
       assert Repo.all(ConfigDB) |> length() == 2
 
       config1 = ConfigDB.get_by_params(%{group: config.group, key: config.key})
-      config2 = ConfigDB.get_by_params(%{group: "pleroma", key: key2})
+      config2 = ConfigDB.get_by_params(%{group: :pleroma, key: key2})
 
-      assert config1.value == ConfigDB.transform("new_value")
-      assert config2.value == ConfigDB.transform("another_value")
+      assert config1.value == [a: 1, b: 2, c: "new_value"]
+      assert config2.value == "another_value"
     end
 
     test "partial update" do
-      config = insert(:config, value: ConfigDB.to_binary(key1: "val1", key2: :val2))
+      config = insert(:config, value: [key1: "val1", key2: :val2])
 
-      {:ok, _config} =
+      {:ok, config} =
         ConfigDB.update_or_create(%{
           group: config.group,
           key: config.key,
@@ -83,15 +71,14 @@ defmodule Pleroma.ConfigDBTest do
 
       updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
 
-      value = ConfigDB.from_binary(updated.value)
-      assert length(value) == 3
-      assert value[:key1] == :val1
-      assert value[:key2] == :val2
-      assert value[:key3] == :val3
+      assert config.value == updated.value
+      assert updated.value[:key1] == :val1
+      assert updated.value[:key2] == :val2
+      assert updated.value[:key3] == :val3
     end
 
     test "deep merge" do
-      config = insert(:config, value: ConfigDB.to_binary(key1: "val1", key2: [k1: :v1, k2: "v2"]))
+      config = insert(:config, value: [key1: "val1", key2: [k1: :v1, k2: "v2"]])
 
       {:ok, config} =
         ConfigDB.update_or_create(%{
@@ -103,18 +90,15 @@ defmodule Pleroma.ConfigDBTest do
       updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
 
       assert config.value == updated.value
-
-      value = ConfigDB.from_binary(updated.value)
-      assert value[:key1] == :val1
-      assert value[:key2] == [k1: :v1, k2: :v2, k3: :v3]
-      assert value[:key3] == :val3
+      assert updated.value[:key1] == :val1
+      assert updated.value[:key2] == [k1: :v1, k2: :v2, k3: :v3]
+      assert updated.value[:key3] == :val3
     end
 
     test "only full update for some keys" do
-      config1 = insert(:config, key: ":ecto_repos", value: ConfigDB.to_binary(repo: Pleroma.Repo))
+      config1 = insert(:config, key: :ecto_repos, value: [repo: Pleroma.Repo])
 
-      config2 =
-        insert(:config, group: ":cors_plug", key: ":max_age", value: ConfigDB.to_binary(18))
+      config2 = insert(:config, group: :cors_plug, key: :max_age, value: 18)
 
       {:ok, _config} =
         ConfigDB.update_or_create(%{
@@ -133,8 +117,8 @@ defmodule Pleroma.ConfigDBTest do
       updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key})
       updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key})
 
-      assert ConfigDB.from_binary(updated1.value) == [another_repo: [Pleroma.Repo]]
-      assert ConfigDB.from_binary(updated2.value) == 777
+      assert updated1.value == [another_repo: [Pleroma.Repo]]
+      assert updated2.value == 777
     end
 
     test "full update if value is not keyword" do
@@ -142,7 +126,7 @@ defmodule Pleroma.ConfigDBTest do
         insert(:config,
           group: ":tesla",
           key: ":adapter",
-          value: ConfigDB.to_binary(Tesla.Adapter.Hackney)
+          value: Tesla.Adapter.Hackney
         )
 
       {:ok, _config} =
@@ -154,20 +138,20 @@ defmodule Pleroma.ConfigDBTest do
 
       updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
 
-      assert ConfigDB.from_binary(updated.value) == Tesla.Adapter.Httpc
+      assert updated.value == Tesla.Adapter.Httpc
     end
 
     test "only full update for some subkeys" do
       config1 =
         insert(:config,
           key: ":emoji",
-          value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1])
+          value: [groups: [a: 1, b: 2], key: [a: 1]]
         )
 
       config2 =
         insert(:config,
           key: ":assets",
-          value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1])
+          value: [mascots: [a: 1, b: 2], key: [a: 1]]
         )
 
       {:ok, _config} =
@@ -187,8 +171,8 @@ defmodule Pleroma.ConfigDBTest do
       updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key})
       updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key})
 
-      assert ConfigDB.from_binary(updated1.value) == [groups: [c: 3, d: 4], key: [a: 1, b: 2]]
-      assert ConfigDB.from_binary(updated2.value) == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]]
+      assert updated1.value == [groups: [c: 3, d: 4], key: [a: 1, b: 2]]
+      assert updated2.value == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]]
     end
   end
 
@@ -206,14 +190,14 @@ defmodule Pleroma.ConfigDBTest do
     end
 
     test "partial subkeys delete" do
-      config = insert(:config, value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1]))
+      config = insert(:config, value: [groups: [a: 1, b: 2], key: [a: 1]])
 
       {:ok, deleted} =
         ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]})
 
       assert Ecto.get_meta(deleted, :state) == :loaded
 
-      assert deleted.value == ConfigDB.to_binary(key: [a: 1])
+      assert deleted.value == [key: [a: 1]]
 
       updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
 
@@ -221,7 +205,7 @@ defmodule Pleroma.ConfigDBTest do
     end
 
     test "full delete if remaining value after subkeys deletion is empty list" do
-      config = insert(:config, value: ConfigDB.to_binary(groups: [a: 1, b: 2]))
+      config = insert(:config, value: [groups: [a: 1, b: 2]])
 
       {:ok, deleted} =
         ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]})
@@ -232,234 +216,159 @@ defmodule Pleroma.ConfigDBTest do
     end
   end
 
-  describe "transform/1" do
+  describe "to_elixir_types/1" do
     test "string" do
-      binary = ConfigDB.transform("value as string")
-      assert binary == :erlang.term_to_binary("value as string")
-      assert ConfigDB.from_binary(binary) == "value as string"
+      assert ConfigDB.to_elixir_types("value as string") == "value as string"
     end
 
     test "boolean" do
-      binary = ConfigDB.transform(false)
-      assert binary == :erlang.term_to_binary(false)
-      assert ConfigDB.from_binary(binary) == false
+      assert ConfigDB.to_elixir_types(false) == false
     end
 
     test "nil" do
-      binary = ConfigDB.transform(nil)
-      assert binary == :erlang.term_to_binary(nil)
-      assert ConfigDB.from_binary(binary) == nil
+      assert ConfigDB.to_elixir_types(nil) == nil
     end
 
     test "integer" do
-      binary = ConfigDB.transform(150)
-      assert binary == :erlang.term_to_binary(150)
-      assert ConfigDB.from_binary(binary) == 150
+      assert ConfigDB.to_elixir_types(150) == 150
     end
 
     test "atom" do
-      binary = ConfigDB.transform(":atom")
-      assert binary == :erlang.term_to_binary(:atom)
-      assert ConfigDB.from_binary(binary) == :atom
+      assert ConfigDB.to_elixir_types(":atom") == :atom
     end
 
     test "ssl options" do
-      binary = ConfigDB.transform([":tlsv1", ":tlsv1.1", ":tlsv1.2"])
-      assert binary == :erlang.term_to_binary([:tlsv1, :"tlsv1.1", :"tlsv1.2"])
-      assert ConfigDB.from_binary(binary) == [:tlsv1, :"tlsv1.1", :"tlsv1.2"]
+      assert ConfigDB.to_elixir_types([":tlsv1", ":tlsv1.1", ":tlsv1.2"]) == [
+               :tlsv1,
+               :"tlsv1.1",
+               :"tlsv1.2"
+             ]
     end
 
     test "pleroma module" do
-      binary = ConfigDB.transform("Pleroma.Bookmark")
-      assert binary == :erlang.term_to_binary(Pleroma.Bookmark)
-      assert ConfigDB.from_binary(binary) == Pleroma.Bookmark
+      assert ConfigDB.to_elixir_types("Pleroma.Bookmark") == Pleroma.Bookmark
     end
 
     test "pleroma string" do
-      binary = ConfigDB.transform("Pleroma")
-      assert binary == :erlang.term_to_binary("Pleroma")
-      assert ConfigDB.from_binary(binary) == "Pleroma"
+      assert ConfigDB.to_elixir_types("Pleroma") == "Pleroma"
     end
 
     test "phoenix module" do
-      binary = ConfigDB.transform("Phoenix.Socket.V1.JSONSerializer")
-      assert binary == :erlang.term_to_binary(Phoenix.Socket.V1.JSONSerializer)
-      assert ConfigDB.from_binary(binary) == Phoenix.Socket.V1.JSONSerializer
+      assert ConfigDB.to_elixir_types("Phoenix.Socket.V1.JSONSerializer") ==
+               Phoenix.Socket.V1.JSONSerializer
     end
 
     test "tesla module" do
-      binary = ConfigDB.transform("Tesla.Adapter.Hackney")
-      assert binary == :erlang.term_to_binary(Tesla.Adapter.Hackney)
-      assert ConfigDB.from_binary(binary) == Tesla.Adapter.Hackney
+      assert ConfigDB.to_elixir_types("Tesla.Adapter.Hackney") == Tesla.Adapter.Hackney
     end
 
     test "ExSyslogger module" do
-      binary = ConfigDB.transform("ExSyslogger")
-      assert binary == :erlang.term_to_binary(ExSyslogger)
-      assert ConfigDB.from_binary(binary) == ExSyslogger
+      assert ConfigDB.to_elixir_types("ExSyslogger") == ExSyslogger
     end
 
     test "Quack.Logger module" do
-      binary = ConfigDB.transform("Quack.Logger")
-      assert binary == :erlang.term_to_binary(Quack.Logger)
-      assert ConfigDB.from_binary(binary) == Quack.Logger
+      assert ConfigDB.to_elixir_types("Quack.Logger") == Quack.Logger
     end
 
     test "Swoosh.Adapters modules" do
-      binary = ConfigDB.transform("Swoosh.Adapters.SMTP")
-      assert binary == :erlang.term_to_binary(Swoosh.Adapters.SMTP)
-      assert ConfigDB.from_binary(binary) == Swoosh.Adapters.SMTP
-      binary = ConfigDB.transform("Swoosh.Adapters.AmazonSES")
-      assert binary == :erlang.term_to_binary(Swoosh.Adapters.AmazonSES)
-      assert ConfigDB.from_binary(binary) == Swoosh.Adapters.AmazonSES
+      assert ConfigDB.to_elixir_types("Swoosh.Adapters.SMTP") == Swoosh.Adapters.SMTP
+      assert ConfigDB.to_elixir_types("Swoosh.Adapters.AmazonSES") == Swoosh.Adapters.AmazonSES
     end
 
     test "sigil" do
-      binary = ConfigDB.transform("~r[comp[lL][aA][iI][nN]er]")
-      assert binary == :erlang.term_to_binary(~r/comp[lL][aA][iI][nN]er/)
-      assert ConfigDB.from_binary(binary) == ~r/comp[lL][aA][iI][nN]er/
+      assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/
     end
 
     test "link sigil" do
-      binary = ConfigDB.transform("~r/https:\/\/example.com/")
-      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/)
-      assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/
+      assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/
     end
 
     test "link sigil with um modifiers" do
-      binary = ConfigDB.transform("~r/https:\/\/example.com/um")
-      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/um)
-      assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/um
+      assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um") ==
+               ~r/https:\/\/example.com/um
     end
 
     test "link sigil with i modifier" do
-      binary = ConfigDB.transform("~r/https:\/\/example.com/i")
-      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/i)
-      assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/i
+      assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i
     end
 
     test "link sigil with s modifier" do
-      binary = ConfigDB.transform("~r/https:\/\/example.com/s")
-      assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/s)
-      assert ConfigDB.from_binary(binary) == ~r/https:\/\/example.com/s
+      assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s") == ~r/https:\/\/example.com/s
     end
 
     test "raise if valid delimiter not found" do
       assert_raise ArgumentError, "valid delimiter for Regex expression not found", fn ->
-        ConfigDB.transform("~r/https://[]{}<>\"'()|example.com/s")
+        ConfigDB.to_elixir_types("~r/https://[]{}<>\"'()|example.com/s")
       end
     end
 
     test "2 child tuple" do
-      binary = ConfigDB.transform(%{"tuple" => ["v1", ":v2"]})
-      assert binary == :erlang.term_to_binary({"v1", :v2})
-      assert ConfigDB.from_binary(binary) == {"v1", :v2}
+      assert ConfigDB.to_elixir_types(%{"tuple" => ["v1", ":v2"]}) == {"v1", :v2}
     end
 
     test "proxy tuple with localhost" do
-      binary =
-        ConfigDB.transform(%{
-          "tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]
-        })
-
-      assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, :localhost, 1234}})
-      assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, :localhost, 1234}}
+      assert ConfigDB.to_elixir_types(%{
+               "tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]
+             }) == {:proxy_url, {:socks5, :localhost, 1234}}
     end
 
     test "proxy tuple with domain" do
-      binary =
-        ConfigDB.transform(%{
-          "tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]
-        })
-
-      assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, 'domain.com', 1234}})
-      assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, 'domain.com', 1234}}
+      assert ConfigDB.to_elixir_types(%{
+               "tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]
+             }) == {:proxy_url, {:socks5, 'domain.com', 1234}}
     end
 
     test "proxy tuple with ip" do
-      binary =
-        ConfigDB.transform(%{
-          "tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]
-        })
-
-      assert binary == :erlang.term_to_binary({:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}})
-      assert ConfigDB.from_binary(binary) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}}
+      assert ConfigDB.to_elixir_types(%{
+               "tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]
+             }) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}}
     end
 
     test "tuple with n childs" do
-      binary =
-        ConfigDB.transform(%{
-          "tuple" => [
-            "v1",
-            ":v2",
-            "Pleroma.Bookmark",
-            150,
-            false,
-            "Phoenix.Socket.V1.JSONSerializer"
-          ]
-        })
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
-               )
-
-      assert ConfigDB.from_binary(binary) ==
-               {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
+      assert ConfigDB.to_elixir_types(%{
+               "tuple" => [
+                 "v1",
+                 ":v2",
+                 "Pleroma.Bookmark",
+                 150,
+                 false,
+                 "Phoenix.Socket.V1.JSONSerializer"
+               ]
+             }) == {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
     end
 
     test "map with string key" do
-      binary = ConfigDB.transform(%{"key" => "value"})
-      assert binary == :erlang.term_to_binary(%{"key" => "value"})
-      assert ConfigDB.from_binary(binary) == %{"key" => "value"}
+      assert ConfigDB.to_elixir_types(%{"key" => "value"}) == %{"key" => "value"}
     end
 
     test "map with atom key" do
-      binary = ConfigDB.transform(%{":key" => "value"})
-      assert binary == :erlang.term_to_binary(%{key: "value"})
-      assert ConfigDB.from_binary(binary) == %{key: "value"}
+      assert ConfigDB.to_elixir_types(%{":key" => "value"}) == %{key: "value"}
     end
 
     test "list of strings" do
-      binary = ConfigDB.transform(["v1", "v2", "v3"])
-      assert binary == :erlang.term_to_binary(["v1", "v2", "v3"])
-      assert ConfigDB.from_binary(binary) == ["v1", "v2", "v3"]
+      assert ConfigDB.to_elixir_types(["v1", "v2", "v3"]) == ["v1", "v2", "v3"]
     end
 
     test "list of modules" do
-      binary = ConfigDB.transform(["Pleroma.Repo", "Pleroma.Activity"])
-      assert binary == :erlang.term_to_binary([Pleroma.Repo, Pleroma.Activity])
-      assert ConfigDB.from_binary(binary) == [Pleroma.Repo, Pleroma.Activity]
+      assert ConfigDB.to_elixir_types(["Pleroma.Repo", "Pleroma.Activity"]) == [
+               Pleroma.Repo,
+               Pleroma.Activity
+             ]
     end
 
     test "list of atoms" do
-      binary = ConfigDB.transform([":v1", ":v2", ":v3"])
-      assert binary == :erlang.term_to_binary([:v1, :v2, :v3])
-      assert ConfigDB.from_binary(binary) == [:v1, :v2, :v3]
+      assert ConfigDB.to_elixir_types([":v1", ":v2", ":v3"]) == [:v1, :v2, :v3]
     end
 
     test "list of mixed values" do
-      binary =
-        ConfigDB.transform([
-          "v1",
-          ":v2",
-          "Pleroma.Repo",
-          "Phoenix.Socket.V1.JSONSerializer",
-          15,
-          false
-        ])
-
-      assert binary ==
-               :erlang.term_to_binary([
-                 "v1",
-                 :v2,
-                 Pleroma.Repo,
-                 Phoenix.Socket.V1.JSONSerializer,
-                 15,
-                 false
-               ])
-
-      assert ConfigDB.from_binary(binary) == [
+      assert ConfigDB.to_elixir_types([
+               "v1",
+               ":v2",
+               "Pleroma.Repo",
+               "Phoenix.Socket.V1.JSONSerializer",
+               15,
+               false
+             ]) == [
                "v1",
                :v2,
                Pleroma.Repo,
@@ -470,40 +379,17 @@ defmodule Pleroma.ConfigDBTest do
     end
 
     test "simple keyword" do
-      binary = ConfigDB.transform([%{"tuple" => [":key", "value"]}])
-      assert binary == :erlang.term_to_binary([{:key, "value"}])
-      assert ConfigDB.from_binary(binary) == [{:key, "value"}]
-      assert ConfigDB.from_binary(binary) == [key: "value"]
-    end
-
-    test "keyword with partial_chain key" do
-      binary =
-        ConfigDB.transform([%{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}])
-
-      assert binary == :erlang.term_to_binary(partial_chain: &:hackney_connect.partial_chain/1)
-      assert ConfigDB.from_binary(binary) == [partial_chain: &:hackney_connect.partial_chain/1]
+      assert ConfigDB.to_elixir_types([%{"tuple" => [":key", "value"]}]) == [key: "value"]
     end
 
     test "keyword" do
-      binary =
-        ConfigDB.transform([
-          %{"tuple" => [":types", "Pleroma.PostgresTypes"]},
-          %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]},
-          %{"tuple" => [":migration_lock", nil]},
-          %{"tuple" => [":key1", 150]},
-          %{"tuple" => [":key2", "string"]}
-        ])
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 types: Pleroma.PostgresTypes,
-                 telemetry_event: [Pleroma.Repo.Instrumenter],
-                 migration_lock: nil,
-                 key1: 150,
-                 key2: "string"
-               )
-
-      assert ConfigDB.from_binary(binary) == [
+      assert ConfigDB.to_elixir_types([
+               %{"tuple" => [":types", "Pleroma.PostgresTypes"]},
+               %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]},
+               %{"tuple" => [":migration_lock", nil]},
+               %{"tuple" => [":key1", 150]},
+               %{"tuple" => [":key2", "string"]}
+             ]) == [
                types: Pleroma.PostgresTypes,
                telemetry_event: [Pleroma.Repo.Instrumenter],
                migration_lock: nil,
@@ -512,86 +398,60 @@ defmodule Pleroma.ConfigDBTest do
              ]
     end
 
+    test "trandformed keyword" do
+      assert ConfigDB.to_elixir_types(a: 1, b: 2, c: "string") == [a: 1, b: 2, c: "string"]
+    end
+
     test "complex keyword with nested mixed childs" do
-      binary =
-        ConfigDB.transform([
-          %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]},
-          %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
-          %{"tuple" => [":link_name", true]},
-          %{"tuple" => [":proxy_remote", false]},
-          %{"tuple" => [":common_map", %{":key" => "value"}]},
-          %{
-            "tuple" => [
-              ":proxy_opts",
-              [
-                %{"tuple" => [":redirect_on_failure", false]},
-                %{"tuple" => [":max_body_length", 1_048_576]},
-                %{
-                  "tuple" => [
-                    ":http",
-                    [%{"tuple" => [":follow_redirect", true]}, %{"tuple" => [":pool", ":upload"]}]
-                  ]
-                }
-              ]
-            ]
-          }
-        ])
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 uploader: Pleroma.Uploaders.Local,
-                 filters: [Pleroma.Upload.Filter.Dedupe],
-                 link_name: true,
-                 proxy_remote: false,
-                 common_map: %{key: "value"},
-                 proxy_opts: [
-                   redirect_on_failure: false,
-                   max_body_length: 1_048_576,
-                   http: [
-                     follow_redirect: true,
-                     pool: :upload
+      assert ConfigDB.to_elixir_types([
+               %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]},
+               %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
+               %{"tuple" => [":link_name", true]},
+               %{"tuple" => [":proxy_remote", false]},
+               %{"tuple" => [":common_map", %{":key" => "value"}]},
+               %{
+                 "tuple" => [
+                   ":proxy_opts",
+                   [
+                     %{"tuple" => [":redirect_on_failure", false]},
+                     %{"tuple" => [":max_body_length", 1_048_576]},
+                     %{
+                       "tuple" => [
+                         ":http",
+                         [
+                           %{"tuple" => [":follow_redirect", true]},
+                           %{"tuple" => [":pool", ":upload"]}
+                         ]
+                       ]
+                     }
                    ]
                  ]
-               )
-
-      assert ConfigDB.from_binary(binary) ==
-               [
-                 uploader: Pleroma.Uploaders.Local,
-                 filters: [Pleroma.Upload.Filter.Dedupe],
-                 link_name: true,
-                 proxy_remote: false,
-                 common_map: %{key: "value"},
-                 proxy_opts: [
-                   redirect_on_failure: false,
-                   max_body_length: 1_048_576,
-                   http: [
-                     follow_redirect: true,
-                     pool: :upload
-                   ]
+               }
+             ]) == [
+               uploader: Pleroma.Uploaders.Local,
+               filters: [Pleroma.Upload.Filter.Dedupe],
+               link_name: true,
+               proxy_remote: false,
+               common_map: %{key: "value"},
+               proxy_opts: [
+                 redirect_on_failure: false,
+                 max_body_length: 1_048_576,
+                 http: [
+                   follow_redirect: true,
+                   pool: :upload
                  ]
                ]
+             ]
     end
 
     test "common keyword" do
-      binary =
-        ConfigDB.transform([
-          %{"tuple" => [":level", ":warn"]},
-          %{"tuple" => [":meta", [":all"]]},
-          %{"tuple" => [":path", ""]},
-          %{"tuple" => [":val", nil]},
-          %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]}
-        ])
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 level: :warn,
-                 meta: [:all],
-                 path: "",
-                 val: nil,
-                 webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
-               )
-
-      assert ConfigDB.from_binary(binary) == [
+      assert ConfigDB.to_elixir_types([
+               %{"tuple" => [":level", ":warn"]},
+               %{"tuple" => [":meta", [":all"]]},
+               %{"tuple" => [":path", ""]},
+               %{"tuple" => [":val", nil]},
+               %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]}
+             ]) == [
                level: :warn,
                meta: [:all],
                path: "",
@@ -601,98 +461,73 @@ defmodule Pleroma.ConfigDBTest do
     end
 
     test "complex keyword with sigil" do
-      binary =
-        ConfigDB.transform([
-          %{"tuple" => [":federated_timeline_removal", []]},
-          %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]},
-          %{"tuple" => [":replace", []]}
-        ])
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 federated_timeline_removal: [],
-                 reject: [~r/comp[lL][aA][iI][nN]er/],
-                 replace: []
-               )
-
-      assert ConfigDB.from_binary(binary) ==
-               [federated_timeline_removal: [], reject: [~r/comp[lL][aA][iI][nN]er/], replace: []]
+      assert ConfigDB.to_elixir_types([
+               %{"tuple" => [":federated_timeline_removal", []]},
+               %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]},
+               %{"tuple" => [":replace", []]}
+             ]) == [
+               federated_timeline_removal: [],
+               reject: [~r/comp[lL][aA][iI][nN]er/],
+               replace: []
+             ]
     end
 
     test "complex keyword with tuples with more than 2 values" do
-      binary =
-        ConfigDB.transform([
-          %{
-            "tuple" => [
-              ":http",
-              [
-                %{
-                  "tuple" => [
-                    ":key1",
-                    [
-                      %{
-                        "tuple" => [
-                          ":_",
-                          [
-                            %{
-                              "tuple" => [
-                                "/api/v1/streaming",
-                                "Pleroma.Web.MastodonAPI.WebsocketHandler",
-                                []
-                              ]
-                            },
-                            %{
-                              "tuple" => [
-                                "/websocket",
-                                "Phoenix.Endpoint.CowboyWebSocket",
-                                %{
-                                  "tuple" => [
-                                    "Phoenix.Transports.WebSocket",
-                                    %{
-                                      "tuple" => [
-                                        "Pleroma.Web.Endpoint",
-                                        "Pleroma.Web.UserSocket",
-                                        []
-                                      ]
-                                    }
-                                  ]
-                                }
-                              ]
-                            },
-                            %{
-                              "tuple" => [
-                                ":_",
-                                "Phoenix.Endpoint.Cowboy2Handler",
-                                %{"tuple" => ["Pleroma.Web.Endpoint", []]}
-                              ]
-                            }
-                          ]
-                        ]
-                      }
-                    ]
-                  ]
-                }
-              ]
-            ]
-          }
-        ])
-
-      assert binary ==
-               :erlang.term_to_binary(
-                 http: [
-                   key1: [
-                     _: [
-                       {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
-                       {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
-                        {Phoenix.Transports.WebSocket,
-                         {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
-                       {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
-                     ]
+      assert ConfigDB.to_elixir_types([
+               %{
+                 "tuple" => [
+                   ":http",
+                   [
+                     %{
+                       "tuple" => [
+                         ":key1",
+                         [
+                           %{
+                             "tuple" => [
+                               ":_",
+                               [
+                                 %{
+                                   "tuple" => [
+                                     "/api/v1/streaming",
+                                     "Pleroma.Web.MastodonAPI.WebsocketHandler",
+                                     []
+                                   ]
+                                 },
+                                 %{
+                                   "tuple" => [
+                                     "/websocket",
+                                     "Phoenix.Endpoint.CowboyWebSocket",
+                                     %{
+                                       "tuple" => [
+                                         "Phoenix.Transports.WebSocket",
+                                         %{
+                                           "tuple" => [
+                                             "Pleroma.Web.Endpoint",
+                                             "Pleroma.Web.UserSocket",
+                                             []
+                                           ]
+                                         }
+                                       ]
+                                     }
+                                   ]
+                                 },
+                                 %{
+                                   "tuple" => [
+                                     ":_",
+                                     "Phoenix.Endpoint.Cowboy2Handler",
+                                     %{"tuple" => ["Pleroma.Web.Endpoint", []]}
+                                   ]
+                                 }
+                               ]
+                             ]
+                           }
+                         ]
+                       ]
+                     }
                    ]
                  ]
-               )
-
-      assert ConfigDB.from_binary(binary) == [
+               }
+             ]) == [
                http: [
                  key1: [
                    {:_,
index 473899d1d57e1ecf5a74548562a6659a638b0385..f53829e094ca5ac272171dc64caa06f58de91bb2 100644 (file)
@@ -6,9 +6,9 @@ defmodule Pleroma.Config.TransferTaskTest do
   use Pleroma.DataCase
 
   import ExUnit.CaptureLog
+  import Pleroma.Factory
 
   alias Pleroma.Config.TransferTask
-  alias Pleroma.ConfigDB
 
   setup do: clear_config(:configurable_from_database, true)
 
@@ -19,31 +19,11 @@ defmodule Pleroma.Config.TransferTaskTest do
     refute Application.get_env(:postgrex, :test_key)
     initial = Application.get_env(:logger, :level)
 
-    ConfigDB.create(%{
-      group: ":pleroma",
-      key: ":test_key",
-      value: [live: 2, com: 3]
-    })
-
-    ConfigDB.create(%{
-      group: ":idna",
-      key: ":test_key",
-      value: [live: 15, com: 35]
-    })
-
-    ConfigDB.create(%{
-      group: ":quack",
-      key: ":test_key",
-      value: [:test_value1, :test_value2]
-    })
-
-    ConfigDB.create(%{
-      group: ":postgrex",
-      key: ":test_key",
-      value: :value
-    })
-
-    ConfigDB.create(%{group: ":logger", key: ":level", value: :debug})
+    insert(:config, key: :test_key, value: [live: 2, com: 3])
+    insert(:config, group: :idna, key: :test_key, value: [live: 15, com: 35])
+    insert(:config, group: :quack, key: :test_key, value: [:test_value1, :test_value2])
+    insert(:config, group: :postgrex, key: :test_key, value: :value)
+    insert(:config, group: :logger, key: :level, value: :debug)
 
     TransferTask.start_link([])
 
@@ -66,17 +46,8 @@ defmodule Pleroma.Config.TransferTaskTest do
     level = Application.get_env(:quack, :level)
     meta = Application.get_env(:quack, :meta)
 
-    ConfigDB.create(%{
-      group: ":quack",
-      key: ":level",
-      value: :info
-    })
-
-    ConfigDB.create(%{
-      group: ":quack",
-      key: ":meta",
-      value: [:none]
-    })
+    insert(:config, group: :quack, key: :level, value: :info)
+    insert(:config, group: :quack, key: :meta, value: [:none])
 
     TransferTask.start_link([])
 
@@ -95,17 +66,8 @@ defmodule Pleroma.Config.TransferTaskTest do
     clear_config(:emoji)
     clear_config(:assets)
 
-    ConfigDB.create(%{
-      group: ":pleroma",
-      key: ":emoji",
-      value: [groups: [a: 1, b: 2]]
-    })
-
-    ConfigDB.create(%{
-      group: ":pleroma",
-      key: ":assets",
-      value: [mascots: [a: 1, b: 2]]
-    })
+    insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]])
+    insert(:config, key: :assets, value: [mascots: [a: 1, b: 2]])
 
     TransferTask.start_link([])
 
@@ -122,12 +84,7 @@ defmodule Pleroma.Config.TransferTaskTest do
 
     test "don't restart if no reboot time settings were changed" do
       clear_config(:emoji)
-
-      ConfigDB.create(%{
-        group: ":pleroma",
-        key: ":emoji",
-        value: [groups: [a: 1, b: 2]]
-      })
+      insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]])
 
       refute String.contains?(
                capture_log(fn -> TransferTask.start_link([]) end),
@@ -137,25 +94,13 @@ defmodule Pleroma.Config.TransferTaskTest do
 
     test "on reboot time key" do
       clear_config(:chat)
-
-      ConfigDB.create(%{
-        group: ":pleroma",
-        key: ":chat",
-        value: [enabled: false]
-      })
-
+      insert(:config, key: :chat, value: [enabled: false])
       assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
     end
 
     test "on reboot time subkey" do
       clear_config(Pleroma.Captcha)
-
-      ConfigDB.create(%{
-        group: ":pleroma",
-        key: "Pleroma.Captcha",
-        value: [seconds_valid: 60]
-      })
-
+      insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60])
       assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
     end
 
@@ -163,17 +108,8 @@ defmodule Pleroma.Config.TransferTaskTest do
       clear_config(:chat)
       clear_config(Pleroma.Captcha)
 
-      ConfigDB.create(%{
-        group: ":pleroma",
-        key: ":chat",
-        value: [enabled: false]
-      })
-
-      ConfigDB.create(%{
-        group: ":pleroma",
-        key: "Pleroma.Captcha",
-        value: [seconds_valid: 60]
-      })
+      insert(:config, key: :chat, value: [enabled: false])
+      insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60])
 
       refute String.contains?(
                capture_log(fn -> TransferTask.load_and_update_env([], false) end),
diff --git a/test/fixtures/create-chat-message.json b/test/fixtures/create-chat-message.json
new file mode 100644 (file)
index 0000000..9c23a1c
--- /dev/null
@@ -0,0 +1,31 @@
+{
+  "actor": "http://2hu.gensokyo/users/raymoo",
+  "id": "http://2hu.gensokyo/objects/1",
+  "object": {
+    "attributedTo": "http://2hu.gensokyo/users/raymoo",
+    "content": "You expected a cute girl? Too bad. <script>alert('XSS')</script>",
+    "id": "http://2hu.gensokyo/objects/2",
+    "published": "2020-02-12T14:08:20Z",
+    "to": [
+      "http://2hu.gensokyo/users/marisa"
+    ],
+    "tag": [
+      {
+        "icon": {
+          "type": "Image",
+          "url": "http://2hu.gensokyo/emoji/Firefox.gif"
+        },
+        "id": "http://2hu.gensokyo/emoji/Firefox.gif",
+        "name": ":firefox:",
+        "type": "Emoji",
+        "updated": "1970-01-01T00:00:00Z"
+      }
+    ],
+    "type": "ChatMessage"
+  },
+  "published": "2018-02-12T14:08:20Z",
+  "to": [
+    "http://2hu.gensokyo/users/marisa"
+  ],
+  "type": "Create"
+}
diff --git a/test/migration_helper/notification_backfill_test.exs b/test/migration_helper/notification_backfill_test.exs
new file mode 100644 (file)
index 0000000..2a62a2b
--- /dev/null
@@ -0,0 +1,56 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MigrationHelper.NotificationBackfillTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Activity
+  alias Pleroma.MigrationHelper.NotificationBackfill
+  alias Pleroma.Notification
+  alias Pleroma.Repo
+  alias Pleroma.Web.CommonAPI
+
+  import Pleroma.Factory
+
+  describe "fill_in_notification_types" do
+    test "it fills in missing notification types" do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"})
+      {:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo")
+      {:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕")
+      {:ok, like} = CommonAPI.favorite(other_user, post.id)
+      {:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕")
+
+      data =
+        react_2.data
+        |> Map.put("type", "EmojiReaction")
+
+      {:ok, react_2} =
+        react_2
+        |> Activity.change(%{data: data})
+        |> Repo.update()
+
+      assert {5, nil} = Repo.update_all(Notification, set: [type: nil])
+
+      NotificationBackfill.fill_in_notification_types()
+
+      assert %{type: "mention"} =
+               Repo.get_by(Notification, user_id: other_user.id, activity_id: post.id)
+
+      assert %{type: "favourite"} =
+               Repo.get_by(Notification, user_id: user.id, activity_id: like.id)
+
+      assert %{type: "pleroma:emoji_reaction"} =
+               Repo.get_by(Notification, user_id: user.id, activity_id: react.id)
+
+      assert %{type: "pleroma:emoji_reaction"} =
+               Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id)
+
+      assert %{type: "pleroma:chat_mention"} =
+               Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id)
+    end
+  end
+end
index 37c255fee5130e96f3d10a4f07f1b7243f3330e2..526f43fab7a53f1313bd602e2f0f392a6c70820c 100644 (file)
@@ -10,6 +10,7 @@ defmodule Pleroma.NotificationTest do
 
   alias Pleroma.FollowingRelationship
   alias Pleroma.Notification
+  alias Pleroma.Repo
   alias Pleroma.Tests.ObanHelpers
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
@@ -31,6 +32,7 @@ defmodule Pleroma.NotificationTest do
       {:ok, [notification]} = Notification.create_notifications(activity)
 
       assert notification.user_id == user.id
+      assert notification.type == "pleroma:emoji_reaction"
     end
 
     test "notifies someone when they are directly addressed" do
@@ -48,6 +50,7 @@ defmodule Pleroma.NotificationTest do
       notified_ids = Enum.sort([notification.user_id, other_notification.user_id])
       assert notified_ids == [other_user.id, third_user.id]
       assert notification.activity_id == activity.id
+      assert notification.type == "mention"
       assert other_notification.activity_id == activity.id
 
       assert [%Pleroma.Marker{unread_count: 2}] =
@@ -303,6 +306,14 @@ defmodule Pleroma.NotificationTest do
 
       assert {:ok, []} == Notification.create_notifications(status)
     end
+
+    test "it disables notifications from people who are invisible" do
+      author = insert(:user, invisible: true)
+      user = insert(:user)
+
+      {:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"})
+      refute Notification.create_notification(status, user)
+    end
   end
 
   describe "follow / follow_request notifications" do
@@ -335,9 +346,12 @@ defmodule Pleroma.NotificationTest do
       # After request is accepted, the same notification is rendered with type "follow":
       assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
 
-      notification_id = notification.id
-      assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
-      assert %{type: "follow"} = NotificationView.render("show.json", render_opts)
+      notification =
+        Repo.get(Notification, notification.id)
+        |> Repo.preload(:activity)
+
+      assert %{type: "follow"} =
+               NotificationView.render("show.json", notification: notification, for: followed_user)
     end
 
     test "it doesn't create a notification for follow-unfollow-follow chains" do
index d5b1b782db6c4be2196124348984bc42547fc234..9165427aee6030584eb4edb199091f86082bcedf 100644 (file)
@@ -21,7 +21,7 @@ defmodule Pleroma.PaginationTest do
       id = Enum.at(notes, 2).id |> Integer.to_string()
 
       %{total: total, items: paginated} =
-        Pagination.fetch_paginated(Object, %{"min_id" => id, "total" => true})
+        Pagination.fetch_paginated(Object, %{min_id: id, total: true})
 
       assert length(paginated) == 2
       assert total == 5
@@ -31,7 +31,7 @@ defmodule Pleroma.PaginationTest do
       id = Enum.at(notes, 2).id |> Integer.to_string()
 
       %{total: total, items: paginated} =
-        Pagination.fetch_paginated(Object, %{"since_id" => id, "total" => true})
+        Pagination.fetch_paginated(Object, %{since_id: id, total: true})
 
       assert length(paginated) == 2
       assert total == 5
@@ -41,7 +41,7 @@ defmodule Pleroma.PaginationTest do
       id = Enum.at(notes, 1).id |> Integer.to_string()
 
       %{total: total, items: paginated} =
-        Pagination.fetch_paginated(Object, %{"max_id" => id, "total" => true})
+        Pagination.fetch_paginated(Object, %{max_id: id, total: true})
 
       assert length(paginated) == 1
       assert total == 5
@@ -50,7 +50,7 @@ defmodule Pleroma.PaginationTest do
     test "paginates by min_id & limit", %{notes: notes} do
       id = Enum.at(notes, 2).id |> Integer.to_string()
 
-      paginated = Pagination.fetch_paginated(Object, %{"min_id" => id, "limit" => 1})
+      paginated = Pagination.fetch_paginated(Object, %{min_id: id, limit: 1})
 
       assert length(paginated) == 1
     end
@@ -64,13 +64,13 @@ defmodule Pleroma.PaginationTest do
     end
 
     test "paginates by limit" do
-      paginated = Pagination.fetch_paginated(Object, %{"limit" => 2}, :offset)
+      paginated = Pagination.fetch_paginated(Object, %{limit: 2}, :offset)
 
       assert length(paginated) == 2
     end
 
     test "paginates by limit & offset" do
-      paginated = Pagination.fetch_paginated(Object, %{"limit" => 2, "offset" => 4}, :offset)
+      paginated = Pagination.fetch_paginated(Object, %{limit: 2, offset: 4}, :offset)
 
       assert length(paginated) == 1
     end
index 1a9b96180ec4e30719386373894fd97e05267c9a..6e22b66a417e7837cf4986f0d082b6f9884ef826 100644 (file)
@@ -397,24 +397,17 @@ defmodule Pleroma.Factory do
     }
   end
 
-  def config_factory do
+  def config_factory(attrs \\ %{}) do
     %Pleroma.ConfigDB{
-      key:
-        sequence(:key, fn key ->
-          # Atom dynamic registration hack in tests
-          "some_key_#{key}"
-          |> String.to_atom()
-          |> inspect()
-        end),
-      group: ":pleroma",
+      key: sequence(:key, &String.to_atom("some_key_#{&1}")),
+      group: :pleroma,
       value:
         sequence(
           :value,
-          fn key ->
-            :erlang.term_to_binary(%{another_key: "#{key}somevalue", another: "#{key}somevalue"})
-          end
+          &%{another_key: "#{&1}somevalue", another: "#{&1}somevalue"}
         )
     }
+    |> merge_attributes(attrs)
   end
 
   def marker_factory do
index 04bc947a97643bfd4b50e4b31eccefe5f5e3c878..e1bddfebf42172572a4a5a9524d08f04860c4c44 100644 (file)
@@ -5,6 +5,8 @@
 defmodule Mix.Tasks.Pleroma.ConfigTest do
   use Pleroma.DataCase
 
+  import Pleroma.Factory
+
   alias Pleroma.ConfigDB
   alias Pleroma.Repo
 
@@ -49,24 +51,19 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
       refute ConfigDB.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"})
       refute ConfigDB.get_by_params(%{group: ":postgrex", key: ":json_library"})
 
-      assert ConfigDB.from_binary(config1.value) == [key: "value", key2: [Repo]]
-      assert ConfigDB.from_binary(config2.value) == [key: "value2", key2: ["Activity"]]
-      assert ConfigDB.from_binary(config3.value) == :info
+      assert config1.value == [key: "value", key2: [Repo]]
+      assert config2.value == [key: "value2", key2: ["Activity"]]
+      assert config3.value == :info
     end
 
     test "config table is truncated before migration" do
-      ConfigDB.create(%{
-        group: ":pleroma",
-        key: ":first_setting",
-        value: [key: "value", key2: ["Activity"]]
-      })
-
+      insert(:config, key: :first_setting, value: [key: "value", key2: ["Activity"]])
       assert Repo.aggregate(ConfigDB, :count, :id) == 1
 
       Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs")
 
       config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"})
-      assert ConfigDB.from_binary(config.value) == [key: "value", key2: [Repo]]
+      assert config.value == [key: "value", key2: [Repo]]
     end
   end
 
@@ -82,19 +79,9 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
     end
 
     test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do
-      ConfigDB.create(%{
-        group: ":pleroma",
-        key: ":setting_first",
-        value: [key: "value", key2: ["Activity"]]
-      })
-
-      ConfigDB.create(%{
-        group: ":pleroma",
-        key: ":setting_second",
-        value: [key: "value2", key2: [Repo]]
-      })
-
-      ConfigDB.create(%{group: ":quack", key: ":level", value: :info})
+      insert(:config, key: :setting_first, value: [key: "value", key2: ["Activity"]])
+      insert(:config, key: :setting_second, value: [key: "value2", key2: [Repo]])
+      insert(:config, group: :quack, key: :level, value: :info)
 
       Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"])
 
@@ -107,9 +94,8 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
     end
 
     test "load a settings with large values and pass to file", %{temp_file: temp_file} do
-      ConfigDB.create(%{
-        group: ":pleroma",
-        key: ":instance",
+      insert(:config,
+        key: :instance,
         value: [
           name: "Pleroma",
           email: "example@example.com",
@@ -163,7 +149,6 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
           extended_nickname_format: true,
           multi_factor_authentication: [
             totp: [
-              # digits 6 or 8
               digits: 6,
               period: 30
             ],
@@ -173,7 +158,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
             ]
           ]
         ]
-      })
+      )
 
       Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", "temp", "-d"])
 
index 6782888545a58f6af3cb14643a09a6b366f28b4d..a8ba0658d0d7d8abf46b80ac0c070dbe0b9da477 100644 (file)
@@ -62,11 +62,11 @@ defmodule Mix.Tasks.Pleroma.RelayTest do
 
       [undo_activity] =
         ActivityPub.fetch_activities([], %{
-          "type" => "Undo",
-          "actor_id" => follower_id,
-          "limit" => 1,
-          "skip_preload" => true,
-          "invisible_actors" => true
+          type: "Undo",
+          actor_id: follower_id,
+          limit: 1,
+          skip_preload: true,
+          invisible_actors: true
         })
 
       assert undo_activity.data["type"] == "Undo"
index b55aa1cdb8475bed2eb13239b1afa5246ddcd377..9220d23fcbaf0a4d2167c822eb3f5f9a523cc6b9 100644 (file)
@@ -4,6 +4,7 @@
 
 defmodule Mix.Tasks.Pleroma.UserTest do
   alias Pleroma.Activity
+  alias Pleroma.MFA
   alias Pleroma.Object
   alias Pleroma.Repo
   alias Pleroma.Tests.ObanHelpers
@@ -278,6 +279,35 @@ defmodule Mix.Tasks.Pleroma.UserTest do
     end
   end
 
+  describe "running reset_mfa" do
+    test "disables MFA" do
+      user =
+        insert(:user,
+          multi_factor_authentication_settings: %MFA.Settings{
+            enabled: true,
+            totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true}
+          }
+        )
+
+      Mix.Tasks.Pleroma.User.run(["reset_mfa", user.nickname])
+
+      assert_received {:mix_shell, :info, [message]}
+      assert message == "Multi-Factor Authentication disabled for #{user.nickname}"
+
+      assert %{enabled: false, totp: false} ==
+               user.nickname
+               |> User.get_cached_by_nickname()
+               |> MFA.mfa_settings()
+    end
+
+    test "no user to reset MFA" do
+      Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"])
+
+      assert_received {:mix_shell, :error, [message]}
+      assert message =~ "No local user"
+    end
+  end
+
   describe "running invite" do
     test "invite token is generated" do
       assert capture_io(fn ->
index b6a463e8c8dd080fcc550ff334a59f8b9348ffdd..62ca304877ca3f02a73229eb761493403410b76e 100644 (file)
@@ -6,21 +6,17 @@ defmodule Pleroma.Upload.Filter.MogrifyTest do
   use Pleroma.DataCase
   import Mock
 
-  alias Pleroma.Config
-  alias Pleroma.Upload
   alias Pleroma.Upload.Filter
 
-  setup do: clear_config([Filter.Mogrify, :args])
-
   test "apply mogrify filter" do
-    Config.put([Filter.Mogrify, :args], [{"tint", "40"}])
+    clear_config(Filter.Mogrify, args: [{"tint", "40"}])
 
     File.cp!(
       "test/fixtures/image.jpg",
       "test/fixtures/image_tmp.jpg"
     )
 
-    upload = %Upload{
+    upload = %Pleroma.Upload{
       name: "an… image.jpg",
       content_type: "image/jpg",
       path: Path.absname("test/fixtures/image_tmp.jpg"),
index 060a940bbaaa88e77042b903f1cbd0471cc63def..2abf0edec6b6ee61d7342923eb5749b7bfd57f1a 100644 (file)
@@ -54,6 +54,7 @@ defmodule Pleroma.UploadTest do
                 %{
                   "name" => "image.jpg",
                   "type" => "Document",
+                  "mediaType" => "image/jpeg",
                   "url" => [
                     %{
                       "href" => "http://localhost:4001/media/post-process-file.jpg",
index 6b344158d8de9cdd05f0bd9f3ff48f3f8709728e..98c79da4f642f7717e8ae12a08d5f27c98cb3b32 100644 (file)
@@ -1122,7 +1122,7 @@ defmodule Pleroma.UserTest do
 
       assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] ==
                ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{
-                 "user" => user2
+                 user: user2
                })
 
       {:ok, _user} = User.deactivate(user)
@@ -1132,7 +1132,7 @@ defmodule Pleroma.UserTest do
 
       assert [] ==
                ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{
-                 "user" => user2
+                 user: user2
                })
     end
   end
@@ -1159,6 +1159,9 @@ defmodule Pleroma.UserTest do
       follower = insert(:user)
       {:ok, follower} = User.follow(follower, user)
 
+      locked_user = insert(:user, name: "locked", locked: true)
+      {:ok, _} = User.follow(user, locked_user, :follow_pending)
+
       object = insert(:note, user: user)
       activity = insert(:note_activity, user: user, note: object)
 
@@ -1177,6 +1180,8 @@ defmodule Pleroma.UserTest do
       refute User.following?(follower, user)
       assert %{deactivated: true} = User.get_by_id(user.id)
 
+      assert [] == User.get_follow_requests(locked_user)
+
       user_activities =
         user.ap_id
         |> Activity.Queries.by_actor()
index 3dcb62873c2ed6463de45a62cfc2de0ffe7ec5d5..7693f6400d07524cedd0bed93b5a716365999b26 100644 (file)
@@ -82,30 +82,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"})
 
-      activities =
-        ActivityPub.fetch_activities([], %{:visibility => "direct", "actor_id" => user.ap_id})
+      activities = ActivityPub.fetch_activities([], %{visibility: "direct", actor_id: user.ap_id})
 
       assert activities == [direct_activity]
 
       activities =
-        ActivityPub.fetch_activities([], %{:visibility => "unlisted", "actor_id" => user.ap_id})
+        ActivityPub.fetch_activities([], %{visibility: "unlisted", actor_id: user.ap_id})
 
       assert activities == [unlisted_activity]
 
       activities =
-        ActivityPub.fetch_activities([], %{:visibility => "private", "actor_id" => user.ap_id})
+        ActivityPub.fetch_activities([], %{visibility: "private", actor_id: user.ap_id})
 
       assert activities == [private_activity]
 
-      activities =
-        ActivityPub.fetch_activities([], %{:visibility => "public", "actor_id" => user.ap_id})
+      activities = ActivityPub.fetch_activities([], %{visibility: "public", actor_id: user.ap_id})
 
       assert activities == [public_activity]
 
       activities =
         ActivityPub.fetch_activities([], %{
-          :visibility => ~w[private public],
-          "actor_id" => user.ap_id
+          visibility: ~w[private public],
+          actor_id: user.ap_id
         })
 
       assert activities == [public_activity, private_activity]
@@ -126,8 +124,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       activities =
         ActivityPub.fetch_activities([], %{
-          "exclude_visibilities" => "direct",
-          "actor_id" => user.ap_id
+          exclude_visibilities: "direct",
+          actor_id: user.ap_id
         })
 
       assert public_activity in activities
@@ -137,8 +135,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       activities =
         ActivityPub.fetch_activities([], %{
-          "exclude_visibilities" => "unlisted",
-          "actor_id" => user.ap_id
+          exclude_visibilities: "unlisted",
+          actor_id: user.ap_id
         })
 
       assert public_activity in activities
@@ -148,8 +146,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       activities =
         ActivityPub.fetch_activities([], %{
-          "exclude_visibilities" => "private",
-          "actor_id" => user.ap_id
+          exclude_visibilities: "private",
+          actor_id: user.ap_id
         })
 
       assert public_activity in activities
@@ -159,8 +157,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       activities =
         ActivityPub.fetch_activities([], %{
-          "exclude_visibilities" => "public",
-          "actor_id" => user.ap_id
+          exclude_visibilities: "public",
+          actor_id: user.ap_id
         })
 
       refute public_activity in activities
@@ -193,23 +191,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       {:ok, status_two} = CommonAPI.post(user, %{status: ". #essais"})
       {:ok, status_three} = CommonAPI.post(user, %{status: ". #test #reject"})
 
-      fetch_one = ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => "test"})
+      fetch_one = ActivityPub.fetch_activities([], %{type: "Create", tag: "test"})
 
-      fetch_two =
-        ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => ["test", "essais"]})
+      fetch_two = ActivityPub.fetch_activities([], %{type: "Create", tag: ["test", "essais"]})
 
       fetch_three =
         ActivityPub.fetch_activities([], %{
-          "type" => "Create",
-          "tag" => ["test", "essais"],
-          "tag_reject" => ["reject"]
+          type: "Create",
+          tag: ["test", "essais"],
+          tag_reject: ["reject"]
         })
 
       fetch_four =
         ActivityPub.fetch_activities([], %{
-          "type" => "Create",
-          "tag" => ["test"],
-          "tag_all" => ["test", "reject"]
+          type: "Create",
+          tag: ["test"],
+          tag_all: ["test", "reject"]
         })
 
       assert fetch_one == [status_one, status_three]
@@ -375,7 +372,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       _listen_activity_2 = insert(:listen)
       _listen_activity_3 = insert(:listen)
 
-      timeline = ActivityPub.fetch_activities([], %{"type" => ["Listen"]})
+      timeline = ActivityPub.fetch_activities([], %{type: ["Listen"]})
 
       assert length(timeline) == 3
     end
@@ -507,7 +504,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       {:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]})
 
-      activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user})
+      activities = ActivityPub.fetch_activities_for_context("2hu", %{blocking_user: user})
       assert activities == [activity_two, activity]
     end
   end
@@ -520,8 +517,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     booster = insert(:user)
     {:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]})
 
-    activities =
-      ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
+    activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true})
 
     assert Enum.member?(activities, activity_two)
     assert Enum.member?(activities, activity_three)
@@ -529,8 +525,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
     {:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
 
-    activities =
-      ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
+    activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true})
 
     assert Enum.member?(activities, activity_two)
     assert Enum.member?(activities, activity_three)
@@ -541,16 +536,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
     activity_three = Activity.get_by_id(activity_three.id)
 
-    activities =
-      ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
+    activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true})
 
     assert Enum.member?(activities, activity_two)
     refute Enum.member?(activities, activity_three)
     refute Enum.member?(activities, boost_activity)
     assert Enum.member?(activities, activity_one)
 
-    activities =
-      ActivityPub.fetch_activities([], %{"blocking_user" => nil, "skip_preload" => true})
+    activities = ActivityPub.fetch_activities([], %{blocking_user: nil, skip_preload: true})
 
     assert Enum.member?(activities, activity_two)
     assert Enum.member?(activities, activity_three)
@@ -573,7 +566,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
     {:ok, activity_four} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"})
 
-    activities = ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
+    activities = ActivityPub.fetch_activities([], %{blocking_user: blocker})
 
     assert Enum.member?(activities, activity_one)
     refute Enum.member?(activities, activity_two)
@@ -581,7 +574,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     refute Enum.member?(activities, activity_four)
   end
 
-  test "doesn't return announce activities concerning blocked users" do
+  test "doesn't return announce activities with blocked users in 'to'" do
     blocker = insert(:user)
     blockee = insert(:user)
     friend = insert(:user)
@@ -595,7 +588,40 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     {:ok, activity_three} = CommonAPI.repeat(activity_two.id, friend)
 
     activities =
-      ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
+      ActivityPub.fetch_activities([], %{blocking_user: blocker})
+      |> Enum.map(fn act -> act.id end)
+
+    assert Enum.member?(activities, activity_one.id)
+    refute Enum.member?(activities, activity_two.id)
+    refute Enum.member?(activities, activity_three.id)
+  end
+
+  test "doesn't return announce activities with blocked users in 'cc'" do
+    blocker = insert(:user)
+    blockee = insert(:user)
+    friend = insert(:user)
+
+    {:ok, _user_relationship} = User.block(blocker, blockee)
+
+    {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"})
+
+    {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"})
+
+    assert object = Pleroma.Object.normalize(activity_two)
+
+    data = %{
+      "actor" => friend.ap_id,
+      "object" => object.data["id"],
+      "context" => object.data["context"],
+      "type" => "Announce",
+      "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+      "cc" => [blockee.ap_id]
+    }
+
+    assert {:ok, activity_three} = ActivityPub.insert(data)
+
+    activities =
+      ActivityPub.fetch_activities([], %{blocking_user: blocker})
       |> Enum.map(fn act -> act.id end)
 
     assert Enum.member?(activities, activity_one.id)
@@ -611,8 +637,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     user = insert(:user)
     {:ok, user} = User.block_domain(user, domain)
 
-    activities =
-      ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
+    activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true})
 
     refute activity in activities
 
@@ -620,8 +645,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     ActivityPub.follow(user, followed_user)
     {:ok, repeat_activity} = CommonAPI.repeat(activity.id, followed_user)
 
-    activities =
-      ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
+    activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true})
 
     refute repeat_activity in activities
   end
@@ -641,8 +665,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
     activity = insert(:note_activity, %{note: note})
 
-    activities =
-      ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true})
+    activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true})
 
     assert activity in activities
 
@@ -653,8 +676,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     bad_activity = insert(:note_activity, %{note: bad_note})
     {:ok, repeat_activity} = CommonAPI.repeat(bad_activity.id, domain_user)
 
-    activities =
-      ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true})
+    activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true})
 
     refute repeat_activity in activities
   end
@@ -669,8 +691,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     activity_one_actor = User.get_by_ap_id(activity_one.data["actor"])
     {:ok, _user_relationships} = User.mute(user, activity_one_actor)
 
-    activities =
-      ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
+    activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true})
 
     assert Enum.member?(activities, activity_two)
     assert Enum.member?(activities, activity_three)
@@ -679,9 +700,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     # Calling with 'with_muted' will deliver muted activities, too.
     activities =
       ActivityPub.fetch_activities([], %{
-        "muting_user" => user,
-        "with_muted" => true,
-        "skip_preload" => true
+        muting_user: user,
+        with_muted: true,
+        skip_preload: true
       })
 
     assert Enum.member?(activities, activity_two)
@@ -690,8 +711,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
     {:ok, _user_mute} = User.unmute(user, activity_one_actor)
 
-    activities =
-      ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
+    activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true})
 
     assert Enum.member?(activities, activity_two)
     assert Enum.member?(activities, activity_three)
@@ -703,15 +723,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
     activity_three = Activity.get_by_id(activity_three.id)
 
-    activities =
-      ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
+    activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true})
 
     assert Enum.member?(activities, activity_two)
     refute Enum.member?(activities, activity_three)
     refute Enum.member?(activities, boost_activity)
     assert Enum.member?(activities, activity_one)
 
-    activities = ActivityPub.fetch_activities([], %{"muting_user" => nil, "skip_preload" => true})
+    activities = ActivityPub.fetch_activities([], %{muting_user: nil, skip_preload: true})
 
     assert Enum.member?(activities, activity_two)
     assert Enum.member?(activities, activity_three)
@@ -727,7 +746,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
     {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
 
-    assert [_activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user})
+    assert [_activity_one] = ActivityPub.fetch_activities([], %{muting_user: user})
   end
 
   test "returns thread muted activities when with_muted is set" do
@@ -739,7 +758,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
 
     assert [_activity_two, _activity_one] =
-             ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true})
+             ActivityPub.fetch_activities([], %{muting_user: user, with_muted: true})
   end
 
   test "does include announces on request" do
@@ -761,7 +780,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})
     {:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user})
 
-    [activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"})
+    [activity] = ActivityPub.fetch_user_activities(user, nil, %{exclude_reblogs: true})
 
     assert activity == expected_activity
   end
@@ -804,7 +823,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       expected_activities = ActivityBuilder.insert_list(10)
       since_id = List.last(activities).id
 
-      activities = ActivityPub.fetch_public_activities(%{"since_id" => since_id})
+      activities = ActivityPub.fetch_public_activities(%{since_id: since_id})
 
       assert collect_ids(activities) == collect_ids(expected_activities)
       assert length(activities) == 10
@@ -819,7 +838,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
         |> ActivityBuilder.insert_list()
         |> List.first()
 
-      activities = ActivityPub.fetch_public_activities(%{"max_id" => max_id})
+      activities = ActivityPub.fetch_public_activities(%{max_id: max_id})
 
       assert length(activities) == 20
       assert collect_ids(activities) == collect_ids(expected_activities)
@@ -831,8 +850,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       later_activities = ActivityBuilder.insert_list(10)
 
-      activities =
-        ActivityPub.fetch_public_activities(%{"page" => "2", "page_size" => "20"}, :offset)
+      activities = ActivityPub.fetch_public_activities(%{page: "2", page_size: "20"}, :offset)
 
       assert length(activities) == 20
 
@@ -848,7 +866,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       {:ok, activity} = CommonAPI.repeat(activity.id, booster)
 
-      activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
+      activities = ActivityPub.fetch_activities([], %{muting_user: user})
 
       refute Enum.any?(activities, fn %{id: id} -> id == activity.id end)
     end
@@ -862,7 +880,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       {:ok, activity} = CommonAPI.repeat(activity.id, booster)
 
-      activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
+      activities = ActivityPub.fetch_activities([], %{muting_user: user})
 
       assert Enum.any?(activities, fn %{id: id} -> id == activity.id end)
     end
@@ -1066,7 +1084,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       assert length(activities) == 3
 
       activities =
-        ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{"user" => user1})
+        ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{user: user1})
         |> Enum.map(fn a -> a.id end)
 
       assert [public_activity.id, private_activity_1.id] == activities
@@ -1115,7 +1133,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     CommonAPI.pin(activity_three.id, user)
     user = refresh_record(user)
 
-    activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"})
+    activities = ActivityPub.fetch_user_activities(user, nil, %{pinned: true})
 
     assert 3 = length(activities)
   end
@@ -1226,7 +1244,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     activity = Repo.preload(activity, :bookmark)
     activity = %Activity{activity | thread_muted?: !!activity.thread_muted?}
 
-    assert ActivityPub.fetch_activities([], %{"user" => user}) == [activity]
+    assert ActivityPub.fetch_activities([], %{user: user}) == [activity]
   end
 
   def data_uri do
@@ -1400,7 +1418,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id]
 
-      result = ActivityPub.fetch_favourites(user, %{"limit" => 2})
+      result = ActivityPub.fetch_favourites(user, %{limit: 2})
       assert Enum.map(result, & &1.id) == [a1.id, a5.id]
     end
   end
@@ -1470,7 +1488,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
     {:ok, _reply} = CommonAPI.post(user, %{status: "yeah", in_reply_to_status_id: activity.id})
 
-    [result] = ActivityPub.fetch_public_activities(%{"exclude_replies" => "true"})
+    [result] = ActivityPub.fetch_public_activities(%{exclude_replies: true})
 
     assert result.id == activity.id
 
@@ -1483,11 +1501,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     test "public timeline", %{users: %{u1: user}} do
       activities_ids =
         %{}
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("local_only", false)
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("reply_filtering_user", user)
+        |> Map.put(:type, ["Create", "Announce"])
+        |> Map.put(:local_only, false)
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:reply_filtering_user, user)
         |> ActivityPub.fetch_public_activities()
         |> Enum.map(& &1.id)
 
@@ -1504,12 +1522,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     } do
       activities_ids =
         %{}
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("local_only", false)
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("reply_visibility", "following")
-        |> Map.put("reply_filtering_user", user)
+        |> Map.put(:type, ["Create", "Announce"])
+        |> Map.put(:local_only, false)
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:reply_visibility, "following")
+        |> Map.put(:reply_filtering_user, user)
         |> ActivityPub.fetch_public_activities()
         |> Enum.map(& &1.id)
 
@@ -1531,12 +1549,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     } do
       activities_ids =
         %{}
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("local_only", false)
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("reply_visibility", "self")
-        |> Map.put("reply_filtering_user", user)
+        |> Map.put(:type, ["Create", "Announce"])
+        |> Map.put(:local_only, false)
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:reply_visibility, "self")
+        |> Map.put(:reply_filtering_user, user)
         |> ActivityPub.fetch_public_activities()
         |> Enum.map(& &1.id)
 
@@ -1555,11 +1573,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     } do
       params =
         %{}
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("user", user)
-        |> Map.put("reply_filtering_user", user)
+        |> Map.put(:type, ["Create", "Announce"])
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:user, user)
+        |> Map.put(:reply_filtering_user, user)
 
       activities_ids =
         ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
@@ -1593,12 +1611,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     } do
       params =
         %{}
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("user", user)
-        |> Map.put("reply_visibility", "following")
-        |> Map.put("reply_filtering_user", user)
+        |> Map.put(:type, ["Create", "Announce"])
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:user, user)
+        |> Map.put(:reply_visibility, "following")
+        |> Map.put(:reply_filtering_user, user)
 
       activities_ids =
         ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
@@ -1632,12 +1650,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     } do
       params =
         %{}
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("user", user)
-        |> Map.put("reply_visibility", "self")
-        |> Map.put("reply_filtering_user", user)
+        |> Map.put(:type, ["Create", "Announce"])
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:user, user)
+        |> Map.put(:reply_visibility, "self")
+        |> Map.put(:reply_filtering_user, user)
 
       activities_ids =
         ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
@@ -1658,6 +1676,40 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       assert Enum.all?(visible_ids, &(&1 in activities_ids))
     end
+
+    test "filtering out announces where the user is the actor of the announced message" do
+      user = insert(:user)
+      other_user = insert(:user)
+      third_user = insert(:user)
+      User.follow(user, other_user)
+
+      {:ok, post} = CommonAPI.post(user, %{status: "yo"})
+      {:ok, other_post} = CommonAPI.post(third_user, %{status: "yo"})
+      {:ok, _announce} = CommonAPI.repeat(post.id, other_user)
+      {:ok, _announce} = CommonAPI.repeat(post.id, third_user)
+      {:ok, announce} = CommonAPI.repeat(other_post.id, other_user)
+
+      params = %{
+        type: ["Announce"]
+      }
+
+      results =
+        [user.ap_id | User.following(user)]
+        |> ActivityPub.fetch_activities(params)
+
+      assert length(results) == 3
+
+      params = %{
+        type: ["Announce"],
+        announce_filtering_user: user
+      }
+
+      [result] =
+        [user.ap_id | User.following(user)]
+        |> ActivityPub.fetch_activities(params)
+
+      assert result.id == announce.id
+    end
   end
 
   describe "replies filtering with private messages" do
@@ -1666,11 +1718,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     test "public timeline", %{users: %{u1: user}} do
       activities_ids =
         %{}
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("local_only", false)
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("user", user)
+        |> Map.put(:type, ["Create", "Announce"])
+        |> Map.put(:local_only, false)
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:user, user)
         |> ActivityPub.fetch_public_activities()
         |> Enum.map(& &1.id)
 
@@ -1680,13 +1732,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do
       activities_ids =
         %{}
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("local_only", false)
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("reply_visibility", "following")
-        |> Map.put("reply_filtering_user", user)
-        |> Map.put("user", user)
+        |> Map.put(:type, ["Create", "Announce"])
+        |> Map.put(:local_only, false)
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:reply_visibility, "following")
+        |> Map.put(:reply_filtering_user, user)
+        |> Map.put(:user, user)
         |> ActivityPub.fetch_public_activities()
         |> Enum.map(& &1.id)
 
@@ -1696,13 +1748,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do
       activities_ids =
         %{}
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("local_only", false)
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("reply_visibility", "self")
-        |> Map.put("reply_filtering_user", user)
-        |> Map.put("user", user)
+        |> Map.put(:type, ["Create", "Announce"])
+        |> Map.put(:local_only, false)
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:reply_visibility, "self")
+        |> Map.put(:reply_filtering_user, user)
+        |> Map.put(:user, user)
         |> ActivityPub.fetch_public_activities()
         |> Enum.map(& &1.id)
 
@@ -1712,10 +1764,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     test "home timeline", %{users: %{u1: user}} do
       params =
         %{}
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("user", user)
+        |> Map.put(:type, ["Create", "Announce"])
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:user, user)
 
       activities_ids =
         ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
@@ -1727,12 +1779,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do
       params =
         %{}
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("user", user)
-        |> Map.put("reply_visibility", "following")
-        |> Map.put("reply_filtering_user", user)
+        |> Map.put(:type, ["Create", "Announce"])
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:user, user)
+        |> Map.put(:reply_visibility, "following")
+        |> Map.put(:reply_filtering_user, user)
 
       activities_ids =
         ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
@@ -1751,12 +1803,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     } do
       params =
         %{}
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("user", user)
-        |> Map.put("reply_visibility", "self")
-        |> Map.put("reply_filtering_user", user)
+        |> Map.put(:type, ["Create", "Announce"])
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:user, user)
+        |> Map.put(:reply_visibility, "self")
+        |> Map.put(:reply_filtering_user, user)
 
       activities_ids =
         ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
@@ -2001,4 +2053,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
              end) =~ "Follower/Following counter update for #{user.ap_id} failed"
     end
   end
+
+  describe "global activity expiration" do
+    setup do: clear_config([:instance, :rewrite_policy])
+
+    test "creates an activity expiration for local Create activities" do
+      Pleroma.Config.put(
+        [:instance, :rewrite_policy],
+        Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy
+      )
+
+      {:ok, %{id: id_create}} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
+      {:ok, _follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"})
+
+      assert [%{activity_id: ^id_create}] = Pleroma.ActivityExpiration |> Repo.all()
+    end
+  end
 end
diff --git a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs
new file mode 100644 (file)
index 0000000..8babf49
--- /dev/null
@@ -0,0 +1,77 @@
+# 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.ActivityExpirationPolicyTest do
+  use ExUnit.Case, async: true
+  alias Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy
+
+  @id Pleroma.Web.Endpoint.url() <> "/activities/cofe"
+
+  test "adds `expires_at` property" do
+    assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} =
+             ActivityExpirationPolicy.filter(%{
+               "id" => @id,
+               "type" => "Create",
+               "object" => %{"type" => "Note"}
+             })
+
+    assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364
+  end
+
+  test "keeps existing `expires_at` if it less than the config setting" do
+    expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: 1)
+
+    assert {:ok, %{"type" => "Create", "expires_at" => ^expires_at}} =
+             ActivityExpirationPolicy.filter(%{
+               "id" => @id,
+               "type" => "Create",
+               "expires_at" => expires_at,
+               "object" => %{"type" => "Note"}
+             })
+  end
+
+  test "overwrites existing `expires_at` if it greater than the config setting" do
+    too_distant_future = NaiveDateTime.utc_now() |> Timex.shift(years: 2)
+
+    assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} =
+             ActivityExpirationPolicy.filter(%{
+               "id" => @id,
+               "type" => "Create",
+               "expires_at" => too_distant_future,
+               "object" => %{"type" => "Note"}
+             })
+
+    assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364
+  end
+
+  test "ignores remote activities" do
+    assert {:ok, activity} =
+             ActivityExpirationPolicy.filter(%{
+               "id" => "https://example.com/123",
+               "type" => "Create",
+               "object" => %{"type" => "Note"}
+             })
+
+    refute Map.has_key?(activity, "expires_at")
+  end
+
+  test "ignores non-Create/Note activities" do
+    assert {:ok, activity} =
+             ActivityExpirationPolicy.filter(%{
+               "id" => "https://example.com/123",
+               "type" => "Follow"
+             })
+
+    refute Map.has_key?(activity, "expires_at")
+
+    assert {:ok, activity} =
+             ActivityExpirationPolicy.filter(%{
+               "id" => "https://example.com/123",
+               "type" => "Create",
+               "object" => %{"type" => "Cofe"}
+             })
+
+    refute Map.has_key?(activity, "expires_at")
+  end
+end
index 724bae058619e61d23fb4d76980c0924cba187d3..ba1b696588336be81b94f00d854a243dbbbdf866 100644 (file)
@@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do
 
   alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy
 
-  setup do: clear_config([:mrf_user_allowlist, :localhost])
+  setup do: clear_config(:mrf_user_allowlist)
 
   test "pass filter if allow list is empty" do
     actor = insert(:user)
@@ -17,14 +17,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do
 
   test "pass filter if allow list isn't empty and user in allow list" do
     actor = insert(:user)
-    Pleroma.Config.put([:mrf_user_allowlist, :localhost], [actor.ap_id, "test-ap-id"])
+    Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => [actor.ap_id, "test-ap-id"]})
     message = %{"actor" => actor.ap_id}
     assert UserAllowListPolicy.filter(message) == {:ok, message}
   end
 
   test "rejected if allow list isn't empty and user not in allow list" do
     actor = insert(:user)
-    Pleroma.Config.put([:mrf_user_allowlist, :localhost], ["test-ap-id"])
+    Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => ["test-ap-id"]})
     message = %{"actor" => actor.ap_id}
     assert UserAllowListPolicy.filter(message) == {:reject, nil}
   end
index 7953eecf2734ee7a7393da0ebfa70fa294ba1613..31224abe0b99f45a2eaea72780b77a49f8bad957 100644 (file)
@@ -2,14 +2,264 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
   use Pleroma.DataCase
 
   alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Builder
   alias Pleroma.Web.ActivityPub.ObjectValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.CommonAPI
 
   import Pleroma.Factory
 
+  describe "attachments" do
+    test "works with honkerific attachments" do
+      attachment = %{
+        "mediaType" => "",
+        "name" => "",
+        "summary" => "298p3RG7j27tfsZ9RQ.jpg",
+        "type" => "Document",
+        "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg"
+      }
+
+      assert {:ok, attachment} =
+               AttachmentValidator.cast_and_validate(attachment)
+               |> Ecto.Changeset.apply_action(:insert)
+
+      assert attachment.mediaType == "application/octet-stream"
+    end
+
+    test "it turns mastodon attachments into our attachments" do
+      attachment = %{
+        "url" =>
+          "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
+        "type" => "Document",
+        "name" => nil,
+        "mediaType" => "image/jpeg"
+      }
+
+      {:ok, attachment} =
+        AttachmentValidator.cast_and_validate(attachment)
+        |> Ecto.Changeset.apply_action(:insert)
+
+      assert [
+               %{
+                 href:
+                   "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
+                 type: "Link",
+                 mediaType: "image/jpeg"
+               }
+             ] = attachment.url
+
+      assert attachment.mediaType == "image/jpeg"
+    end
+
+    test "it handles our own uploads" do
+      user = insert(:user)
+
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "an_image.jpg"
+      }
+
+      {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+      {:ok, attachment} =
+        attachment.data
+        |> AttachmentValidator.cast_and_validate()
+        |> Ecto.Changeset.apply_action(:insert)
+
+      assert attachment.mediaType == "image/jpeg"
+    end
+  end
+
+  describe "chat message create activities" do
+    test "it is invalid if the object already exists" do
+      user = insert(:user)
+      recipient = insert(:user)
+      {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey")
+      object = Object.normalize(activity, false)
+
+      {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id])
+
+      {:error, cng} = ObjectValidator.validate(create_data, [])
+
+      assert {:object, {"The object to create already exists", []}} in cng.errors
+    end
+
+    test "it is invalid if the object data has a different `to` or `actor` field" do
+      user = insert(:user)
+      recipient = insert(:user)
+      {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey")
+
+      {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id])
+
+      {:error, cng} = ObjectValidator.validate(create_data, [])
+
+      assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors
+      assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors
+    end
+  end
+
+  describe "chat messages" do
+    setup do
+      clear_config([:instance, :remote_limit])
+      user = insert(:user)
+      recipient = insert(:user, local: false)
+
+      {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:")
+
+      %{user: user, recipient: recipient, valid_chat_message: valid_chat_message}
+    end
+
+    test "let's through some basic html", %{user: user, recipient: recipient} do
+      {:ok, valid_chat_message, _} =
+        Builder.chat_message(
+          user,
+          recipient.ap_id,
+          "hey <a href='https://example.org'>example</a> <script>alert('uguu')</script>"
+        )
+
+      assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+      assert object["content"] ==
+               "hey <a href=\"https://example.org\">example</a> alert(&#39;uguu&#39;)"
+    end
+
+    test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do
+      assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+      assert Map.put(valid_chat_message, "attachment", nil) == object
+    end
+
+    test "validates for a basic object with an attachment", %{
+      valid_chat_message: valid_chat_message,
+      user: user
+    } do
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "an_image.jpg"
+      }
+
+      {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+      valid_chat_message =
+        valid_chat_message
+        |> Map.put("attachment", attachment.data)
+
+      assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+      assert object["attachment"]
+    end
+
+    test "validates for a basic object with an attachment in an array", %{
+      valid_chat_message: valid_chat_message,
+      user: user
+    } do
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "an_image.jpg"
+      }
+
+      {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+      valid_chat_message =
+        valid_chat_message
+        |> Map.put("attachment", [attachment.data])
+
+      assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+      assert object["attachment"]
+    end
+
+    test "validates for a basic object with an attachment but without content", %{
+      valid_chat_message: valid_chat_message,
+      user: user
+    } do
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "an_image.jpg"
+      }
+
+      {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+      valid_chat_message =
+        valid_chat_message
+        |> Map.put("attachment", attachment.data)
+        |> Map.delete("content")
+
+      assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+      assert object["attachment"]
+    end
+
+    test "does not validate if the message has no content", %{
+      valid_chat_message: valid_chat_message
+    } do
+      contentless =
+        valid_chat_message
+        |> Map.delete("content")
+
+      refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, []))
+    end
+
+    test "does not validate if the message is longer than the remote_limit", %{
+      valid_chat_message: valid_chat_message
+    } do
+      Pleroma.Config.put([:instance, :remote_limit], 2)
+      refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
+    end
+
+    test "does not validate if the recipient is blocking the actor", %{
+      valid_chat_message: valid_chat_message,
+      user: user,
+      recipient: recipient
+    } do
+      Pleroma.User.block(recipient, user)
+      refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
+    end
+
+    test "does not validate if the actor or the recipient is not in our system", %{
+      valid_chat_message: valid_chat_message
+    } do
+      chat_message =
+        valid_chat_message
+        |> Map.put("actor", "https://raymoo.com/raymoo")
+
+      {:error, _} = ObjectValidator.validate(chat_message, [])
+
+      chat_message =
+        valid_chat_message
+        |> Map.put("to", ["https://raymoo.com/raymoo"])
+
+      {:error, _} = ObjectValidator.validate(chat_message, [])
+    end
+
+    test "does not validate for a message with multiple recipients", %{
+      valid_chat_message: valid_chat_message,
+      user: user,
+      recipient: recipient
+    } do
+      chat_message =
+        valid_chat_message
+        |> Map.put("to", [user.ap_id, recipient.ap_id])
+
+      assert {:error, _} = ObjectValidator.validate(chat_message, [])
+    end
+
+    test "does not validate if it doesn't concern local users" do
+      user = insert(:user, local: false)
+      recipient = insert(:user, local: false)
+
+      {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey")
+      assert {:error, _} = ObjectValidator.validate(valid_chat_message, [])
+    end
+  end
+
   describe "EmojiReacts" do
     setup do
       user = insert(:user)
index 8342131828b8c76441ba05c44ea0801d64e22b99..c8911948e7379e8f25e3a4c33df0f3e79800aa36 100644 (file)
@@ -1,3 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
 defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do
   alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
   use Pleroma.DataCase
diff --git a/test/web/activity_pub/object_validators/types/safe_text_test.exs b/test/web/activity_pub/object_validators/types/safe_text_test.exs
new file mode 100644 (file)
index 0000000..d4a5745
--- /dev/null
@@ -0,0 +1,30 @@
+# 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.Types.SafeTextTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeText
+
+  test "it lets normal text go through" do
+    text = "hey how are you"
+    assert {:ok, text} == SafeText.cast(text)
+  end
+
+  test "it removes html tags from text" do
+    text = "hey look xss <script>alert('foo')</script>"
+    assert {:ok, "hey look xss alert(&#39;foo&#39;)"} == SafeText.cast(text)
+  end
+
+  test "it keeps basic html tags" do
+    text = "hey <a href='http://gensokyo.2hu'>look</a> xss <script>alert('foo')</script>"
+
+    assert {:ok, "hey <a href=\"http://gensokyo.2hu\">look</a> xss alert(&#39;foo&#39;)"} ==
+             SafeText.cast(text)
+  end
+
+  test "errors for non-text" do
+    assert :error == SafeText.cast(1)
+  end
+end
index 26557720b5606c57e65bb81d8734d614761fa8cb..8deb64501380858e083e6757bd5d03a2e6391469 100644 (file)
@@ -33,7 +33,10 @@ defmodule Pleroma.Web.ActivityPub.PipelineTest do
         {
           Pleroma.Web.ActivityPub.SideEffects,
           [],
-          [handle: fn o, m -> {:ok, o, m} end]
+          [
+            handle: fn o, m -> {:ok, o, m} end,
+            handle_after_transaction: fn m -> m end
+          ]
         },
         {
           Pleroma.Web.Federator,
@@ -71,7 +74,7 @@ defmodule Pleroma.Web.ActivityPub.PipelineTest do
         {
           Pleroma.Web.ActivityPub.SideEffects,
           [],
-          [handle: fn o, m -> {:ok, o, m} end]
+          [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end]
         },
         {
           Pleroma.Web.Federator,
@@ -110,7 +113,7 @@ defmodule Pleroma.Web.ActivityPub.PipelineTest do
         {
           Pleroma.Web.ActivityPub.SideEffects,
           [],
-          [handle: fn o, m -> {:ok, o, m} end]
+          [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end]
         },
         {
           Pleroma.Web.Federator,
index a80104ea78cdd44f047781ad59ae4aef802da42e..6bbbaae87c620aa0e9bb66e7e858087cb9f492a4 100644 (file)
@@ -7,6 +7,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
   use Pleroma.DataCase
 
   alias Pleroma.Activity
+  alias Pleroma.Chat
+  alias Pleroma.Chat.MessageReference
   alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Repo
@@ -20,6 +22,48 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
   import Pleroma.Factory
   import Mock
 
+  describe "handle_after_transaction" do
+    test "it streams out notifications and streams" do
+      author = insert(:user, local: true)
+      recipient = insert(:user, local: true)
+
+      {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+
+      {:ok, create_activity_data, _meta} =
+        Builder.create(author, chat_message_data["id"], [recipient.ap_id])
+
+      {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+      {:ok, _create_activity, meta} =
+        SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
+
+      assert [notification] = meta[:notifications]
+
+      with_mocks([
+        {
+          Pleroma.Web.Streamer,
+          [],
+          [
+            stream: fn _, _ -> nil end
+          ]
+        },
+        {
+          Pleroma.Web.Push,
+          [],
+          [
+            send: fn _ -> nil end
+          ]
+        }
+      ]) do
+        SideEffects.handle_after_transaction(meta)
+
+        assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
+        assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
+        assert called(Pleroma.Web.Push.send(notification))
+      end
+    end
+  end
+
   describe "delete objects" do
     setup do
       user = insert(:user)
@@ -290,6 +334,147 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
     end
   end
 
+  describe "creation of ChatMessages" do
+    test "notifies the recipient" do
+      author = insert(:user, local: false)
+      recipient = insert(:user, local: true)
+
+      {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+
+      {:ok, create_activity_data, _meta} =
+        Builder.create(author, chat_message_data["id"], [recipient.ap_id])
+
+      {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+      {:ok, _create_activity, _meta} =
+        SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
+
+      assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id)
+    end
+
+    test "it streams the created ChatMessage" do
+      author = insert(:user, local: true)
+      recipient = insert(:user, local: true)
+
+      {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+
+      {:ok, create_activity_data, _meta} =
+        Builder.create(author, chat_message_data["id"], [recipient.ap_id])
+
+      {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+      {:ok, _create_activity, meta} =
+        SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
+
+      assert [_, _] = meta[:streamables]
+    end
+
+    test "it creates a Chat and MessageReferences for the local users and bumps the unread count, except for the author" do
+      author = insert(:user, local: true)
+      recipient = insert(:user, local: true)
+
+      {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+
+      {:ok, create_activity_data, _meta} =
+        Builder.create(author, chat_message_data["id"], [recipient.ap_id])
+
+      {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+      with_mocks([
+        {
+          Pleroma.Web.Streamer,
+          [],
+          [
+            stream: fn _, _ -> nil end
+          ]
+        },
+        {
+          Pleroma.Web.Push,
+          [],
+          [
+            send: fn _ -> nil end
+          ]
+        }
+      ]) do
+        {:ok, _create_activity, meta} =
+          SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
+
+        # The notification gets created
+        assert [notification] = meta[:notifications]
+        assert notification.activity_id == create_activity.id
+
+        # But it is not sent out
+        refute called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
+        refute called(Pleroma.Web.Push.send(notification))
+
+        # Same for the user chat stream
+        assert [{topics, _}, _] = meta[:streamables]
+        assert topics == ["user", "user:pleroma_chat"]
+        refute called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
+
+        chat = Chat.get(author.id, recipient.ap_id)
+
+        [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all()
+
+        assert cm_ref.object.data["content"] == "hey"
+        assert cm_ref.unread == false
+
+        chat = Chat.get(recipient.id, author.ap_id)
+
+        [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all()
+
+        assert cm_ref.object.data["content"] == "hey"
+        assert cm_ref.unread == true
+      end
+    end
+
+    test "it creates a Chat for the local users and bumps the unread count" do
+      author = insert(:user, local: false)
+      recipient = insert(:user, local: true)
+
+      {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+
+      {:ok, create_activity_data, _meta} =
+        Builder.create(author, chat_message_data["id"], [recipient.ap_id])
+
+      {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+      {:ok, _create_activity, _meta} =
+        SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
+
+      # An object is created
+      assert Object.get_by_ap_id(chat_message_data["id"])
+
+      # The remote user won't get a chat
+      chat = Chat.get(author.id, recipient.ap_id)
+      refute chat
+
+      # The local user will get a chat
+      chat = Chat.get(recipient.id, author.ap_id)
+      assert chat
+
+      author = insert(:user, local: true)
+      recipient = insert(:user, local: true)
+
+      {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+
+      {:ok, create_activity_data, _meta} =
+        Builder.create(author, chat_message_data["id"], [recipient.ap_id])
+
+      {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+      {:ok, _create_activity, _meta} =
+        SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
+
+      # Both users are local and get the chat
+      chat = Chat.get(author.id, recipient.ap_id)
+      assert chat
+
+      chat = Chat.get(recipient.id, author.ap_id)
+      assert chat
+    end
+  end
+
   describe "announce objects" do
     setup do
       poster = insert(:user)
diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs
new file mode 100644 (file)
index 0000000..d6736dc
--- /dev/null
@@ -0,0 +1,153 @@
+# 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.Transmogrifier.ChatMessageTest do
+  use Pleroma.DataCase
+
+  import Pleroma.Factory
+
+  alias Pleroma.Activity
+  alias Pleroma.Chat
+  alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.Transmogrifier
+
+  describe "handle_incoming" do
+    test "handles chonks with attachment" do
+      data = %{
+        "@context" => "https://www.w3.org/ns/activitystreams",
+        "actor" => "https://honk.tedunangst.com/u/tedu",
+        "id" => "https://honk.tedunangst.com/u/tedu/honk/x6gt8X8PcyGkQcXxzg1T",
+        "object" => %{
+          "attachment" => [
+            %{
+              "mediaType" => "image/jpeg",
+              "name" => "298p3RG7j27tfsZ9RQ.jpg",
+              "summary" => "298p3RG7j27tfsZ9RQ.jpg",
+              "type" => "Document",
+              "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg"
+            }
+          ],
+          "attributedTo" => "https://honk.tedunangst.com/u/tedu",
+          "content" => "",
+          "id" => "https://honk.tedunangst.com/u/tedu/chonk/26L4wl5yCbn4dr4y1b",
+          "published" => "2020-05-18T01:13:03Z",
+          "to" => [
+            "https://dontbulling.me/users/lain"
+          ],
+          "type" => "ChatMessage"
+        },
+        "published" => "2020-05-18T01:13:03Z",
+        "to" => [
+          "https://dontbulling.me/users/lain"
+        ],
+        "type" => "Create"
+      }
+
+      _user = insert(:user, ap_id: data["actor"])
+      _user = insert(:user, ap_id: hd(data["to"]))
+
+      assert {:ok, _activity} = Transmogrifier.handle_incoming(data)
+    end
+
+    test "it rejects messages that don't contain content" do
+      data =
+        File.read!("test/fixtures/create-chat-message.json")
+        |> Poison.decode!()
+
+      object =
+        data["object"]
+        |> Map.delete("content")
+
+      data =
+        data
+        |> Map.put("object", object)
+
+      _author =
+        insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now())
+
+      _recipient =
+        insert(:user,
+          ap_id: List.first(data["to"]),
+          local: true,
+          last_refreshed_at: DateTime.utc_now()
+        )
+
+      {:error, _} = Transmogrifier.handle_incoming(data)
+    end
+
+    test "it rejects messages that don't concern local users" do
+      data =
+        File.read!("test/fixtures/create-chat-message.json")
+        |> Poison.decode!()
+
+      _author =
+        insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now())
+
+      _recipient =
+        insert(:user,
+          ap_id: List.first(data["to"]),
+          local: false,
+          last_refreshed_at: DateTime.utc_now()
+        )
+
+      {:error, _} = Transmogrifier.handle_incoming(data)
+    end
+
+    test "it rejects messages where the `to` field of activity and object don't match" do
+      data =
+        File.read!("test/fixtures/create-chat-message.json")
+        |> Poison.decode!()
+
+      author = insert(:user, ap_id: data["actor"])
+      _recipient = insert(:user, ap_id: List.first(data["to"]))
+
+      data =
+        data
+        |> Map.put("to", author.ap_id)
+
+      assert match?({:error, _}, Transmogrifier.handle_incoming(data))
+      refute Object.get_by_ap_id(data["object"]["id"])
+    end
+
+    test "it fetches the actor if they aren't in our system" do
+      Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+
+      data =
+        File.read!("test/fixtures/create-chat-message.json")
+        |> Poison.decode!()
+        |> Map.put("actor", "http://mastodon.example.org/users/admin")
+        |> put_in(["object", "actor"], "http://mastodon.example.org/users/admin")
+
+      _recipient = insert(:user, ap_id: List.first(data["to"]), local: true)
+
+      {:ok, %Activity{} = _activity} = Transmogrifier.handle_incoming(data)
+    end
+
+    test "it inserts it and creates a chat" do
+      data =
+        File.read!("test/fixtures/create-chat-message.json")
+        |> Poison.decode!()
+
+      author =
+        insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now())
+
+      recipient = insert(:user, ap_id: List.first(data["to"]), local: true)
+
+      {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data)
+      assert activity.local == false
+
+      assert activity.actor == author.ap_id
+      assert activity.recipients == [recipient.ap_id, author.ap_id]
+
+      %Object{} = object = Object.get_by_ap_id(activity.data["object"])
+
+      assert object
+      assert object.data["content"] == "You expected a cute girl? Too bad. alert(&#39;XSS&#39;)"
+      assert match?(%{"firefox" => _}, object.data["emoji"])
+
+      refute Chat.get(author.id, recipient.ap_id)
+      assert Chat.get(recipient.id, author.ap_id)
+    end
+  end
+end
index 967389fae465407cb372548a203eb2d1de270e28..06c39eed67e074c00e83d4000e2435c6ee7d7491 100644 (file)
@@ -5,6 +5,7 @@
 defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
   use Pleroma.DataCase
   alias Pleroma.Activity
+  alias Pleroma.Notification
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.Transmogrifier
@@ -12,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
 
   import Pleroma.Factory
   import Ecto.Query
+  import Mock
 
   setup_all do
     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -57,9 +59,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
       activity = Repo.get(Activity, activity.id)
       assert activity.data["state"] == "accept"
       assert User.following?(User.get_cached_by_ap_id(data["actor"]), user)
+
+      [notification] = Notification.for_user(user)
+      assert notification.type == "follow"
     end
 
-    test "with locked accounts, it does not create a follow or an accept" do
+    test "with locked accounts, it does create a Follow, but not an Accept" do
       user = insert(:user, locked: true)
 
       data =
@@ -81,6 +86,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
         |> Repo.all()
 
       assert Enum.empty?(accepts)
+
+      [notification] = Notification.for_user(user)
+      assert notification.type == "follow_request"
     end
 
     test "it works for follow requests when you are already followed, creating a new accept activity" do
@@ -144,6 +152,23 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
       assert activity.data["state"] == "reject"
     end
 
+    test "it rejects incoming follow requests if the following errors for some reason" do
+      user = insert(:user)
+
+      data =
+        File.read!("test/fixtures/mastodon-follow-activity.json")
+        |> Poison.decode!()
+        |> Map.put("object", user.ap_id)
+
+      with_mock Pleroma.User, [:passthrough], follow: fn _, _ -> {:error, :testing} end do
+        {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data)
+
+        %Activity{} = activity = Activity.get_by_ap_id(id)
+
+        assert activity.data["state"] == "reject"
+      end
+    end
+
     test "it works for incoming follow requests from hubzilla" do
       user = insert(:user)
 
index 2aaec510dcd2fd1c86efe74b404862e2ca6e72bd..e3d3ccb8d190a2b94b50145e4861d155245c1013 100644 (file)
@@ -337,7 +337,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
         "tags" => [],
         "avatar" => User.avatar_url(user) |> MediaProxy.url(),
         "display_name" => HTML.strip_tags(user.name || user.nickname),
-        "confirmation_pending" => false
+        "confirmation_pending" => false,
+        "url" => user.ap_id
       }
 
       assert expected == json_response(conn, 200)
@@ -614,7 +615,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "tags" => [],
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(admin.name || admin.nickname),
-            "confirmation_pending" => false
+            "confirmation_pending" => false,
+            "url" => admin.ap_id
           },
           %{
             "deactivated" => user.deactivated,
@@ -625,7 +627,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "tags" => ["foo", "bar"],
             "avatar" => User.avatar_url(user) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user.name || user.nickname),
-            "confirmation_pending" => false
+            "confirmation_pending" => false,
+            "url" => user.ap_id
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -697,7 +700,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
-                   "confirmation_pending" => false
+                   "confirmation_pending" => false,
+                   "url" => user.ap_id
                  }
                ]
              }
@@ -722,7 +726,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
-                   "confirmation_pending" => false
+                   "confirmation_pending" => false,
+                   "url" => user.ap_id
                  }
                ]
              }
@@ -747,7 +752,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
-                   "confirmation_pending" => false
+                   "confirmation_pending" => false,
+                   "url" => user.ap_id
                  }
                ]
              }
@@ -772,7 +778,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
-                   "confirmation_pending" => false
+                   "confirmation_pending" => false,
+                   "url" => user.ap_id
                  }
                ]
              }
@@ -797,7 +804,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
-                   "confirmation_pending" => false
+                   "confirmation_pending" => false,
+                   "url" => user.ap_id
                  }
                ]
              }
@@ -822,7 +830,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
-                   "confirmation_pending" => false
+                   "confirmation_pending" => false,
+                   "url" => user.ap_id
                  }
                ]
              }
@@ -842,7 +851,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "tags" => [],
                    "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user2.name || user2.nickname),
-                   "confirmation_pending" => false
+                   "confirmation_pending" => false,
+                   "url" => user2.ap_id
                  }
                ]
              }
@@ -874,7 +884,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
-                   "confirmation_pending" => false
+                   "confirmation_pending" => false,
+                   "url" => user.ap_id
                  }
                ]
              }
@@ -899,7 +910,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "tags" => [],
             "avatar" => User.avatar_url(user) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user.name || user.nickname),
-            "confirmation_pending" => false
+            "confirmation_pending" => false,
+            "url" => user.ap_id
           },
           %{
             "deactivated" => admin.deactivated,
@@ -910,7 +922,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "tags" => [],
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(admin.name || admin.nickname),
-            "confirmation_pending" => false
+            "confirmation_pending" => false,
+            "url" => admin.ap_id
           },
           %{
             "deactivated" => false,
@@ -921,7 +934,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "tags" => [],
             "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname),
-            "confirmation_pending" => false
+            "confirmation_pending" => false,
+            "url" => old_admin.ap_id
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -951,7 +965,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "tags" => [],
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(admin.name || admin.nickname),
-            "confirmation_pending" => false
+            "confirmation_pending" => false,
+            "url" => admin.ap_id
           },
           %{
             "deactivated" => false,
@@ -962,7 +977,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "tags" => [],
             "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname),
-            "confirmation_pending" => false
+            "confirmation_pending" => false,
+            "url" => second_admin.ap_id
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -994,7 +1010,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "tags" => [],
                    "avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(moderator.name || moderator.nickname),
-                   "confirmation_pending" => false
+                   "confirmation_pending" => false,
+                   "url" => moderator.ap_id
                  }
                ]
              }
@@ -1019,7 +1036,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "tags" => ["first"],
             "avatar" => User.avatar_url(user1) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user1.name || user1.nickname),
-            "confirmation_pending" => false
+            "confirmation_pending" => false,
+            "url" => user1.ap_id
           },
           %{
             "deactivated" => false,
@@ -1030,7 +1048,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "tags" => ["second"],
             "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user2.name || user2.nickname),
-            "confirmation_pending" => false
+            "confirmation_pending" => false,
+            "url" => user2.ap_id
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -1069,7 +1088,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
-                   "confirmation_pending" => false
+                   "confirmation_pending" => false,
+                   "url" => user.ap_id
                  }
                ]
              }
@@ -1093,7 +1113,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "tags" => [],
                    "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(admin.name || admin.nickname),
-                   "confirmation_pending" => false
+                   "confirmation_pending" => false,
+                   "url" => admin.ap_id
                  }
                ]
              }
@@ -1155,7 +1176,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                "tags" => [],
                "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                "display_name" => HTML.strip_tags(user.name || user.nickname),
-               "confirmation_pending" => false
+               "confirmation_pending" => false,
+               "url" => user.ap_id
              }
 
     log_entry = Repo.one(ModerationLog)
@@ -1604,57 +1626,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
     end
   end
 
-  describe "relays" do
-    test "POST /relay", %{conn: conn, admin: admin} do
-      conn =
-        post(conn, "/api/pleroma/admin/relay", %{
-          relay_url: "http://mastodon.example.org/users/admin"
-        })
-
-      assert json_response(conn, 200) == "http://mastodon.example.org/users/admin"
-
-      log_entry = Repo.one(ModerationLog)
-
-      assert ModerationLog.get_log_entry_message(log_entry) ==
-               "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
-    end
-
-    test "GET /relay", %{conn: conn} do
-      relay_user = Pleroma.Web.ActivityPub.Relay.get_actor()
-
-      ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"]
-      |> Enum.each(fn ap_id ->
-        {:ok, user} = User.get_or_fetch_by_ap_id(ap_id)
-        User.follow(relay_user, user)
-      end)
-
-      conn = get(conn, "/api/pleroma/admin/relay")
-
-      assert json_response(conn, 200)["relays"] -- ["mastodon.example.org", "mstdn.io"] == []
-    end
-
-    test "DELETE /relay", %{conn: conn, admin: admin} do
-      post(conn, "/api/pleroma/admin/relay", %{
-        relay_url: "http://mastodon.example.org/users/admin"
-      })
-
-      conn =
-        delete(conn, "/api/pleroma/admin/relay", %{
-          relay_url: "http://mastodon.example.org/users/admin"
-        })
-
-      assert json_response(conn, 200) == "http://mastodon.example.org/users/admin"
-
-      [log_entry_one, log_entry_two] = Repo.all(ModerationLog)
-
-      assert ModerationLog.get_log_entry_message(log_entry_one) ==
-               "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
-
-      assert ModerationLog.get_log_entry_message(log_entry_two) ==
-               "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin"
-    end
-  end
-
   describe "instances" do
     test "GET /instances/:instance/statuses", %{conn: conn} do
       user = insert(:user, local: false, nickname: "archaeme@archae.me")
index 780de8d18f46d5b512e2d964aae94170cce1b815..064ef9bc7cb82224d6b4b5188602d5b2e9f21348 100644 (file)
@@ -57,12 +57,12 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
         ]
       } = json_response_and_validate_schema(conn, 200)
 
-      assert key1 == config1.key
-      assert key2 == config2.key
+      assert key1 == inspect(config1.key)
+      assert key2 == inspect(config2.key)
     end
 
     test "db is added to settings that are in db", %{conn: conn} do
-      _config = insert(:config, key: ":instance", value: ConfigDB.to_binary(name: "Some name"))
+      _config = insert(:config, key: ":instance", value: [name: "Some name"])
 
       %{"configs" => configs} =
         conn
@@ -83,7 +83,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
 
       config3 =
         insert(:config,
-          value: ConfigDB.to_binary(k1: :v1, k2: :v2)
+          value: [k1: :v1, k2: :v2]
         )
 
       %{"configs" => configs} =
@@ -93,42 +93,45 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
 
       assert length(configs) > 3
 
+      saved_configs = [config1, config2, config3]
+      keys = Enum.map(saved_configs, &inspect(&1.key))
+
       received_configs =
         Enum.filter(configs, fn %{"group" => group, "key" => key} ->
-          group == ":pleroma" and key in [config1.key, config2.key, config3.key]
+          group == ":pleroma" and key in keys
         end)
 
       assert length(received_configs) == 3
 
       db_keys =
         config3.value
-        |> ConfigDB.from_binary()
         |> Keyword.keys()
-        |> ConfigDB.convert()
+        |> ConfigDB.to_json_types()
+
+      keys = Enum.map(saved_configs -- [config3], &inspect(&1.key))
+
+      values = Enum.map(saved_configs, &ConfigDB.to_json_types(&1.value))
+
+      mapset_keys = MapSet.new(keys ++ db_keys)
 
       Enum.each(received_configs, fn %{"value" => value, "db" => db} ->
-        assert db in [[config1.key], [config2.key], db_keys]
+        db = MapSet.new(db)
+        assert MapSet.subset?(db, mapset_keys)
 
-        assert value in [
-                 ConfigDB.from_binary_with_convert(config1.value),
-                 ConfigDB.from_binary_with_convert(config2.value),
-                 ConfigDB.from_binary_with_convert(config3.value)
-               ]
+        assert value in values
       end)
     end
 
     test "subkeys with full update right merge", %{conn: conn} do
-      config1 =
-        insert(:config,
-          key: ":emoji",
-          value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1])
-        )
+      insert(:config,
+        key: ":emoji",
+        value: [groups: [a: 1, b: 2], key: [a: 1]]
+      )
 
-      config2 =
-        insert(:config,
-          key: ":assets",
-          value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1])
-        )
+      insert(:config,
+        key: ":assets",
+        value: [mascots: [a: 1, b: 2], key: [a: 1]]
+      )
 
       %{"configs" => configs} =
         conn
@@ -137,14 +140,14 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
 
       vals =
         Enum.filter(configs, fn %{"group" => group, "key" => key} ->
-          group == ":pleroma" and key in [config1.key, config2.key]
+          group == ":pleroma" and key in [":emoji", ":assets"]
         end)
 
       emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end)
       assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end)
 
-      emoji_val = ConfigDB.transform_with_out_binary(emoji["value"])
-      assets_val = ConfigDB.transform_with_out_binary(assets["value"])
+      emoji_val = ConfigDB.to_elixir_types(emoji["value"])
+      assets_val = ConfigDB.to_elixir_types(assets["value"])
 
       assert emoji_val[:groups] == [a: 1, b: 2]
       assert assets_val[:mascots] == [a: 1, b: 2]
@@ -277,7 +280,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                    "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]},
                    "db" => [":key5"]
                  }
-               ]
+               ],
+               "need_reboot" => false
              }
 
       assert Application.get_env(:pleroma, :key1) == "value1"
@@ -357,7 +361,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                    "value" => "https://hooks.slack.com/services/KEY",
                    "db" => [":webhook_url"]
                  }
-               ]
+               ],
+               "need_reboot" => false
              }
 
       assert Application.get_env(:quack, :level) == :info
@@ -366,14 +371,14 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
     end
 
     test "saving config with partial update", %{conn: conn} do
-      config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2))
+      insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2))
 
       conn =
         conn
         |> put_req_header("content-type", "application/json")
         |> post("/api/pleroma/admin/config", %{
           configs: [
-            %{group: config.group, key: config.key, value: [%{"tuple" => [":key3", 3]}]}
+            %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]}
           ]
         })
 
@@ -389,7 +394,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                    ],
                    "db" => [":key1", ":key2", ":key3"]
                  }
-               ]
+               ],
+               "need_reboot" => false
              }
     end
 
@@ -500,8 +506,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
     end
 
     test "saving config with nested merge", %{conn: conn} do
-      config =
-        insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2]))
+      insert(:config, key: :key1, value: [key1: 1, key2: [k1: 1, k2: 2]])
 
       conn =
         conn
@@ -509,8 +514,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
         |> post("/api/pleroma/admin/config", %{
           configs: [
             %{
-              group: config.group,
-              key: config.key,
+              group: ":pleroma",
+              key: ":key1",
               value: [
                 %{"tuple" => [":key3", 3]},
                 %{
@@ -548,7 +553,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                    ],
                    "db" => [":key1", ":key3", ":key2"]
                  }
-               ]
+               ],
+               "need_reboot" => false
              }
     end
 
@@ -588,7 +594,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                    ],
                    "db" => [":ssl_options"]
                  }
-               ]
+               ],
+               "need_reboot" => false
              }
 
       assert Application.get_env(:pleroma, :key1) == [
@@ -600,12 +607,11 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
       backends = Application.get_env(:logger, :backends)
       on_exit(fn -> Application.put_env(:logger, :backends, backends) end)
 
-      config =
-        insert(:config,
-          group: ":logger",
-          key: ":backends",
-          value: :erlang.term_to_binary([])
-        )
+      insert(:config,
+        group: :logger,
+        key: :backends,
+        value: []
+      )
 
       Pleroma.Config.TransferTask.load_and_update_env([], false)
 
@@ -617,8 +623,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
         |> post("/api/pleroma/admin/config", %{
           configs: [
             %{
-              group: config.group,
-              key: config.key,
+              group: ":logger",
+              key: ":backends",
               value: [":console"]
             }
           ]
@@ -634,7 +640,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                    ],
                    "db" => [":backends"]
                  }
-               ]
+               ],
+               "need_reboot" => false
              }
 
       assert Application.get_env(:logger, :backends) == [
@@ -643,19 +650,18 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
     end
 
     test "saving full setting if value is not keyword", %{conn: conn} do
-      config =
-        insert(:config,
-          group: ":tesla",
-          key: ":adapter",
-          value: :erlang.term_to_binary(Tesla.Adapter.Hackey)
-        )
+      insert(:config,
+        group: :tesla,
+        key: :adapter,
+        value: Tesla.Adapter.Hackey
+      )
 
       conn =
         conn
         |> put_req_header("content-type", "application/json")
         |> post("/api/pleroma/admin/config", %{
           configs: [
-            %{group: config.group, key: config.key, value: "Tesla.Adapter.Httpc"}
+            %{group: ":tesla", key: ":adapter", value: "Tesla.Adapter.Httpc"}
           ]
         })
 
@@ -667,7 +673,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                    "value" => "Tesla.Adapter.Httpc",
                    "db" => [":adapter"]
                  }
-               ]
+               ],
+               "need_reboot" => false
              }
     end
 
@@ -677,13 +684,13 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
       token: token
     } do
       ueberauth = Application.get_env(:ueberauth, Ueberauth)
-      config1 = insert(:config, key: ":keyaa1")
-      config2 = insert(:config, key: ":keyaa2")
+      insert(:config, key: :keyaa1)
+      insert(:config, key: :keyaa2)
 
       config3 =
         insert(:config,
-          group: ":ueberauth",
-          key: "Ueberauth"
+          group: :ueberauth,
+          key: Ueberauth
         )
 
       conn =
@@ -691,8 +698,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
         |> put_req_header("content-type", "application/json")
         |> post("/api/pleroma/admin/config", %{
           configs: [
-            %{group: config1.group, key: config1.key, value: "another_value"},
-            %{group: config2.group, key: config2.key, value: "another_value"}
+            %{group: ":pleroma", key: ":keyaa1", value: "another_value"},
+            %{group: ":pleroma", key: ":keyaa2", value: "another_value"}
           ]
         })
 
@@ -700,22 +707,23 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                "configs" => [
                  %{
                    "group" => ":pleroma",
-                   "key" => config1.key,
+                   "key" => ":keyaa1",
                    "value" => "another_value",
                    "db" => [":keyaa1"]
                  },
                  %{
                    "group" => ":pleroma",
-                   "key" => config2.key,
+                   "key" => ":keyaa2",
                    "value" => "another_value",
                    "db" => [":keyaa2"]
                  }
-               ]
+               ],
+               "need_reboot" => false
              }
 
       assert Application.get_env(:pleroma, :keyaa1) == "another_value"
       assert Application.get_env(:pleroma, :keyaa2) == "another_value"
-      assert Application.get_env(:ueberauth, Ueberauth) == ConfigDB.from_binary(config3.value)
+      assert Application.get_env(:ueberauth, Ueberauth) == config3.value
 
       conn =
         build_conn()
@@ -724,7 +732,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
         |> put_req_header("content-type", "application/json")
         |> post("/api/pleroma/admin/config", %{
           configs: [
-            %{group: config2.group, key: config2.key, delete: true},
+            %{group: ":pleroma", key: ":keyaa2", delete: true},
             %{
               group: ":ueberauth",
               key: "Ueberauth",
@@ -734,7 +742,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
         })
 
       assert json_response_and_validate_schema(conn, 200) == %{
-               "configs" => []
+               "configs" => [],
+               "need_reboot" => false
              }
 
       assert Application.get_env(:ueberauth, Ueberauth) == ueberauth
@@ -801,7 +810,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                      ":name"
                    ]
                  }
-               ]
+               ],
+               "need_reboot" => false
              }
     end
 
@@ -935,7 +945,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                    ],
                    "db" => [":http"]
                  }
-               ]
+               ],
+               "need_reboot" => false
              }
     end
 
@@ -1000,7 +1011,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                      ],
                      "db" => [":key2", ":key3"]
                    }
-                 ]
+                 ],
+                 "need_reboot" => false
                }
     end
 
@@ -1027,7 +1039,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                      "value" => %{"key" => "some_val"},
                      "db" => [":key1"]
                    }
-                 ]
+                 ],
+                 "need_reboot" => false
                }
     end
 
@@ -1077,16 +1090,16 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                      ":background"
                    ]
                  }
-               ]
+               ],
+               "need_reboot" => false
              }
     end
 
     test "delete part of settings by atom subkeys", %{conn: conn} do
-      config =
-        insert(:config,
-          key: ":keyaa1",
-          value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3")
-        )
+      insert(:config,
+        key: :keyaa1,
+        value: [subkey1: "val1", subkey2: "val2", subkey3: "val3"]
+      )
 
       conn =
         conn
@@ -1094,8 +1107,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
         |> post("/api/pleroma/admin/config", %{
           configs: [
             %{
-              group: config.group,
-              key: config.key,
+              group: ":pleroma",
+              key: ":keyaa1",
               subkeys: [":subkey1", ":subkey3"],
               delete: true
             }
@@ -1110,7 +1123,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
                    "value" => [%{"tuple" => [":subkey2", "val2"]}],
                    "db" => [":subkey2"]
                  }
-               ]
+               ],
+               "need_reboot" => false
              }
     end
 
@@ -1236,6 +1250,90 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
       assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == "value5"
       assert Application.get_env(:not_real, :anything) == "value6"
     end
+
+    test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do
+      clear_config(Pleroma.Upload.Filter.Mogrify)
+
+      assert conn
+             |> put_req_header("content-type", "application/json")
+             |> post("/api/pleroma/admin/config", %{
+               configs: [
+                 %{
+                   group: ":pleroma",
+                   key: "Pleroma.Upload.Filter.Mogrify",
+                   value: [
+                     %{"tuple" => [":args", ["auto-orient", "strip"]]}
+                   ]
+                 }
+               ]
+             })
+             |> json_response_and_validate_schema(200) == %{
+               "configs" => [
+                 %{
+                   "group" => ":pleroma",
+                   "key" => "Pleroma.Upload.Filter.Mogrify",
+                   "value" => [
+                     %{"tuple" => [":args", ["auto-orient", "strip"]]}
+                   ],
+                   "db" => [":args"]
+                 }
+               ],
+               "need_reboot" => false
+             }
+
+      assert Config.get(Pleroma.Upload.Filter.Mogrify) == [args: ["auto-orient", "strip"]]
+
+      assert conn
+             |> put_req_header("content-type", "application/json")
+             |> post("/api/pleroma/admin/config", %{
+               configs: [
+                 %{
+                   group: ":pleroma",
+                   key: "Pleroma.Upload.Filter.Mogrify",
+                   value: [
+                     %{
+                       "tuple" => [
+                         ":args",
+                         [
+                           "auto-orient",
+                           "strip",
+                           "{\"implode\", \"1\"}",
+                           "{\"resize\", \"3840x1080>\"}"
+                         ]
+                       ]
+                     }
+                   ]
+                 }
+               ]
+             })
+             |> json_response(200) == %{
+               "configs" => [
+                 %{
+                   "group" => ":pleroma",
+                   "key" => "Pleroma.Upload.Filter.Mogrify",
+                   "value" => [
+                     %{
+                       "tuple" => [
+                         ":args",
+                         [
+                           "auto-orient",
+                           "strip",
+                           "{\"implode\", \"1\"}",
+                           "{\"resize\", \"3840x1080>\"}"
+                         ]
+                       ]
+                     }
+                   ],
+                   "db" => [":args"]
+                 }
+               ],
+               "need_reboot" => false
+             }
+
+      assert Config.get(Pleroma.Upload.Filter.Mogrify) == [
+               args: ["auto-orient", "strip", {"implode", "1"}, {"resize", "3840x1080>"}]
+             ]
+    end
   end
 
   describe "GET /api/pleroma/admin/config/descriptions" do
diff --git a/test/web/admin_api/controllers/relay_controller_test.exs b/test/web/admin_api/controllers/relay_controller_test.exs
new file mode 100644 (file)
index 0000000..64086ad
--- /dev/null
@@ -0,0 +1,92 @@
+# 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.RelayControllerTest do
+  use Pleroma.Web.ConnCase
+
+  import Pleroma.Factory
+
+  alias Pleroma.Config
+  alias Pleroma.ModerationLog
+  alias Pleroma.Repo
+  alias Pleroma.User
+
+  setup_all do
+    Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+
+    :ok
+  end
+
+  setup do
+    admin = insert(:user, is_admin: true)
+    token = insert(:oauth_admin_token, user: admin)
+
+    conn =
+      build_conn()
+      |> assign(:user, admin)
+      |> assign(:token, token)
+
+    {:ok, %{admin: admin, token: token, conn: conn}}
+  end
+
+  describe "relays" do
+    test "POST /relay", %{conn: conn, admin: admin} do
+      conn =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/pleroma/admin/relay", %{
+          relay_url: "http://mastodon.example.org/users/admin"
+        })
+
+      assert json_response_and_validate_schema(conn, 200) ==
+               "http://mastodon.example.org/users/admin"
+
+      log_entry = Repo.one(ModerationLog)
+
+      assert ModerationLog.get_log_entry_message(log_entry) ==
+               "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
+    end
+
+    test "GET /relay", %{conn: conn} do
+      relay_user = Pleroma.Web.ActivityPub.Relay.get_actor()
+
+      ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"]
+      |> Enum.each(fn ap_id ->
+        {:ok, user} = User.get_or_fetch_by_ap_id(ap_id)
+        User.follow(relay_user, user)
+      end)
+
+      conn = get(conn, "/api/pleroma/admin/relay")
+
+      assert json_response_and_validate_schema(conn, 200)["relays"] --
+               ["mastodon.example.org", "mstdn.io"] == []
+    end
+
+    test "DELETE /relay", %{conn: conn, admin: admin} do
+      conn
+      |> put_req_header("content-type", "application/json")
+      |> post("/api/pleroma/admin/relay", %{
+        relay_url: "http://mastodon.example.org/users/admin"
+      })
+
+      conn =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> delete("/api/pleroma/admin/relay", %{
+          relay_url: "http://mastodon.example.org/users/admin"
+        })
+
+      assert json_response_and_validate_schema(conn, 200) ==
+               "http://mastodon.example.org/users/admin"
+
+      [log_entry_one, log_entry_two] = Repo.all(ModerationLog)
+
+      assert ModerationLog.get_log_entry_message(log_entry_one) ==
+               "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
+
+      assert ModerationLog.get_log_entry_message(log_entry_two) ==
+               "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin"
+    end
+  end
+end
index 2291f76dd31f151458789a6000f20d5994f32a6a..6bd26050ef003b04cf763f1f6e599de6b900942e 100644 (file)
@@ -5,7 +5,9 @@
 defmodule Pleroma.Web.CommonAPITest do
   use Pleroma.DataCase
   alias Pleroma.Activity
+  alias Pleroma.Chat
   alias Pleroma.Conversation.Participation
+  alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
@@ -23,6 +25,150 @@ defmodule Pleroma.Web.CommonAPITest do
   setup do: clear_config([:instance, :limit])
   setup do: clear_config([:instance, :max_pinned_statuses])
 
+  describe "posting chat messages" do
+    setup do: clear_config([:instance, :chat_limit])
+
+    test "it posts a chat message without content but with an attachment" do
+      author = insert(:user)
+      recipient = insert(:user)
+
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "an_image.jpg"
+      }
+
+      {:ok, upload} = ActivityPub.upload(file, actor: author.ap_id)
+
+      with_mocks([
+        {
+          Pleroma.Web.Streamer,
+          [],
+          [
+            stream: fn _, _ ->
+              nil
+            end
+          ]
+        },
+        {
+          Pleroma.Web.Push,
+          [],
+          [
+            send: fn _ -> nil end
+          ]
+        }
+      ]) do
+        {:ok, activity} =
+          CommonAPI.post_chat_message(
+            author,
+            recipient,
+            nil,
+            media_id: upload.id
+          )
+
+        notification =
+          Notification.for_user_and_activity(recipient, activity)
+          |> Repo.preload(:activity)
+
+        assert called(Pleroma.Web.Push.send(notification))
+        assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
+        assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
+
+        assert activity
+      end
+    end
+
+    test "it adds html newlines" do
+      author = insert(:user)
+      recipient = insert(:user)
+
+      other_user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post_chat_message(
+          author,
+          recipient,
+          "uguu\nuguuu"
+        )
+
+      assert other_user.ap_id not in activity.recipients
+
+      object = Object.normalize(activity, false)
+
+      assert object.data["content"] == "uguu<br/>uguuu"
+    end
+
+    test "it linkifies" do
+      author = insert(:user)
+      recipient = insert(:user)
+
+      other_user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post_chat_message(
+          author,
+          recipient,
+          "https://example.org is the site of @#{other_user.nickname} #2hu"
+        )
+
+      assert other_user.ap_id not in activity.recipients
+
+      object = Object.normalize(activity, false)
+
+      assert object.data["content"] ==
+               "<a href=\"https://example.org\" rel=\"ugc\">https://example.org</a> is the site of <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{
+                 other_user.id
+               }\" href=\"#{other_user.ap_id}\" rel=\"ugc\">@<span>#{other_user.nickname}</span></a></span> <a class=\"hashtag\" data-tag=\"2hu\" href=\"http://localhost:4001/tag/2hu\">#2hu</a>"
+    end
+
+    test "it posts a chat message" do
+      author = insert(:user)
+      recipient = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post_chat_message(
+          author,
+          recipient,
+          "a test message <script>alert('uuu')</script> :firefox:"
+        )
+
+      assert activity.data["type"] == "Create"
+      assert activity.local
+      object = Object.normalize(activity)
+
+      assert object.data["type"] == "ChatMessage"
+      assert object.data["to"] == [recipient.ap_id]
+
+      assert object.data["content"] ==
+               "a test message &lt;script&gt;alert(&#39;uuu&#39;)&lt;/script&gt; :firefox:"
+
+      assert object.data["emoji"] == %{
+               "firefox" => "http://localhost:4001/emoji/Firefox.gif"
+             }
+
+      assert Chat.get(author.id, recipient.ap_id)
+      assert Chat.get(recipient.id, author.ap_id)
+
+      assert :ok == Pleroma.Web.Federator.perform(:publish, activity)
+    end
+
+    test "it reject messages over the local limit" do
+      Pleroma.Config.put([:instance, :chat_limit], 2)
+
+      author = insert(:user)
+      recipient = insert(:user)
+
+      {:error, message} =
+        CommonAPI.post_chat_message(
+          author,
+          recipient,
+          "123"
+        )
+
+      assert message == :content_too_long
+    end
+  end
+
   describe "unblocking" do
     test "it works even without an existing block activity" do
       blocked = insert(:user)
index e278d61f59edd46f3671707ea1cad2f487507b89..70ef0e8b5012aa9a15f7fe1fab7fc39015f325f7 100644 (file)
@@ -54,6 +54,27 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
     assert response == expected_response
   end
 
+  test "by default, does not contain pleroma:chat_mention" do
+    %{user: user, conn: conn} = oauth_access(["read:notifications"])
+    other_user = insert(:user)
+
+    {:ok, _activity} = CommonAPI.post_chat_message(other_user, user, "hey")
+
+    result =
+      conn
+      |> get("/api/v1/notifications")
+      |> json_response_and_validate_schema(200)
+
+    assert [] == result
+
+    result =
+      conn
+      |> get("/api/v1/notifications?include_types[]=pleroma:chat_mention")
+      |> json_response_and_validate_schema(200)
+
+    assert [_] = result
+  end
+
   test "getting a single notification" do
     %{user: user, conn: conn} = oauth_access(["read:notifications"])
     other_user = insert(:user)
@@ -292,6 +313,33 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
       assert public_activity.id in activity_ids
       refute unlisted_activity.id in activity_ids
     end
+
+    test "doesn't return less than the requested amount of records when the user's reply is liked" do
+      user = insert(:user)
+      %{user: other_user, conn: conn} = oauth_access(["read:notifications"])
+
+      {:ok, mention} =
+        CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "public"})
+
+      {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "public"})
+
+      {:ok, reply} =
+        CommonAPI.post(other_user, %{
+          status: ".",
+          visibility: "public",
+          in_reply_to_status_id: activity.id
+        })
+
+      {:ok, _favorite} = CommonAPI.favorite(user, reply.id)
+
+      activity_ids =
+        conn
+        |> get("/api/v1/notifications?exclude_visibilities[]=direct&limit=2")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["status"]["id"])
+
+      assert [reply.id, mention.id] == activity_ids
+    end
   end
 
   test "filters notifications using exclude_types" do
index 84d46895edcb7b436cd57ed4022c0e597975211c..c605957b126a7e1d60c868ff9c7e9b8cd97aca9d 100644 (file)
@@ -111,6 +111,44 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
                %{"name" => "prone", "url" => "#{Web.base_url()}/tag/prone"},
                %{"name" => "AccidentProne", "url" => "#{Web.base_url()}/tag/AccidentProne"}
              ]
+
+      results =
+        conn
+        |> get("/api/v2/search?#{URI.encode_query(%{q: "https://shpposter.club/users/shpuld"})}")
+        |> json_response_and_validate_schema(200)
+
+      assert results["hashtags"] == [
+               %{"name" => "shpuld", "url" => "#{Web.base_url()}/tag/shpuld"}
+             ]
+
+      results =
+        conn
+        |> get(
+          "/api/v2/search?#{
+            URI.encode_query(%{
+              q:
+                "https://www.washingtonpost.com/sports/2020/06/10/" <>
+                  "nascar-ban-display-confederate-flag-all-events-properties/"
+            })
+          }"
+        )
+        |> json_response_and_validate_schema(200)
+
+      assert results["hashtags"] == [
+               %{"name" => "nascar", "url" => "#{Web.base_url()}/tag/nascar"},
+               %{"name" => "ban", "url" => "#{Web.base_url()}/tag/ban"},
+               %{"name" => "display", "url" => "#{Web.base_url()}/tag/display"},
+               %{"name" => "confederate", "url" => "#{Web.base_url()}/tag/confederate"},
+               %{"name" => "flag", "url" => "#{Web.base_url()}/tag/flag"},
+               %{"name" => "all", "url" => "#{Web.base_url()}/tag/all"},
+               %{"name" => "events", "url" => "#{Web.base_url()}/tag/events"},
+               %{"name" => "properties", "url" => "#{Web.base_url()}/tag/properties"},
+               %{
+                 "name" => "NascarBanDisplayConfederateFlagAllEventsProperties",
+                 "url" =>
+                   "#{Web.base_url()}/tag/NascarBanDisplayConfederateFlagAllEventsProperties"
+               }
+             ]
     end
 
     test "excludes a blocked users from search results", %{conn: conn} do
index 700c82e4fc4d96a61e598010d26c2e67440b60b1..648e6f2ce54b50494cf32fa483c99b8e95e2fe48 100644 (file)
@@ -1541,14 +1541,49 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
            } = response
   end
 
+  test "favorites paginate correctly" do
+    %{user: user, conn: conn} = oauth_access(["read:favourites"])
+    other_user = insert(:user)
+    {:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"})
+    {:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"})
+    {:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"})
+
+    {:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id)
+    {:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id)
+    {:ok, third_favorite} = CommonAPI.favorite(user, second_post.id)
+
+    result =
+      conn
+      |> get("/api/v1/favourites?limit=1")
+
+    assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200)
+    assert post_id == second_post.id
+
+    # Using the header for pagination works correctly
+    [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ")
+    [_, max_id] = Regex.run(~r/max_id=(.*)>;/, next)
+
+    assert max_id == third_favorite.id
+
+    result =
+      conn
+      |> get("/api/v1/favourites?max_id=#{max_id}")
+
+    assert [%{"id" => first_post_id}, %{"id" => third_post_id}] =
+             json_response_and_validate_schema(result, 200)
+
+    assert first_post_id == first_post.id
+    assert third_post_id == third_post.id
+  end
+
   test "returns the favorites of a user" do
     %{user: user, conn: conn} = oauth_access(["read:favourites"])
     other_user = insert(:user)
 
     {:ok, _} = CommonAPI.post(other_user, %{status: "bla"})
-    {:ok, activity} = CommonAPI.post(other_user, %{status: "traps are happy"})
+    {:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"})
 
-    {:ok, _} = CommonAPI.favorite(user, activity.id)
+    {:ok, last_like} = CommonAPI.favorite(user, activity.id)
 
     first_conn = get(conn, "/api/v1/favourites")
 
@@ -1566,9 +1601,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
 
     {:ok, _} = CommonAPI.favorite(user, second_activity.id)
 
-    last_like = status["id"]
-
-    second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like}")
+    second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}")
 
     assert [second_status] = json_response_and_validate_schema(second_conn, 200)
     assert second_status["id"] == to_string(second_activity.id)
index 4aa260663d364ccf7586daded8cbc29df86213b4..d36bb1ae8f75beb1202d820eab5c97afcc1f1760 100644 (file)
@@ -58,7 +58,9 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
       result =
         conn
         |> post("/api/v1/push/subscription", %{
-          "data" => %{"alerts" => %{"mention" => true, "test" => true}},
+          "data" => %{
+            "alerts" => %{"mention" => true, "test" => true, "pleroma:chat_mention" => true}
+          },
           "subscription" => @sub
         })
         |> json_response_and_validate_schema(200)
@@ -66,7 +68,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
       [subscription] = Pleroma.Repo.all(Subscription)
 
       assert %{
-               "alerts" => %{"mention" => true},
+               "alerts" => %{"mention" => true, "pleroma:chat_mention" => true},
                "endpoint" => subscription.endpoint,
                "id" => to_string(subscription.id),
                "server_key" => @server_key
index 7ac70dc582b105827d33ba62d474ebac7f744da5..80b1f734c041da92081c3f120fac8ab0911a5ef3 100644 (file)
@@ -73,6 +73,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
         fields: []
       },
       pleroma: %{
+        ap_id: user.ap_id,
         background_image: "https://example.com/images/asuka_hospital.png",
         confirmation_pending: false,
         tags: [],
@@ -149,6 +150,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
         fields: []
       },
       pleroma: %{
+        ap_id: user.ap_id,
         background_image: nil,
         confirmation_pending: false,
         tags: [],
index 6f84366f86d0f6e69c1c02912c46717361589b83..2e8203c9b1d6b75798d561c3faca2c429aaf1a9e 100644 (file)
@@ -15,8 +15,17 @@ defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do
     user = insert(:user)
     other_user = insert(:user)
 
+    {:ok, parent} = CommonAPI.post(user, %{status: "parent"})
+
     {:ok, activity} =
-      CommonAPI.post(user, %{status: "hey @#{other_user.nickname}", visibility: "direct"})
+      CommonAPI.post(user, %{
+        status: "hey @#{other_user.nickname}",
+        visibility: "direct",
+        in_reply_to_id: parent.id
+      })
+
+    {:ok, _reply_activity} =
+      CommonAPI.post(user, %{status: "hu", visibility: "public", in_reply_to_id: parent.id})
 
     [participation] = Participation.for_user_with_last_activity_id(user)
 
index f15be1df145c0b3635871391ceefba3208f59916..9c399b2df2e94a6a4b992f9e396166dabaaad90f 100644 (file)
@@ -6,7 +6,10 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
   use Pleroma.DataCase
 
   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.CommonAPI
@@ -14,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
   alias Pleroma.Web.MastodonAPI.AccountView
   alias Pleroma.Web.MastodonAPI.NotificationView
   alias Pleroma.Web.MastodonAPI.StatusView
+  alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
   import Pleroma.Factory
 
   defp test_notifications_rendering(notifications, user, expected_result) do
@@ -31,6 +35,30 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
     assert expected_result == result
   end
 
+  test "ChatMessage notification" do
+    user = insert(:user)
+    recipient = insert(:user)
+    {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "what's up my dude")
+
+    {:ok, [notification]} = Notification.create_notifications(activity)
+
+    object = Object.normalize(activity)
+    chat = Chat.get(recipient.id, user.ap_id)
+
+    cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+    expected = %{
+      id: to_string(notification.id),
+      pleroma: %{is_seen: false},
+      type: "pleroma:chat_mention",
+      account: AccountView.render("show.json", %{user: user, for: recipient}),
+      chat_message: MessageReferenceView.render("show.json", %{chat_message_reference: cm_ref}),
+      created_at: Utils.to_masto_date(notification.inserted_at)
+    }
+
+    test_notifications_rendering([notification], recipient, [expected])
+  end
+
   test "Mention notification" do
     user = insert(:user)
     mentioned_user = insert(:user)
@@ -111,9 +139,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
     test_notifications_rendering([notification], followed, [expected])
 
     User.perform(:delete, follower)
-    notification = Notification |> Repo.one() |> Repo.preload(:activity)
-
-    test_notifications_rendering([notification], followed, [])
+    refute Repo.one(Notification)
   end
 
   @tag capture_log: true
index 9bcc07b37c50f2a240b28b29bc5e47eb363ebbc4..00925caad9b14344ca24b621059137118df2daff 100644 (file)
@@ -145,7 +145,8 @@ defmodule Pleroma.Web.NodeInfoTest do
       "shareable_emoji_packs",
       "multifetch",
       "pleroma_emoji_reactions",
-      "pleroma:api/v1/notifications:include_types_filter"
+      "pleroma:api/v1/notifications:include_types_filter",
+      "pleroma_chat_messages"
     ]
 
     assert MapSet.subset?(
diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs
new file mode 100644 (file)
index 0000000..82e1674
--- /dev/null
@@ -0,0 +1,336 @@
+# 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.ChatControllerTest do
+  use Pleroma.Web.ConnCase, async: true
+
+  alias Pleroma.Chat
+  alias Pleroma.Chat.MessageReference
+  alias Pleroma.Object
+  alias Pleroma.User
+  alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.CommonAPI
+
+  import Pleroma.Factory
+
+  describe "POST /api/v1/pleroma/chats/:id/messages/:message_id/read" do
+    setup do: oauth_access(["write:chats"])
+
+    test "it marks one message as read", %{conn: conn, user: user} do
+      other_user = insert(:user)
+
+      {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup")
+      {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2")
+      {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+      object = Object.normalize(create, false)
+      cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+      assert cm_ref.unread == true
+
+      result =
+        conn
+        |> post("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}/read")
+        |> json_response_and_validate_schema(200)
+
+      assert result["unread"] == false
+
+      cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+      assert cm_ref.unread == false
+    end
+  end
+
+  describe "POST /api/v1/pleroma/chats/:id/read" do
+    setup do: oauth_access(["write:chats"])
+
+    test "given a `last_read_id`, it marks everything until then as read", %{
+      conn: conn,
+      user: user
+    } do
+      other_user = insert(:user)
+
+      {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup")
+      {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2")
+      {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+      object = Object.normalize(create, false)
+      cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+      assert cm_ref.unread == true
+
+      result =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/v1/pleroma/chats/#{chat.id}/read", %{"last_read_id" => cm_ref.id})
+        |> json_response_and_validate_schema(200)
+
+      assert result["unread"] == 1
+
+      cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+      assert cm_ref.unread == false
+    end
+  end
+
+  describe "POST /api/v1/pleroma/chats/:id/messages" do
+    setup do: oauth_access(["write:chats"])
+
+    test "it posts a message to the chat", %{conn: conn, user: user} do
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+
+      result =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"})
+        |> json_response_and_validate_schema(200)
+
+      assert result["content"] == "Hallo!!"
+      assert result["chat_id"] == chat.id |> to_string()
+    end
+
+    test "it fails if there is no content", %{conn: conn, user: user} do
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+
+      result =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/v1/pleroma/chats/#{chat.id}/messages")
+        |> json_response_and_validate_schema(400)
+
+      assert result
+    end
+
+    test "it works with an attachment", %{conn: conn, user: user} do
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "an_image.jpg"
+      }
+
+      {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
+
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+
+      result =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{
+          "media_id" => to_string(upload.id)
+        })
+        |> json_response_and_validate_schema(200)
+
+      assert result["attachment"]
+    end
+  end
+
+  describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do
+    setup do: oauth_access(["write:chats"])
+
+    test "it deletes a message from the chat", %{conn: conn, user: user} do
+      recipient = insert(:user)
+
+      {:ok, message} =
+        CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend")
+
+      {:ok, other_message} = CommonAPI.post_chat_message(recipient, user, "nico nico ni")
+
+      object = Object.normalize(message, false)
+
+      chat = Chat.get(user.id, recipient.ap_id)
+
+      cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+      # Deleting your own message removes the message and the reference
+      result =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}")
+        |> json_response_and_validate_schema(200)
+
+      assert result["id"] == cm_ref.id
+      refute MessageReference.get_by_id(cm_ref.id)
+      assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id)
+
+      # Deleting other people's messages just removes the reference
+      object = Object.normalize(other_message, false)
+      cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+      result =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}")
+        |> json_response_and_validate_schema(200)
+
+      assert result["id"] == cm_ref.id
+      refute MessageReference.get_by_id(cm_ref.id)
+      assert Object.get_by_id(object.id)
+    end
+  end
+
+  describe "GET /api/v1/pleroma/chats/:id/messages" do
+    setup do: oauth_access(["read:chats"])
+
+    test "it paginates", %{conn: conn, user: user} do
+      recipient = insert(:user)
+
+      Enum.each(1..30, fn _ ->
+        {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey")
+      end)
+
+      chat = Chat.get(user.id, recipient.ap_id)
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats/#{chat.id}/messages")
+        |> json_response_and_validate_schema(200)
+
+      assert length(result) == 20
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}")
+        |> json_response_and_validate_schema(200)
+
+      assert length(result) == 10
+    end
+
+    test "it returns the messages for a given chat", %{conn: conn, user: user} do
+      other_user = insert(:user)
+      third_user = insert(:user)
+
+      {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey")
+      {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey")
+      {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?")
+      {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?")
+
+      chat = Chat.get(user.id, other_user.ap_id)
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats/#{chat.id}/messages")
+        |> json_response_and_validate_schema(200)
+
+      result
+      |> Enum.each(fn message ->
+        assert message["chat_id"] == chat.id |> to_string()
+      end)
+
+      assert length(result) == 3
+
+      # Trying to get the chat of a different user
+      result =
+        conn
+        |> assign(:user, other_user)
+        |> get("/api/v1/pleroma/chats/#{chat.id}/messages")
+
+      assert result |> json_response(404)
+    end
+  end
+
+  describe "POST /api/v1/pleroma/chats/by-account-id/:id" do
+    setup do: oauth_access(["write:chats"])
+
+    test "it creates or returns a chat", %{conn: conn} do
+      other_user = insert(:user)
+
+      result =
+        conn
+        |> post("/api/v1/pleroma/chats/by-account-id/#{other_user.id}")
+        |> json_response_and_validate_schema(200)
+
+      assert result["id"]
+    end
+  end
+
+  describe "GET /api/v1/pleroma/chats/:id" do
+    setup do: oauth_access(["read:chats"])
+
+    test "it returns a chat", %{conn: conn, user: user} do
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats/#{chat.id}")
+        |> json_response_and_validate_schema(200)
+
+      assert result["id"] == to_string(chat.id)
+    end
+  end
+
+  describe "GET /api/v1/pleroma/chats" do
+    setup do: oauth_access(["read:chats"])
+
+    test "it does not return chats with users you blocked", %{conn: conn, user: user} do
+      recipient = insert(:user)
+
+      {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id)
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats")
+        |> json_response_and_validate_schema(200)
+
+      assert length(result) == 1
+
+      User.block(user, recipient)
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats")
+        |> json_response_and_validate_schema(200)
+
+      assert length(result) == 0
+    end
+
+    test "it returns all chats", %{conn: conn, user: user} do
+      Enum.each(1..30, fn _ ->
+        recipient = insert(:user)
+        {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id)
+      end)
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats")
+        |> json_response_and_validate_schema(200)
+
+      assert length(result) == 30
+    end
+
+    test "it return a list of chats the current user is participating in, in descending order of updates",
+         %{conn: conn, user: user} do
+      har = insert(:user)
+      jafnhar = insert(:user)
+      tridi = insert(:user)
+
+      {:ok, chat_1} = Chat.get_or_create(user.id, har.ap_id)
+      :timer.sleep(1000)
+      {:ok, _chat_2} = Chat.get_or_create(user.id, jafnhar.ap_id)
+      :timer.sleep(1000)
+      {:ok, chat_3} = Chat.get_or_create(user.id, tridi.ap_id)
+      :timer.sleep(1000)
+
+      # bump the second one
+      {:ok, chat_2} = Chat.bump_or_create(user.id, jafnhar.ap_id)
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats")
+        |> json_response_and_validate_schema(200)
+
+      ids = Enum.map(result, & &1["id"])
+
+      assert ids == [
+               chat_2.id |> to_string(),
+               chat_3.id |> to_string(),
+               chat_1.id |> to_string()
+             ]
+    end
+  end
+end
diff --git a/test/web/pleroma_api/views/chat/message_reference_view_test.exs b/test/web/pleroma_api/views/chat/message_reference_view_test.exs
new file mode 100644 (file)
index 0000000..e5b1652
--- /dev/null
@@ -0,0 +1,61 @@
+# 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.MessageReferenceViewTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Chat
+  alias Pleroma.Chat.MessageReference
+  alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.CommonAPI
+  alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+
+  import Pleroma.Factory
+
+  test "it displays a chat message" do
+    user = insert(:user)
+    recipient = insert(:user)
+
+    file = %Plug.Upload{
+      content_type: "image/jpg",
+      path: Path.absname("test/fixtures/image.jpg"),
+      filename: "an_image.jpg"
+    }
+
+    {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
+    {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:")
+
+    chat = Chat.get(user.id, recipient.ap_id)
+
+    object = Object.normalize(activity)
+
+    cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+    chat_message = MessageReferenceView.render("show.json", chat_message_reference: cm_ref)
+
+    assert chat_message[:id] == cm_ref.id
+    assert chat_message[:content] == "kippis :firefox:"
+    assert chat_message[:account_id] == user.id
+    assert chat_message[:chat_id]
+    assert chat_message[:created_at]
+    assert chat_message[:unread] == false
+    assert match?([%{shortcode: "firefox"}], chat_message[:emojis])
+
+    {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk", media_id: upload.id)
+
+    object = Object.normalize(activity)
+
+    cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+    chat_message_two = MessageReferenceView.render("show.json", chat_message_reference: cm_ref)
+
+    assert chat_message_two[:id] == cm_ref.id
+    assert chat_message_two[:content] == "gkgkgk"
+    assert chat_message_two[:account_id] == recipient.id
+    assert chat_message_two[:chat_id] == chat_message[:chat_id]
+    assert chat_message_two[:attachment]
+    assert chat_message_two[:unread] == true
+  end
+end
diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs
new file mode 100644 (file)
index 0000000..14eecb1
--- /dev/null
@@ -0,0 +1,48 @@
+# 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.ChatViewTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Chat
+  alias Pleroma.Chat.MessageReference
+  alias Pleroma.Object
+  alias Pleroma.Web.CommonAPI
+  alias Pleroma.Web.CommonAPI.Utils
+  alias Pleroma.Web.MastodonAPI.AccountView
+  alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+  alias Pleroma.Web.PleromaAPI.ChatView
+
+  import Pleroma.Factory
+
+  test "it represents a chat" do
+    user = insert(:user)
+    recipient = insert(:user)
+
+    {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id)
+
+    represented_chat = ChatView.render("show.json", chat: chat)
+
+    assert represented_chat == %{
+             id: "#{chat.id}",
+             account: AccountView.render("show.json", user: recipient),
+             unread: 0,
+             last_message: nil,
+             updated_at: Utils.to_masto_date(chat.updated_at)
+           }
+
+    {:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello")
+
+    chat_message = Object.normalize(chat_message_creation, false)
+
+    {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id)
+
+    represented_chat = ChatView.render("show.json", chat: chat)
+
+    cm_ref = MessageReference.for_chat_and_object(chat, chat_message)
+
+    assert represented_chat[:last_message] ==
+             MessageReferenceView.render("show.json", chat_message_reference: cm_ref)
+  end
+end
index a826b24c90f110a28ef929df4ec9efbb550d13d9..b48952b29026ffde4f12e2017736d4875ea69063 100644 (file)
@@ -5,8 +5,10 @@
 defmodule Pleroma.Web.Push.ImplTest do
   use Pleroma.DataCase
 
+  alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.User
+  alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.Push.Impl
   alias Pleroma.Web.Push.Subscription
@@ -60,7 +62,8 @@ defmodule Pleroma.Web.Push.ImplTest do
     notif =
       insert(:notification,
         user: user,
-        activity: activity
+        activity: activity,
+        type: "mention"
       )
 
     assert Impl.perform(notif) == {:ok, [:ok, :ok]}
@@ -126,7 +129,7 @@ defmodule Pleroma.Web.Push.ImplTest do
            ) ==
              "@Bob: Lorem ipsum dolor sit amet, consectetur  adipiscing elit. Fusce sagittis fini..."
 
-    assert Impl.format_title(%{activity: activity}) ==
+    assert Impl.format_title(%{activity: activity, type: "mention"}) ==
              "New Mention"
   end
 
@@ -136,9 +139,10 @@ defmodule Pleroma.Web.Push.ImplTest do
     {:ok, _, _, activity} = CommonAPI.follow(user, other_user)
     object = Object.normalize(activity, false)
 
-    assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has followed you"
+    assert Impl.format_body(%{activity: activity, type: "follow"}, user, object) ==
+             "@Bob has followed you"
 
-    assert Impl.format_title(%{activity: activity}) ==
+    assert Impl.format_title(%{activity: activity, type: "follow"}) ==
              "New Follower"
   end
 
@@ -157,7 +161,7 @@ defmodule Pleroma.Web.Push.ImplTest do
     assert Impl.format_body(%{activity: announce_activity}, user, object) ==
              "@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur  adipiscing elit. Fusce sagittis fini..."
 
-    assert Impl.format_title(%{activity: announce_activity}) ==
+    assert Impl.format_title(%{activity: announce_activity, type: "reblog"}) ==
              "New Repeat"
   end
 
@@ -173,9 +177,10 @@ defmodule Pleroma.Web.Push.ImplTest do
     {:ok, activity} = CommonAPI.favorite(user, activity.id)
     object = Object.normalize(activity)
 
-    assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has favorited your post"
+    assert Impl.format_body(%{activity: activity, type: "favourite"}, user, object) ==
+             "@Bob has favorited your post"
 
-    assert Impl.format_title(%{activity: activity}) ==
+    assert Impl.format_title(%{activity: activity, type: "favourite"}) ==
              "New Favorite"
   end
 
@@ -193,6 +198,46 @@ defmodule Pleroma.Web.Push.ImplTest do
   end
 
   describe "build_content/3" do
+    test "builds content for chat messages" do
+      user = insert(:user)
+      recipient = insert(:user)
+
+      {:ok, chat} = CommonAPI.post_chat_message(user, recipient, "hey")
+      object = Object.normalize(chat, false)
+      [notification] = Notification.for_user(recipient)
+
+      res = Impl.build_content(notification, user, object)
+
+      assert res == %{
+               body: "@#{user.nickname}: hey",
+               title: "New Chat Message"
+             }
+    end
+
+    test "builds content for chat messages with no content" do
+      user = insert(:user)
+      recipient = insert(:user)
+
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "an_image.jpg"
+      }
+
+      {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
+
+      {:ok, chat} = CommonAPI.post_chat_message(user, recipient, nil, media_id: upload.id)
+      object = Object.normalize(chat, false)
+      [notification] = Notification.for_user(recipient)
+
+      res = Impl.build_content(notification, user, object)
+
+      assert res == %{
+               body: "@#{user.nickname}: (Attachment)",
+               title: "New Chat Message"
+             }
+    end
+
     test "hides details for notifications when privacy option enabled" do
       user = insert(:user, nickname: "Bob")
       user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true})
@@ -218,7 +263,7 @@ defmodule Pleroma.Web.Push.ImplTest do
           status: "<Lorem ipsum dolor sit amet."
         })
 
-      notif = insert(:notification, user: user2, activity: activity)
+      notif = insert(:notification, user: user2, activity: activity, type: "mention")
 
       actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
       object = Object.normalize(activity)
@@ -229,7 +274,7 @@ defmodule Pleroma.Web.Push.ImplTest do
 
       {:ok, activity} = CommonAPI.favorite(user, activity.id)
 
-      notif = insert(:notification, user: user2, activity: activity)
+      notif = insert(:notification, user: user2, activity: activity, type: "favourite")
 
       actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
       object = Object.normalize(activity)
@@ -268,7 +313,7 @@ defmodule Pleroma.Web.Push.ImplTest do
             "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
         })
 
-      notif = insert(:notification, user: user2, activity: activity)
+      notif = insert(:notification, user: user2, activity: activity, type: "mention")
 
       actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
       object = Object.normalize(activity)
@@ -281,7 +326,7 @@ defmodule Pleroma.Web.Push.ImplTest do
 
       {:ok, activity} = CommonAPI.favorite(user, activity.id)
 
-      notif = insert(:notification, user: user2, activity: activity)
+      notif = insert(:notification, user: user2, activity: activity, type: "favourite")
 
       actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
       object = Object.normalize(activity)
index e54a13bc804a43d25c38f2e92db52c8e5fd353a0..420a612c63e91d33129962b27370b9d1991f3e24 100644 (file)
@@ -60,19 +60,19 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
   test "doesn't just add a title" do
     assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/non-ogp") ==
              {:error,
-              "Found metadata was invalid or incomplete: %{url: \"http://example.com/non-ogp\"}"}
+              "Found metadata was invalid or incomplete: %{\"url\" => \"http://example.com/non-ogp\"}"}
   end
 
   test "parses ogp" do
     assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/ogp") ==
              {:ok,
               %{
-                image: "http://ia.media-imdb.com/images/rock.jpg",
-                title: "The Rock",
-                description:
+                "image" => "http://ia.media-imdb.com/images/rock.jpg",
+                "title" => "The Rock",
+                "description" =>
                   "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
-                type: "video.movie",
-                url: "http://example.com/ogp"
+                "type" => "video.movie",
+                "url" => "http://example.com/ogp"
               }}
   end
 
@@ -80,12 +80,12 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
     assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/ogp-missing-title") ==
              {:ok,
               %{
-                image: "http://ia.media-imdb.com/images/rock.jpg",
-                title: "The Rock (1996)",
-                description:
+                "image" => "http://ia.media-imdb.com/images/rock.jpg",
+                "title" => "The Rock (1996)",
+                "description" =>
                   "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
-                type: "video.movie",
-                url: "http://example.com/ogp-missing-title"
+                "type" => "video.movie",
+                "url" => "http://example.com/ogp-missing-title"
               }}
   end
 
@@ -93,12 +93,12 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
     assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/twitter-card") ==
              {:ok,
               %{
-                card: "summary",
-                site: "@flickr",
-                image: "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg",
-                title: "Small Island Developing States Photo Submission",
-                description: "View the album on Flickr.",
-                url: "http://example.com/twitter-card"
+                "card" => "summary",
+                "site" => "@flickr",
+                "image" => "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg",
+                "title" => "Small Island Developing States Photo Submission",
+                "description" => "View the album on Flickr.",
+                "url" => "http://example.com/twitter-card"
               }}
   end
 
@@ -106,27 +106,28 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
     assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/oembed") ==
              {:ok,
               %{
-                author_name: "‮‭‬bees‬",
-                author_url: "https://www.flickr.com/photos/bees/",
-                cache_age: 3600,
-                flickr_type: "photo",
-                height: "768",
-                html:
+                "author_name" => "‮‭‬bees‬",
+                "author_url" => "https://www.flickr.com/photos/bees/",
+                "cache_age" => 3600,
+                "flickr_type" => "photo",
+                "height" => "768",
+                "html" =>
                   "<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by ‮‭‬bees‬, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"></a><script async src=\"https://embedr.flickr.com/assets/client-code.js\" charset=\"utf-8\"></script>",
-                license: "All Rights Reserved",
-                license_id: 0,
-                provider_name: "Flickr",
-                provider_url: "https://www.flickr.com/",
-                thumbnail_height: 150,
-                thumbnail_url: "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg",
-                thumbnail_width: 150,
-                title: "Bacon Lollys",
-                type: "photo",
-                url: "http://example.com/oembed",
-                version: "1.0",
-                web_page: "https://www.flickr.com/photos/bees/2362225867/",
-                web_page_short_url: "https://flic.kr/p/4AK2sc",
-                width: "1024"
+                "license" => "All Rights Reserved",
+                "license_id" => 0,
+                "provider_name" => "Flickr",
+                "provider_url" => "https://www.flickr.com/",
+                "thumbnail_height" => 150,
+                "thumbnail_url" =>
+                  "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg",
+                "thumbnail_width" => 150,
+                "title" => "Bacon Lollys",
+                "type" => "photo",
+                "url" => "http://example.com/oembed",
+                "version" => "1.0",
+                "web_page" => "https://www.flickr.com/photos/bees/2362225867/",
+                "web_page_short_url" => "https://flic.kr/p/4AK2sc",
+                "width" => "1024"
               }}
   end
 
index 87c767c15ce8b279f9e78a7cdc075c5f0825cdad..219f005a2b9d7b54cdc1c53f60630385912df983 100644 (file)
@@ -7,8 +7,7 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
   alias Pleroma.Web.RichMedia.Parsers.TwitterCard
 
   test "returns error when html not contains twitter card" do
-    assert TwitterCard.parse([{"html", [], [{"head", [], []}, {"body", [], []}]}], %{}) ==
-             {:error, "No twitter card metadata found"}
+    assert TwitterCard.parse([{"html", [], [{"head", [], []}, {"body", [], []}]}], %{}) == %{}
   end
 
   test "parses twitter card with only name attributes" do
@@ -17,15 +16,21 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
       |> Floki.parse_document!()
 
     assert TwitterCard.parse(html, %{}) ==
-             {:ok,
-              %{
-                "app:id:googleplay": "com.nytimes.android",
-                "app:name:googleplay": "NYTimes",
-                "app:url:googleplay": "nytimes://reader/id/100000006583622",
-                site: nil,
-                title:
-                  "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times"
-              }}
+             %{
+               "app:id:googleplay" => "com.nytimes.android",
+               "app:name:googleplay" => "NYTimes",
+               "app:url:googleplay" => "nytimes://reader/id/100000006583622",
+               "site" => nil,
+               "description" =>
+                 "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
+               "image" =>
+                 "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg",
+               "type" => "article",
+               "url" =>
+                 "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html",
+               "title" =>
+                 "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database."
+             }
   end
 
   test "parses twitter card with only property attributes" do
@@ -34,19 +39,19 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
       |> Floki.parse_document!()
 
     assert TwitterCard.parse(html, %{}) ==
-             {:ok,
-              %{
-                card: "summary_large_image",
-                description:
-                  "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
-                image:
-                  "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg",
-                "image:alt": "",
-                title:
-                  "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.",
-                url:
-                  "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"
-              }}
+             %{
+               "card" => "summary_large_image",
+               "description" =>
+                 "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
+               "image" =>
+                 "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg",
+               "image:alt" => "",
+               "title" =>
+                 "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.",
+               "url" =>
+                 "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html",
+               "type" => "article"
+             }
   end
 
   test "parses twitter card with name & property attributes" do
@@ -55,23 +60,23 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
       |> Floki.parse_document!()
 
     assert TwitterCard.parse(html, %{}) ==
-             {:ok,
-              %{
-                "app:id:googleplay": "com.nytimes.android",
-                "app:name:googleplay": "NYTimes",
-                "app:url:googleplay": "nytimes://reader/id/100000006583622",
-                card: "summary_large_image",
-                description:
-                  "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
-                image:
-                  "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg",
-                "image:alt": "",
-                site: nil,
-                title:
-                  "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.",
-                url:
-                  "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"
-              }}
+             %{
+               "app:id:googleplay" => "com.nytimes.android",
+               "app:name:googleplay" => "NYTimes",
+               "app:url:googleplay" => "nytimes://reader/id/100000006583622",
+               "card" => "summary_large_image",
+               "description" =>
+                 "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
+               "image" =>
+                 "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg",
+               "image:alt" => "",
+               "site" => nil,
+               "title" =>
+                 "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.",
+               "url" =>
+                 "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html",
+               "type" => "article"
+             }
   end
 
   test "respect only first title tag on the page" do
@@ -84,14 +89,17 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
       File.read!("test/fixtures/margaret-corbin-grave-west-point.html") |> Floki.parse_document!()
 
     assert TwitterCard.parse(html, %{}) ==
-             {:ok,
-              %{
-                site: "@atlasobscura",
-                title:
-                  "The Missing Grave of Margaret Corbin, Revolutionary War Veteran - Atlas Obscura",
-                card: "summary_large_image",
-                image: image_path
-              }}
+             %{
+               "site" => "@atlasobscura",
+               "title" => "The Missing Grave of Margaret Corbin, Revolutionary War Veteran",
+               "card" => "summary_large_image",
+               "image" => image_path,
+               "description" =>
+                 "She's the only woman veteran honored with a monument at West Point. But where was she buried?",
+               "site_name" => "Atlas Obscura",
+               "type" => "article",
+               "url" => "http://www.atlasobscura.com/articles/margaret-corbin-grave-west-point"
+             }
   end
 
   test "takes first founded title in html head if there is html markup error" do
@@ -100,14 +108,20 @@ defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
       |> Floki.parse_document!()
 
     assert TwitterCard.parse(html, %{}) ==
-             {:ok,
-              %{
-                site: nil,
-                title:
-                  "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times",
-                "app:id:googleplay": "com.nytimes.android",
-                "app:name:googleplay": "NYTimes",
-                "app:url:googleplay": "nytimes://reader/id/100000006583622"
-              }}
+             %{
+               "site" => nil,
+               "title" =>
+                 "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.",
+               "app:id:googleplay" => "com.nytimes.android",
+               "app:name:googleplay" => "NYTimes",
+               "app:url:googleplay" => "nytimes://reader/id/100000006583622",
+               "description" =>
+                 "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
+               "image" =>
+                 "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-facebookJumbo.jpg",
+               "type" => "article",
+               "url" =>
+                 "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"
+             }
   end
 end
index 3f012259a0a07ad1a5c514c09f70eef05d723fa9..245f6e63f53f7bccab9782c236cea453ecf0f943 100644 (file)
@@ -7,11 +7,15 @@ defmodule Pleroma.Web.StreamerTest do
 
   import Pleroma.Factory
 
+  alias Pleroma.Chat
+  alias Pleroma.Chat.MessageReference
   alias Pleroma.Conversation.Participation
   alias Pleroma.List
+  alias Pleroma.Object
   alias Pleroma.User
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.Streamer
+  alias Pleroma.Web.StreamerView
 
   @moduletag needs_streamer: true, capture_log: true
 
@@ -145,6 +149,57 @@ defmodule Pleroma.Web.StreamerTest do
       refute Streamer.filtered_by_user?(user, notify)
     end
 
+    test "it sends chat messages to the 'user:pleroma_chat' stream", %{user: user} do
+      other_user = insert(:user)
+
+      {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno")
+      object = Object.normalize(create_activity, false)
+      chat = Chat.get(user.id, other_user.ap_id)
+      cm_ref = MessageReference.for_chat_and_object(chat, object)
+      cm_ref = %{cm_ref | chat: chat, object: object}
+
+      Streamer.get_topic_and_add_socket("user:pleroma_chat", user)
+      Streamer.stream("user:pleroma_chat", {user, cm_ref})
+
+      text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
+
+      assert text =~ "hey cirno"
+      assert_receive {:text, ^text}
+    end
+
+    test "it sends chat messages to the 'user' stream", %{user: user} do
+      other_user = insert(:user)
+
+      {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno")
+      object = Object.normalize(create_activity, false)
+      chat = Chat.get(user.id, other_user.ap_id)
+      cm_ref = MessageReference.for_chat_and_object(chat, object)
+      cm_ref = %{cm_ref | chat: chat, object: object}
+
+      Streamer.get_topic_and_add_socket("user", user)
+      Streamer.stream("user", {user, cm_ref})
+
+      text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
+
+      assert text =~ "hey cirno"
+      assert_receive {:text, ^text}
+    end
+
+    test "it sends chat message notifications to the 'user:notification' stream", %{user: user} do
+      other_user = insert(:user)
+
+      {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey")
+
+      notify =
+        Repo.get_by(Pleroma.Notification, user_id: user.id, activity_id: create_activity.id)
+        |> Repo.preload(:activity)
+
+      Streamer.get_topic_and_add_socket("user:notification", user)
+      Streamer.stream("user:notification", notify)
+      assert_receive {:render_with_user, _, _, ^notify}
+      refute Streamer.filtered_by_user?(user, notify)
+    end
+
     test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{
       user: user
     } do
index 5864f9e5f675f2d420cad8ac39de3b1b3ec8b94e..6d2991a6071d3b19ea5aab0d787fa86a2f007a9f 100644 (file)
@@ -11,7 +11,10 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorkerTest do
   import Pleroma.Factory
   import ExUnit.CaptureLog
 
-  setup do: clear_config([ActivityExpiration, :enabled])
+  setup do
+    clear_config([ActivityExpiration, :enabled])
+    clear_config([:instance, :rewrite_policy])
+  end
 
   test "deletes an expiration activity" do
     Pleroma.Config.put([ActivityExpiration, :enabled], true)
@@ -36,6 +39,35 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorkerTest do
     refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id)
   end
 
+  test "works with ActivityExpirationPolicy" do
+    Pleroma.Config.put([ActivityExpiration, :enabled], true)
+
+    Pleroma.Config.put(
+      [:instance, :rewrite_policy],
+      Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy
+    )
+
+    user = insert(:user)
+
+    days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365)
+
+    {:ok, %{id: id} = activity} = Pleroma.Web.CommonAPI.post(user, %{status: "cofe"})
+
+    past_date =
+      NaiveDateTime.utc_now() |> Timex.shift(days: -days) |> NaiveDateTime.truncate(:second)
+
+    activity
+    |> Repo.preload(:expiration)
+    |> Map.get(:expiration)
+    |> Ecto.Changeset.change(%{scheduled_at: past_date})
+    |> Repo.update!()
+
+    Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(:ops, :pid)
+
+    assert [%{data: %{"type" => "Delete", "deleted_activity_id" => ^id}}] =
+             Pleroma.Repo.all(Pleroma.Activity)
+  end
+
   describe "delete_activity/1" do
     test "adds log message if activity isn't find" do
       assert capture_log([level: :error], fn ->